Compare commits

...

28 Commits

Author SHA1 Message Date
134e601d57 Debug-Schritte entfernt
All checks were successful
release-tag / release-image (push) Successful in 2m13s
2025-07-31 23:13:52 +02:00
842c541c13 Hotfix - Login-Bug
Some checks failed
release-tag / release-image (push) Has been cancelled
2025-07-31 23:12:11 +02:00
41cee8af1d Berechnungs-Cache eingebaut, um Anzeige zu beschleunigen. Standard-Wert für Cache ist 6 Stunden und ist In-Memory
All checks were successful
release-tag / release-image (push) Successful in 2m18s
2025-07-31 22:32:19 +02:00
190cded4e5 Kompakte Ansicht finalisiert
All checks were successful
release-tag / release-image (push) Successful in 2m19s
2025-07-28 23:24:30 +02:00
4ecdd40613 Update-1 2025-07-28 23:03:07 +02:00
7c73ac7749 Anpassung UI
All checks were successful
release-tag / release-image (push) Successful in 2m15s
2025-07-28 17:44:15 +02:00
afc6bcae83 Images entfernt
All checks were successful
release-tag / release-image (push) Successful in 5m19s
2025-07-28 15:23:28 +02:00
ecf5cf9773 fix
All checks were successful
release-tag / release-image (push) Successful in 2m37s
2025-07-28 15:19:05 +02:00
33b2a2d966 fix max-width
All checks were successful
release-tag / release-image (push) Successful in 2m13s
2025-07-28 15:15:54 +02:00
e12f631c4a fix
All checks were successful
release-tag / release-image (push) Successful in 2m8s
2025-07-28 15:09:25 +02:00
3c71785849 Image hinzugefügt - Test
All checks were successful
release-tag / release-image (push) Successful in 2m47s
2025-07-28 15:03:31 +02:00
9308b914ff Waren hinzugefügt
All checks were successful
release-tag / release-image (push) Successful in 2m30s
2025-07-28 14:50:11 +02:00
4fccd54805 Verteilung angepasst, kleine Designanpassungen
All checks were successful
release-tag / release-image (push) Successful in 2m18s
2025-07-28 13:10:13 +02:00
83d8644ef9 Anpassungen
All checks were successful
release-tag / release-image (push) Successful in 2m53s
2025-07-28 12:26:17 +02:00
24cfcd61ed fix
All checks were successful
release-tag / release-image (push) Successful in 2m19s
2025-07-27 11:32:00 +02:00
58b7552714 Updated Multiselect um mehrere Waren angeben zu können. 2025-07-27 11:31:19 +02:00
92bff5640f Commodities aktualisiert
All checks were successful
release-tag / release-image (push) Successful in 2m8s
2025-07-26 21:25:51 +02:00
b3a35f7e74 ui fix
All checks were successful
release-tag / release-image (push) Successful in 2m16s
2025-07-26 20:59:32 +02:00
ca7eadfcb2 Änderungen an UI, POIs hinzugefügt, Suchlisten, Waren, Schiffsauswahl, Start-, Zielort und Zeitaufwand hinzugefügt.
All checks were successful
release-tag / release-image (push) Successful in 2m47s
2025-07-26 15:36:58 +02:00
d6d5b4b647 test
All checks were successful
release-tag / release-image (push) Successful in 2m55s
2025-07-25 21:09:40 +02:00
15d4bffb72 Aktualisierte Anzeige UEC und Favicon
All checks were successful
release-tag / release-image (push) Successful in 2m58s
2025-07-25 17:39:51 +02:00
ec3899677a favicon hinzugefügt
All checks were successful
release-tag / release-image (push) Successful in 2m34s
2025-07-25 15:54:52 +02:00
5bd89aa32b fix
All checks were successful
release-tag / release-image (push) Successful in 2m52s
2025-07-25 13:15:58 +02:00
4bd351cd6d Impressum hinzugefügt
All checks were successful
release-tag / release-image (push) Successful in 2m42s
2025-07-25 12:33:14 +02:00
fdd080523d Fix persistente Anzeige der Tabcontainer-Seite bei Interaktion
All checks were successful
release-tag / release-image (push) Successful in 2m50s
2025-07-24 06:52:30 +02:00
4ad790de64 tempsrc hinzugefügt, um neue Sources auf bestehende Volumes zu fixen.
All checks were successful
release-tag / release-image (push) Successful in 2m13s
2025-07-23 23:10:13 +02:00
73ea4b4040 Umstellung auf Tab-Container
All checks were successful
release-tag / release-image (push) Successful in 2m13s
2025-07-23 22:59:29 +02:00
07e19bd89f Temp 2025-07-23 22:56:52 +02:00
14 changed files with 5792 additions and 181 deletions

View File

@@ -14,16 +14,23 @@ FROM alpine:3.20
# HTTPS-Callouts in Alpine brauchen ca-certificates # HTTPS-Callouts in Alpine brauchen ca-certificates
RUN apk add --no-cache ca-certificates RUN apk add --no-cache ca-certificates
RUN mkdir /data RUN mkdir /data
RUN mkdir /dynamicsrc
RUN mkdir /tempsrc
COPY --from=builder /bin/sctradingtool /bin/sctradingtool COPY --from=builder /bin/sctradingtool /bin/sctradingtool
COPY ./static /data/static COPY ./static /data/static
COPY ./static /tempsrc/static
COPY ./dynamicsrc /dynamicsrc
# Default listens on :8080 siehe main.go # Default listens on :8080 siehe main.go
EXPOSE 8080 EXPOSE 8080
# Environment defaults; können per compose überschrieben werden # Environment defaults; können per compose überschrieben werden
ENV REDIS_ADDR=redis:6379 \ ENV KT_USERNAME=admin \
BLOCKLIST_MODE=slave \ KT_PASSWORD=admin \
HASH_NAME=bl:manual \ KT_MEMBER=guest \
MASTER_URL=https://flod-proxy.send.nrw KT_HASIMPRESSUM=true \
KT_IMPRESSUM=https://www.google.de \
KT_PRODUCTIVE=true
ENTRYPOINT ["/bin/sctradingtool"] ENTRYPOINT ["/bin/sctradingtool"]

BIN
data.db

Binary file not shown.

BIN
dynamicsrc/footerlower.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
dynamicsrc/footerupper.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

1
dynamicsrc/pois.json Normal file

File diff suppressed because one or more lines are too long

3
go.mod
View File

@@ -10,8 +10,9 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/crypto v0.40.0
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.34.0 // indirect
modernc.org/libc v1.65.10 // indirect modernc.org/libc v1.65.10 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect

30
go.sum
View File

@@ -1,5 +1,7 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -8,16 +10,40 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA=
modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc= modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc=
modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po= modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI= modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE= modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

648
main.go
View File

@@ -1,7 +1,11 @@
package main package main
import ( import (
"crypto/rand"
"database/sql" "database/sql"
"encoding/hex"
"encoding/json"
"fmt"
"html/template" "html/template"
"log" "log"
"math" "math"
@@ -10,11 +14,16 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"golang.org/x/crypto/bcrypt"
_ "modernc.org/sqlite" // statt github.com/mattn/go-sqlite3 _ "modernc.org/sqlite" // statt github.com/mattn/go-sqlite3
) )
/* Quellen */
/* https://starmap.space/api/v3/oc/index.php */
func GetENV(k, d string) string { func GetENV(k, d string) string {
if v := os.Getenv(k); v != "" { if v := os.Getenv(k); v != "" {
return v return v
@@ -35,8 +44,81 @@ var (
password = GetENV("KT_PASSWORD", "root") password = GetENV("KT_PASSWORD", "root")
membername = GetENV("KT_MEMBER", "demo") membername = GetENV("KT_MEMBER", "demo")
productive = Enabled("KT_PRODUCTIVE", false) productive = Enabled("KT_PRODUCTIVE", false)
hasimpressum = Enabled("KT_HASIMPRESSUM", false)
impressum = GetENV("KT_IMPRESSUM", "")
hashedPassword = ""
orte = []string{}
schiffe = []string{
"", "100i", "125a", "135c", "Arrow", "Aurora CL", "Aurora ES", "Aurora LN", "Aurora LX", "Aurora MR",
"Avenger Stalker", "Avenger Titan", "Avenger Titan Renegade", "Avenger Warlock",
"Blade", "Buccaneer", "C1 Spirit", "C2 Hercules Starlifter", "M2 Hercules Starlifter", "A2 Hercules Starlifter", "C8 Pisces", "C8R Pisces Rescue",
"C8X Pisces Expedition", "Carrack", "Caterpillar", "Constellation Andromeda",
"Constellation Aquila", "Constellation Phoenix", "Constellation Taurus", "Corsair",
"Cutlass Black", "Cutlass Blue", "Cutlass Red", "Cutter", "Defender", "Eclipse",
"Freelancer", "Freelancer DUR", "Freelancer MAX", "Gladiator", "Gladius", "Glaive",
"Hammerhead", "Hawk", "Herald", "Hurricane", "Idris-P", "Idris-K", "Idris-M", "Javelin",
"Khartu-Al", "Kraken", "M50", "Merchantman", "Mercury Star Runner", "Mustang Alpha",
"Mustang Beta", "Mustang Delta", "Mustang Gamma", "Mustang Omega", "Nomad",
"Orion", "P-52 Merlin", "P-72 Archimedes", "Prospector", "Prowler", "Prowler Utility", "Raft",
"Reclaimer", "Redeemer", "Reliant Kore", "Reliant Mako", "Reliant Sen", "Reliant Tana",
"Retaliator", "Sabre Peregrine", "Starfarer Gemini", "Talon", "Talon Shrike",
"Terrapin", "Vulture", "Hull-A", "Hull-C", "Zeus ES", "Zeus CL",
// …weitere Capital- und Concept-Schiffe sind ebenfalls bekannt!
}
waren = []string{
"", "Laranite", "Titanium", "Medical Supplies", "Gold", "Agricium", "Hydrogen", "Hydrogen Fuel", "Nitrogen", "Astatine", "Processed Food", "Scrap", "Recycled Material Composite", "Agricultural Supplies",
"Iodine", "Aluminium", "Copper", "Lithium", "Silicon", "Tungsten", "Aphorite", "Beryl", "Bexalite", "Waste", "Osoian hides", "Borase", "WiDoW", "Fresh Food", "Heart of the Woods", "Pressurized Ice", "Atlasium",
"Corundum", "Diamond", "Dolivine", "Hadanite", "Hephaestanite", "Laranite", "Quartz", "Taranite", "Stims", "Carbon", "Slam", "Distilled Spirits", "Maze", "Gasping Weevil Eggs", "E'tam", "Iron", "Methane",
}
) )
type POI struct {
ItemID int `json:"item_id"`
System string `json:"System"`
Planet string `json:"Planet"`
PoiName string `json:"PoiName"`
Type string `json:"Type"`
Classification string `json:"Classification"`
Latitude float64 `json:"Latitude"`
Longitude float64 `json:"Longitude"`
Longitude360 float64 `json:"Longitude360"`
Height float64 `json:"Height"`
XCoord float64 `json:"XCoord"`
YCoord float64 `json:"YCoord"`
ZCoord float64 `json:"ZCoord"`
QTMarker int `json:"QTMarker"`
NextPOI string `json:"NextPOI"`
NextQTMarker string `json:"NextQTMarker"`
Comment string `json:"Comment"`
Submitted string `json:"Submitted"` // oder time.Time, falls du umwandelst
Introduced string `json:"Introduced"` // z.B. "Unknown"
GUID string `json:"GUID"`
POISize *string `json:"POI_Size"`
POIType *string `json:"POI_Type"`
POIEntries string `json:"POI_Entries"`
POIAccessableFoot string `json:"POI_Accessable_Foot"`
POIAccessableVehicle string `json:"POI_Accessable_Vehicle"`
POIAccessableShip string `json:"POI_Accessable_Ship"`
POIDefenses string `json:"POI_Defenses"`
POILandingPads string `json:"POI_LandingPads"`
POIVehiclePads string `json:"POI_VehiclePads"`
POITerminals string `json:"POI_Terminals"`
POIMedBay string `json:"POI_MedBay"`
POIServices string `json:"POI_Services"`
POIAtmosphere *string `json:"POI_Atmosphere"`
POINPCs string `json:"POI_NPCs"`
ZoneArmistice int `json:"Zone_Armistice"`
ZoneNoFly int `json:"Zone_NoFly"`
ZoneTrespassing int `json:"Zone_Trespassing"`
ZoneBiome string `json:"Zone_Biome"`
ZoneGravitation int `json:"Zone_Gravitation"`
ZoneTemperatureMin string `json:"Zone_Temperature_Min"`
ZoneTemperatureMax string `json:"Zone_Temperature_Max"`
Minerals *string `json:"Minerals"`
SpecialLoot string `json:"SpecialLoot"`
Missions string `json:"Missions"`
}
type Entry struct { type Entry struct {
ID int ID int
Anfangsbestand float64 Anfangsbestand float64
@@ -46,6 +128,11 @@ type Entry struct {
Gesamtwert float64 Gesamtwert float64
Bezahlt bool Bezahlt bool
CreatedAt string CreatedAt string
Startort string
Zielort string
Schiff string
Ware string
Zeitaufwand float64
} }
type Abteilung struct { type Abteilung struct {
@@ -61,6 +148,9 @@ type Monatsstatistik struct {
Monat string // z.B. "07.2025" Monat string // z.B. "07.2025"
Summe float64 // bezahlte Summe float64 // bezahlte
SummeOffen float64 // noch nicht bezahlt SummeOffen float64 // noch nicht bezahlt
Prozent float64
ProzentOffen float64
Eintraege []Entry
} }
var tmpl = template.Must(template.New("form").Funcs(template.FuncMap{ var tmpl = template.Must(template.New("form").Funcs(template.FuncMap{
@@ -104,18 +194,78 @@ func reverse(s string) string {
func isAuthenticated(r *http.Request) bool { func isAuthenticated(r *http.Request) bool {
cookie, err := r.Cookie("session") cookie, err := r.Cookie("session")
return err == nil && cookie.Value == "authenticated" if err != nil {
return false
}
// Prüfen, ob der Token im sessionStore existiert
_, ok := sessionStore[cookie.Value]
return ok
}
var sessionStore = make(map[string]string) // token → username
var loginAttempts = make(map[string]int)
var loginLastAttempt = make(map[string]time.Time)
var loginBlockedUntil = make(map[string]time.Time)
var loginMutex sync.Mutex
func hashPassword(pw string) string {
hash, _ := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
return string(hash)
}
func checkPasswordHash(pw, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw))
return err == nil
}
func generateSessionToken() string {
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
return "" // handle error besser im echten Code
}
return hex.EncodeToString(b)
} }
func main() { func main() {
var ( var (
db *sql.DB db *sql.DB
err error err error
data []byte
) )
if productive { if productive {
db, err = sql.Open("sqlite", "/data/data.db") db, err = sql.Open("sqlite", "/data/data.db")
if err != nil {
panic(err)
}
data, err = os.ReadFile("/dynamicsrc/pois.json")
if err != nil {
panic(err)
}
} else { } else {
db, err = sql.Open("sqlite", "./data.db") db, err = sql.Open("sqlite", "./data.db")
if err != nil {
panic(err)
}
data, err = os.ReadFile("./dynamicsrc/pois.json")
if err != nil {
panic(err)
}
}
hashedPassword = hashPassword(password)
var pois []POI
if err := json.Unmarshal(data, &pois); err != nil {
panic(err)
}
orte = append(orte, "")
for _, poi := range pois {
if poi.System == "Stanton" || poi.System == "Pyro" {
formatted := fmt.Sprintf("%s - %s - %s (%s)", poi.System, poi.Planet, poi.PoiName, poi.Type)
orte = append(orte, formatted)
}
} }
// //
if err != nil { if err != nil {
@@ -132,34 +282,86 @@ func main() {
} }
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
ip := strings.Split(r.RemoteAddr, ":")[0]
loginMutex.Lock()
blockUntil, blocked := loginBlockedUntil[ip]
if blocked && time.Now().Before(blockUntil) {
loginMutex.Unlock()
http.Error(w, "Zu viele Fehlversuche. Bitte versuch es später erneut.", http.StatusTooManyRequests)
return
}
loginMutex.Unlock()
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
r.ParseForm() r.ParseForm()
user := r.FormValue("username") user := r.FormValue("username")
pass := r.FormValue("password") pass := r.FormValue("password")
if user == username && pass == password {
if user == username && checkPasswordHash(pass, hashedPassword) {
token := generateSessionToken()
// Speichere Session
sessionStore[token] = user
// Cookie setzen
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: "session", Name: "session",
Value: "authenticated", Value: token,
Path: "/", Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
}) })
// Erfolgreich -> Versuche zurücksetzen
loginMutex.Lock()
delete(loginAttempts, ip)
delete(loginLastAttempt, ip)
delete(loginBlockedUntil, ip)
loginMutex.Unlock()
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
// Fehlversuch behandeln
loginMutex.Lock()
loginAttempts[ip]++
loginLastAttempt[ip] = time.Now()
if loginAttempts[ip] >= 5 {
loginBlockedUntil[ip] = time.Now().Add(10 * time.Minute)
}
loginMutex.Unlock()
http.Error(w, "Login fehlgeschlagen", http.StatusUnauthorized) http.Error(w, "Login fehlgeschlagen", http.StatusUnauthorized)
return return
} }
// GET: Login-Formular
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
w.Write([]byte(loginForm)) w.Write([]byte(loginForm))
}) })
http.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session")
if err == nil {
token := cookie.Value
// Token aus dem serverseitigen Store löschen
delete(sessionStore, token)
// Cookie ungültig machen
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: "session", Name: "session",
Value: "", Value: "",
Path: "/", Path: "/",
MaxAge: -1, MaxAge: -1,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
}) })
}
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
}) })
@@ -172,6 +374,11 @@ func main() {
if id != "" { if id != "" {
db.Exec("DELETE FROM eintraege WHERE id = ?", id) db.Exec("DELETE FROM eintraege WHERE id = ?", id)
} }
cacheMutex.Lock()
cache.LastComputed = time.Time{} // auf null zurücksetzen
cacheMutex.Unlock()
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
}) })
@@ -185,6 +392,11 @@ func main() {
// Auto-Increment-Zähler zurücksetzen // Auto-Increment-Zähler zurücksetzen
db.Exec("DELETE FROM sqlite_sequence WHERE name='eintraege'") db.Exec("DELETE FROM sqlite_sequence WHERE name='eintraege'")
cacheMutex.Lock()
cache.LastComputed = time.Time{} // auf null zurücksetzen
cacheMutex.Unlock()
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
}) })
@@ -198,6 +410,11 @@ func main() {
if id != "" { if id != "" {
db.Exec("UPDATE eintraege SET bezahlt = 1 WHERE id = ?", id) db.Exec("UPDATE eintraege SET bezahlt = 1 WHERE id = ?", id)
} }
cacheMutex.Lock()
cache.LastComputed = time.Time{} // auf null zurücksetzen
cacheMutex.Unlock()
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
}) })
@@ -211,6 +428,11 @@ func main() {
if id != "" { if id != "" {
db.Exec("UPDATE eintraege SET bezahlt = 0 WHERE id = ?", id) db.Exec("UPDATE eintraege SET bezahlt = 0 WHERE id = ?", id)
} }
cacheMutex.Lock()
cache.LastComputed = time.Time{} // auf null zurücksetzen
cacheMutex.Unlock()
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
}) })
@@ -226,17 +448,39 @@ func main() {
prozent, _ := strconv.ParseFloat(r.FormValue("prozentwert"), 64) prozent, _ := strconv.ParseFloat(r.FormValue("prozentwert"), 64)
diff := ende - anfang diff := ende - anfang
abgabe := (diff / 100) * prozent abgabe := (diff / 100) * prozent
startort := r.FormValue("startort")
zielort := r.FormValue("zielort")
schiff := r.FormValue("schiff")
ware := r.Form["ware"]
wareStr := strings.Join(ware, ", ")
zeitaufwand, _ := strconv.ParseFloat(r.FormValue("zeitaufwand"), 64)
_, err := db.Exec(`INSERT INTO eintraege (anfangsbestand, endbestand, prozentwert, abgabe, created_at) VALUES (?, ?, ?, ?, datetime('now'))`, anfang, ende, prozent, abgabe) _, err := db.Exec(`INSERT INTO eintraege (anfangsbestand, endbestand, prozentwert, abgabe, created_at, startort, zielort, schiff, ware, zeitaufwand) VALUES (?, ?, ?, ?, datetime('now'), ?, ?, ?, ?, ?)`, anfang, ende, prozent, abgabe, startort, zielort, schiff, wareStr, zeitaufwand)
if err != nil { if err != nil {
http.Error(w, "Fehler beim Einfügen", http.StatusInternalServerError) http.Error(w, "Fehler beim Einfügen", http.StatusInternalServerError)
return return
} }
cacheMutex.Lock()
cache.LastComputed = time.Time{} // auf null zurücksetzen
cacheMutex.Unlock()
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
rows, err := db.Query(`SELECT id, anfangsbestand, endbestand, prozentwert, abgabe, bezahlt, created_at FROM eintraege`) cacheMutex.RLock()
validCache := time.Since(cache.LastComputed) < 6*time.Hour
cachedData := cache.Data
cacheMutex.RUnlock()
if validCache {
cachedData.LoggedIn = isAuthenticated(r)
tmpl.Execute(w, cachedData)
return
}
rows, err := db.Query(`SELECT id, anfangsbestand, endbestand, prozentwert, abgabe, bezahlt, created_at, startort, zielort, schiff, ware, zeitaufwand FROM eintraege`)
if err != nil { if err != nil {
http.Error(w, "Fehler beim Abrufen", http.StatusInternalServerError) http.Error(w, "Fehler beim Abrufen", http.StatusInternalServerError)
return return
@@ -250,7 +494,13 @@ func main() {
var e Entry var e Entry
var bezahlt int var bezahlt int
var createdAt sql.NullString var createdAt sql.NullString
err := rows.Scan(&e.ID, &e.Anfangsbestand, &e.Endbestand, &e.Prozentwert, &e.Abgabe, &bezahlt, &createdAt) var startort sql.NullString
var zielort sql.NullString
var schiff sql.NullString
var ware sql.NullString
var zeitaufwand sql.NullFloat64
err := rows.Scan(&e.ID, &e.Anfangsbestand, &e.Endbestand, &e.Prozentwert, &e.Abgabe, &bezahlt, &createdAt, &startort, &zielort, &schiff, &ware, &zeitaufwand)
if err != nil { if err != nil {
log.Println("Fehler beim Scan:", err) log.Println("Fehler beim Scan:", err)
continue continue
@@ -269,6 +519,36 @@ func main() {
} else { } else {
e.CreatedAt = "unbekannt" e.CreatedAt = "unbekannt"
} }
/**/
if startort.Valid {
e.Startort = startort.String
} else {
e.Startort = "unbekannt"
}
if zielort.Valid {
e.Zielort = zielort.String
} else {
e.Zielort = "unbekannt"
}
if schiff.Valid {
e.Schiff = schiff.String
} else {
e.Schiff = "unbekannt"
}
if ware.Valid {
e.Ware = ware.String
} else {
e.Ware = "unbekannt"
}
if zeitaufwand.Valid {
e.Zeitaufwand = zeitaufwand.Float64
} else {
e.Zeitaufwand = 0
}
eintraege = append(eintraege, e) eintraege = append(eintraege, e)
@@ -285,11 +565,14 @@ func main() {
if _, ok := monatsMap[monatKey]; !ok { if _, ok := monatsMap[monatKey]; !ok {
monatsMap[monatKey] = &Monatsstatistik{Monat: monatKey} monatsMap[monatKey] = &Monatsstatistik{Monat: monatKey}
} }
monatsMap[monatKey].Eintraege = append(monatsMap[monatKey].Eintraege, e)
if e.Bezahlt { if e.Bezahlt {
monatsMap[monatKey].Summe += e.Abgabe monatsMap[monatKey].Summe += e.Abgabe
} else { } else {
monatsMap[monatKey].SummeOffen += e.Abgabe monatsMap[monatKey].SummeOffen += e.Abgabe
} }
monatsMap[monatKey].Prozent = monatsMap[monatKey].Summe / (monatsMap[monatKey].Summe + monatsMap[monatKey].SummeOffen) * 100
monatsMap[monatKey].ProzentOffen = monatsMap[monatKey].SummeOffen / (monatsMap[monatKey].Summe + monatsMap[monatKey].SummeOffen) * 100
} }
var monatsStat []Monatsstatistik var monatsStat []Monatsstatistik
@@ -303,10 +586,10 @@ func main() {
tj, _ := time.Parse("01.2006", monatsStat[j].Monat) tj, _ := time.Parse("01.2006", monatsStat[j].Monat)
return ti.Before(tj) return ti.Before(tj)
}) })
//test
// Dynamische Abteilungen frei anpassbar // Dynamische Abteilungen frei anpassbar
abteilungen := []Abteilung{ abteilungen := []Abteilung{
{Name: "Raumkampf", Anteil: 15, Beispiel: "CF-337 Panther", WertItem: 36308}, {Name: "Raumkampf (+8)", Anteil: 23, Beispiel: "CF-337 Panther", WertItem: 36308},
{Name: "Bodenkampf", Anteil: 8, Beispiel: "P4-AR Rifle", WertItem: 5900}, {Name: "Bodenkampf", Anteil: 8, Beispiel: "P4-AR Rifle", WertItem: 5900},
{Name: "Racing", Anteil: 3, Beispiel: "LumaCore - Power Plant", WertItem: 69300}, {Name: "Racing", Anteil: 3, Beispiel: "LumaCore - Power Plant", WertItem: 69300},
{Name: "Medical", Anteil: 5, Beispiel: "ParaMed Medical Device", WertItem: 1250}, {Name: "Medical", Anteil: 5, Beispiel: "ParaMed Medical Device", WertItem: 1250},
@@ -319,7 +602,7 @@ func main() {
{Name: "Basebuilding (+10)", Anteil: 0, Beispiel: "CF-337 Panther", WertItem: 36308}, {Name: "Basebuilding (+10)", Anteil: 0, Beispiel: "CF-337 Panther", WertItem: 36308},
{Name: "Crafting (+8)", Anteil: 0, Beispiel: "CF-337 Panther", WertItem: 36308}, {Name: "Crafting (+8)", Anteil: 0, Beispiel: "CF-337 Panther", WertItem: 36308},
{Name: "Forschung (+5)", Anteil: 0, Beispiel: "CF-337 Panther", WertItem: 36308}, {Name: "Forschung (+5)", Anteil: 0, Beispiel: "CF-337 Panther", WertItem: 36308},
{Name: "Events (-23)", Anteil: 38, Beispiel: "CF-337 Panther", WertItem: 36308}, {Name: "Events (-15)", Anteil: 30, Beispiel: "CF-337 Panther", WertItem: 36308},
{Name: "Roleplay", Anteil: 3, Beispiel: "Clothing", WertItem: 8400}, {Name: "Roleplay", Anteil: 3, Beispiel: "Clothing", WertItem: 8400},
{Name: "Kunstflug", Anteil: 3, Beispiel: "Beacon Undersuit Crimson", WertItem: 1000}, {Name: "Kunstflug", Anteil: 3, Beispiel: "Beacon Undersuit Crimson", WertItem: 1000},
} }
@@ -329,7 +612,28 @@ func main() {
abteilungen[i].WertOffen = (abteilungen[i].Anteil / 100) * offeneSumme abteilungen[i].WertOffen = (abteilungen[i].Anteil / 100) * offeneSumme
} }
tmpl.Execute(w, struct { computed := TemplateData{
Entries: eintraege,
Summe: summe,
OffeneSumme: offeneSumme,
Abteilungen: abteilungen,
Monatsstatistik: monatsStat,
Member: membername,
HasImpressum: hasimpressum,
Impressum: impressum,
Orte: orte,
Schiffe: schiffe,
Waren: waren,
}
cacheMutex.Lock()
cache.Data = computed
cache.LastComputed = time.Now()
cacheMutex.Unlock()
tmpl.Execute(w, computed)
/*tmpl.Execute(w, struct {
Entries []Entry Entries []Entry
Summe float64 Summe float64
OffeneSumme float64 OffeneSumme float64
@@ -337,6 +641,11 @@ func main() {
Monatsstatistik []Monatsstatistik Monatsstatistik []Monatsstatistik
LoggedIn bool LoggedIn bool
Member string Member string
HasImpressum bool
Impressum string
Orte []string
Schiffe []string
Waren []string
}{ }{
Entries: eintraege, Entries: eintraege,
Summe: summe, Summe: summe,
@@ -345,13 +654,42 @@ func main() {
Monatsstatistik: monatsStat, Monatsstatistik: monatsStat,
LoggedIn: isAuthenticated(r), LoggedIn: isAuthenticated(r),
Member: membername, Member: membername,
}) HasImpressum: hasimpressum,
Impressum: impressum,
Orte: orte,
Schiffe: schiffe,
Waren: waren,
})*/
}) })
log.Println("Server läuft auf http://0.0.0.0:8080") log.Println("Server läuft auf http://0.0.0.0:8080")
http.ListenAndServe(":8080", nil) http.ListenAndServe(":8080", nil)
} }
type TemplateData struct {
Entries []Entry
Summe float64
OffeneSumme float64
Abteilungen []Abteilung
Monatsstatistik []Monatsstatistik
LoggedIn bool
Member string
HasImpressum bool
Impressum string
Orte []string
Schiffe []string
Waren []string
}
type CachedData struct {
Data TemplateData // das Struct, das du an tmpl.Execute übergibst
LastComputed time.Time
}
var cache CachedData
var cacheMutex sync.RWMutex
func createTable(db *sql.DB) { func createTable(db *sql.DB) {
_, err := db.Exec(` _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS eintraege ( CREATE TABLE IF NOT EXISTS eintraege (
@@ -360,22 +698,34 @@ func createTable(db *sql.DB) {
endbestand REAL, endbestand REAL,
prozentwert REAL, prozentwert REAL,
abgabe REAL, abgabe REAL,
bezahlt INTEGER DEFAULT 0 bezahlt INTEGER DEFAULT 0,
created_at TEXT,
startort TEXT,
zielort TEXT,
schiff TEXT,
ware TEXT,
zeitaufwand INTEGER
); );
`) `)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
// Falls die Tabelle schon existiert, aber die Spalte "bezahlt" fehlt (z.B. nach Update) // Ergänze ALTER TABLE nur für Migration bestehender Tabellen
_, err = db.Exec(`ALTER TABLE eintraege ADD COLUMN bezahlt INTEGER DEFAULT 0;`) addColumn := func(column, colType string) {
_, err := db.Exec(`ALTER TABLE eintraege ADD COLUMN ` + column + ` ` + colType)
if err != nil && !strings.Contains(err.Error(), "duplicate column") { if err != nil && !strings.Contains(err.Error(), "duplicate column") {
log.Fatal(err) log.Fatalf("Fehler beim Hinzufügen der Spalte %s: %v", column, err)
} }
_, err = db.Exec(`ALTER TABLE eintraege ADD COLUMN created_at TEXT`)
if err != nil && !strings.Contains(err.Error(), "duplicate column") {
log.Fatal(err)
} }
addColumn("bezahlt", "INTEGER DEFAULT 0")
addColumn("created_at", "TEXT")
addColumn("startort", "TEXT")
addColumn("zielort", "TEXT")
addColumn("schiff", "TEXT")
addColumn("ware", "TEXT")
addColumn("zeitaufwand", "INTEGER")
} }
const loginForm = ` const loginForm = `
@@ -385,6 +735,7 @@ const loginForm = `
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Login</title> <title>Login</title>
<link href="/static/css/bootstrap.min.css" rel="stylesheet"> <link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
</head> </head>
<body class="bg-light"> <body class="bg-light">
<div class="container mt-5"> <div class="container mt-5">
@@ -412,6 +763,8 @@ const htmlTemplate = `
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Abgabe-Berechnung</title> <title>Abgabe-Berechnung</title>
<link href="/static/css/bootstrap.min.css" rel="stylesheet"> <link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/tom-select.default.min.css" rel="stylesheet">
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
</head> </head>
<body class="bg-light"> <body class="bg-light">
<div class="container mt-5"> <div class="container mt-5">
@@ -426,7 +779,7 @@ const htmlTemplate = `
</div> </div>
<h1 class="mb-4">Beitrag zur Community vom Mitglied der Trading-Staffel ({{.Member}})</h1> <h1 class="mb-4">Beitrag zur Community vom Mitglied der Trading-Staffel ({{.Member}})</h1>
<div class="alert alert-light"> <div class="alert alert-info">
<strong>Folgender Wert wurde erwirtschaftet und wird bald zur Verfügung gestellt:</strong> {{formatNumber .OffeneSumme}} UEC <strong>Folgender Wert wurde erwirtschaftet und wird bald zur Verfügung gestellt:</strong> {{formatNumber .OffeneSumme}} UEC
</div> </div>
@@ -450,7 +803,7 @@ const htmlTemplate = `
<div class="col"> <div class="col">
<label class="form-label">Prozentwert</label> <label class="form-label">Prozentwert</label>
<select name="prozentwert" class="form-select"> <select name="prozentwert" class="form-select">
<option value="30">30%</option> <option value="30">30% (Standard)</option>
<option value="10">10%</option> <option value="10">10%</option>
<option value="15">15%</option> <option value="15">15%</option>
<option value="20">20%</option> <option value="20">20%</option>
@@ -463,17 +816,85 @@ const htmlTemplate = `
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-3">
<div class="col">
<label class="form-label">Startort</label>
<select id="startort" name="startort" class="form-select">
{{range .Orte}}
<option value="{{.}}">{{.}}</option>
{{end}}
</select>
</div>
<div class="col">
<label class="form-label">Zielort</label>
<select id="zielort" name="zielort" class="form-select">
{{range .Orte}}
<option value="{{.}}">{{.}}</option>
{{end}}
</select>
</div>
</div>
<div class="row mb-3">
<div class="col">
<label class="form-label">Schiff</label>
<select id="schiff" name="schiff" class="form-select">
{{range .Schiffe}}
<option value="{{.}}">{{.}}</option>
{{end}}
</select>
</div>
<div class="col">
<label class="form-label">Ware</label>
<select id="ware" name="ware" class="form-select" multiple>
{{range .Waren}}
<option value="{{.}}">{{.}}</option>
{{end}}
</select>
</div>
<div class="col">
<label class="form-label">Zeitaufwand (min)</label>
<input type="number" name="zeitaufwand" class="form-control" min="1" required>
</div>
</div>
<button type="submit" class="btn btn-primary">Berechnen & Speichern</button> <button type="submit" class="btn btn-primary">Berechnen & Speichern</button>
</form> </form>
<hr />
{{end}} {{end}}
<h4 class="mt-4">Monatliche Übersicht</h4> <h2 class="mb-3">Auswertungen</h2>
<ul class="nav nav-tabs" id="auswertungTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="monat-tab" data-bs-toggle="tab" data-bs-target="#monat" type="button" role="tab" aria-controls="monat" aria-selected="true">
Monatliche Übersicht
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="abteilung-tab" data-bs-toggle="tab" data-bs-target="#abteilung" type="button" role="tab" aria-controls="abteilung" aria-selected="false">
Verteilung auf Abteilungen
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="items-tab" data-bs-toggle="tab" data-bs-target="#items" type="button" role="tab" aria-controls="items" aria-selected="false">
Gegenwert in Items
</button>
</li>
</ul>
<div class="tab-content border border-top-0 p-4 bg-white" id="auswertungTabsContent">
<!-- Monatliche Übersicht -->
<div class="tab-pane fade show active" id="monat" role="tabpanel" aria-labelledby="monat-tab">
<h5 class="mb-3">Monatliche Übersicht</h5>
<table class="table table-bordered"> <table class="table table-bordered">
<thead> <thead>
<tr> <tr>
<th>Monat</th> <th>Monat</th>
<th>Abgaben verteilt</th> <th>Abgaben verteilt</th>
<th>Abgaben offen</th> <th>Abgaben offen</th>
<th>Statistik</th>
<th>Aktionen</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -482,30 +903,40 @@ const htmlTemplate = `
<td>{{.Monat}}</td> <td>{{.Monat}}</td>
<td>{{formatNumber .Summe}} UEC</td> <td>{{formatNumber .Summe}} UEC</td>
<td>{{formatNumber .SummeOffen}} UEC</td> <td>{{formatNumber .SummeOffen}} UEC</td>
<td>
<div class="progress">
<div class="progress-bar progress-bar-striped bg-success" role="progressbar" style="width: {{.Prozent}}%" aria-valuenow="{{.Prozent}}" aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar progress-bar-striped bg-danger" role="progressbar" style="width: {{.ProzentOffen}}%" aria-valuenow="{{.ProzentOffen}}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</td>
<td>
<button class="btn btn-sm btn-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#monat-{{.Monat}}" aria-expanded="false" aria-controls="monat-{{.Monat}}">
Details
</button>
</td>
</tr> </tr>
{{end}}
</tbody>
</table>
<hr /> <tr>
<td colspan="9" class="p-0 border-0">
<h2 class="mb-3">Gespeicherte Einträge</h2> <div class="collapse" id="monat-{{.Monat}}">
<div class="bg-light p-3 border-top">
<strong>Interne Infos (Details):</strong>
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
<th>#</th> <th>#</th>
<th>Datum</th> <th>Datum</th>
<th>Anfang</th> <th>UEC Anfang</th>
<th>Ende</th> <th>UEC Ende</th>
<th>Profit</th> <th>UEC Profit</th>
<th>Prozent</th> <th>Prozent</th>
<th>Abgabe</th> <th>UEC Abgabe</th>
<th>Status</th> <th>Status</th>
{{if .LoggedIn}}<th>Aktion</th>{{end}} {{if $.LoggedIn}}<th>Aktion</th>{{else}}<th>Erweitert</th>{{end}}
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="eintragsTabelle">
{{range .Entries}} {{range .Eintraege}}
<tr> <tr>
<td>{{.ID}}</td> <td>{{.ID}}</td>
<td>{{formatDate .CreatedAt}}</td> <td>{{formatDate .CreatedAt}}</td>
@@ -513,42 +944,79 @@ const htmlTemplate = `
<td>{{formatNumber .Endbestand}}</td> <td>{{formatNumber .Endbestand}}</td>
<td>{{formatNumber .Gesamtwert}}</td> <td>{{formatNumber .Gesamtwert}}</td>
<td>{{formatNumber .Prozentwert}}%</td> <td>{{formatNumber .Prozentwert}}%</td>
<td> <td>{{formatNumber .Abgabe}}</td>
{{formatNumber .Abgabe}}
</td>
<td> <td>
{{if .Bezahlt}} {{if .Bezahlt}}
{{if $.LoggedIn}} {{if $.LoggedIn}}
<a href="/unmarkaspaid?id={{.ID}}" class="btn btn-sm btn-outline-success">Als unverteilt markieren</a> <a href="/unmarkaspaid?id={{.ID}}" class="btn btn-sm btn-outline-danger">✗ stornieren</a>
{{else}} {{else}}
<span class="badge bg-success">✓ verteilt</span> <span class="badge bg-success">✓ Erledigt</span>
{{end}} {{end}}
{{else}} {{else}}
{{if $.LoggedIn}} {{if $.LoggedIn}}
<a href="/markaspaid?id={{.ID}}" class="btn btn-sm btn-outline-success">Als verteilt markieren</a> <a href="/markaspaid?id={{.ID}}" class="btn btn-sm btn-outline-success">✓ abgeben</a>
{{else}} {{else}}
<span class="badge bg-danger">✗ nicht verteilt</span> <span class="badge bg-danger">✗ Offen</span>
{{end}} {{end}}
{{end}} {{end}}
</td> </td>
{{if $.LoggedIn}}
<td> <td>
<button class="btn btn-sm btn-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#details-{{.ID}}" aria-expanded="false" aria-controls="details-{{.ID}}">
Details
</button>
{{if $.LoggedIn}}
<a href="/delete?id={{.ID}}" class="btn btn-sm btn-danger" onclick="return confirm('Eintrag wirklich löschen?')">Löschen</a> <a href="/delete?id={{.ID}}" class="btn btn-sm btn-danger" onclick="return confirm('Eintrag wirklich löschen?')">Löschen</a>
</td>
{{end}} {{end}}
</td>
</tr>
<tr>
<td colspan="9" class="p-0 border-0">
<div class="collapse" id="details-{{.ID}}">
<div class="bg-light p-3 border-top">
<strong>Interne Infos (Details):</strong>
<table class="table table-sm table-bordered mb-0">
<thead>
<tr>
<th>Startort</th>
<th>Zielort</th>
<th>Schiff</th>
<th>Ware</th>
<th>Zeit (min)</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{.Startort}}</td>
<td>{{.Zielort}}</td>
<td>{{.Schiff}}</td>
<td>{{.Ware}}</td>
<td>{{formatNumber .Zeitaufwand}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</td>
</tr> </tr>
{{end}} {{end}}
</tbody> </tbody>
</table> </table>
<hr />
<div class="alert alert-info">
<strong>Die tatsächlichen Werte können abweichen.</strong> Die dargestellten Werte sind meine Vorstellung einer sinnvollen Verteilung.<br>
Die Summe wird an die Orga-Leitung entrichtet. Die entgültige Entscheidung über die Verteilung obliegt der Orga-Leitung.
</div> </div>
<h4 class="mt-4">Verteilung auf Abteilungen:</h4> <!-- Verteilung auf Abteilungen -->
<div class="tab-pane fade" id="abteilung" role="tabpanel" aria-labelledby="abteilung-tab">
<div class="alert alert-info">
<strong>Die tatsächlichen Werte können abweichen.</strong> Die dargestellten Werte sind meine Vorstellung einer sinnvollen Verteilung.<br>
Die Summe wird an die Orga-Leitung entrichtet. Die endgültige Entscheidung über die Verteilung obliegt der Orga-Leitung.
</div>
<h5 class="mb-3">Verteilung auf Abteilungen:</h5>
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
@@ -569,8 +1037,11 @@ const htmlTemplate = `
{{end}} {{end}}
</tbody> </tbody>
</table> </table>
</div>
<h4 class="mt-4">Gegenwert in Items:</h4> <!-- Gegenwert in Items -->
<div class="tab-pane fade" id="items" role="tabpanel" aria-labelledby="items-tab">
<h5 class="mb-3">Gegenwert in Items:</h5>
<table class="table table-bordered"> <table class="table table-bordered">
<thead> <thead>
<tr> <tr>
@@ -599,13 +1070,88 @@ const htmlTemplate = `
{{end}} {{end}}
</tbody> </tbody>
</table> </table>
</div>
</div>
{{if .LoggedIn}} {{if .LoggedIn}}
<form action="/reset" method="POST" onsubmit="return confirm('Alle Einträge wirklich löschen?')"> <form action="/reset" method="POST" onsubmit="return confirm('Alle Einträge wirklich löschen?')">
<button type="submit" class="btn btn-outline-danger mt-3">Alle Einträge löschen</button> <button type="submit" class="btn btn-outline-danger mt-3">Alle Einträge löschen</button>
</form> </form>
{{end}} {{end}}
<hr />
{{if .HasImpressum}}
<div class="alert alert-light">
<strong><a href="{{.Impressum}}">Impressum</a></strong>
</div> </div>
{{end}}
<hr />
</div>
<style>
.max {
max-width: 100%;
}
</style>
<script src="/static/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/tom-select.complete.min.js"></script>
<script>
new TomSelect("#zielort", {
create: true, // erlaubt Freitext
sortField: "text"
});
</script>
<script>
new TomSelect("#startort", {
create: true, // erlaubt Freitext
sortField: "text"
});
</script>
<script>
new TomSelect("#schiff", {
create: true, // erlaubt Freitext
sortField: "text"
});
</script>
<script>
new TomSelect("#ware", {
create: true, // erlaubt Freitext
sortField: "text",
plugins: ['remove_button'] // ← erlaubt Entfernen per „x“-Button
});
</script>
<script>
document.addEventListener("DOMContentLoaded", function () {
const tabKey = "lastActiveTab";
// Tabs initialisieren - sicherstellen, dass Bootstrap geladen ist
const triggerElList = [].slice.call(document.querySelectorAll('#auswertungTabs button[data-bs-toggle="tab"]'));
const tabList = triggerElList.map(function (triggerEl) {
return new bootstrap.Tab(triggerEl);
});
// Falls gespeicherter Tab vorhanden ist, anzeigen
const lastTabId = localStorage.getItem(tabKey);
if (lastTabId) {
const selector = '#auswertungTabs button[data-bs-target="' + lastTabId + '"]';
const lastTabTrigger = document.querySelector(selector);
if (lastTabTrigger) {
new bootstrap.Tab(lastTabTrigger).show();
}
}
// Tab-Wechsel speichern
triggerElList.forEach(function (triggerEl) {
triggerEl.addEventListener("shown.bs.tab", function (event) {
const target = event.target.getAttribute("data-bs-target");
localStorage.setItem(tabKey, target);
});
});
});
</script>
</body> </body>
</html> </html>
` `

2
static/css/tom-select.default.min.css vendored Normal file

File diff suppressed because one or more lines are too long

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/img/footerlower.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
static/img/footerupper.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

7
static/js/bootstrap.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

5021
static/js/tom-select.complete.min.js vendored Normal file

File diff suppressed because it is too large Load Diff