Compare commits
14 Commits
24cfcd61ed
...
1.0.0
Author | SHA1 | Date | |
---|---|---|---|
134e601d57 | |||
842c541c13 | |||
41cee8af1d | |||
190cded4e5 | |||
4ecdd40613 | |||
7c73ac7749 | |||
afc6bcae83 | |||
ecf5cf9773 | |||
33b2a2d966 | |||
e12f631c4a | |||
3c71785849 | |||
9308b914ff | |||
4fccd54805 | |||
83d8644ef9 |
BIN
dynamicsrc/footerlower.webp
Normal file
BIN
dynamicsrc/footerlower.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
dynamicsrc/footerupper.webp
Normal file
BIN
dynamicsrc/footerupper.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 128 KiB |
3
go.mod
3
go.mod
@@ -10,8 +10,9 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // 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/sys v0.33.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
modernc.org/libc v1.65.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
|
30
go.sum
30
go.sum
@@ -1,5 +1,7 @@
|
||||
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/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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
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/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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
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/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
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/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/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=
|
||||
|
453
main.go
453
main.go
@@ -1,7 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
@@ -12,8 +14,10 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
_ "modernc.org/sqlite" // statt github.com/mattn/go-sqlite3
|
||||
)
|
||||
|
||||
@@ -36,34 +40,35 @@ func Enabled(k string, def bool) bool {
|
||||
}
|
||||
|
||||
var (
|
||||
username = GetENV("KT_USERNAME", "root")
|
||||
password = GetENV("KT_PASSWORD", "root")
|
||||
membername = GetENV("KT_MEMBER", "demo")
|
||||
productive = Enabled("KT_PRODUCTIVE", false)
|
||||
hasimpressum = Enabled("KT_HASIMPRESSUM", false)
|
||||
impressum = GetENV("KT_IMPRESSUM", "")
|
||||
orte = []string{}
|
||||
schiffe = []string{
|
||||
username = GetENV("KT_USERNAME", "root")
|
||||
password = GetENV("KT_PASSWORD", "root")
|
||||
membername = GetENV("KT_MEMBER", "demo")
|
||||
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", "C8 Pisces", "C8R Pisces Rescue",
|
||||
"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-K", "Idris-M", "Javelin",
|
||||
"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 Bomber", "Sabre Peregrine", "Starfarer Gemini", "Talon", "Talon Shrike",
|
||||
"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", "Nitrogen", "Astatine",
|
||||
"Iodine", "Aluminium", "Copper", "Lithium", "Silicon", "Tungsten", "Aphorite", "Beryl", "Bexalite",
|
||||
"Corundum", "Diamond", "Dolivine", "Hadanite", "Hephaestanite", "Laranite", "Quartz", "Taranite",
|
||||
"", "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",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -140,9 +145,12 @@ type Abteilung struct {
|
||||
}
|
||||
|
||||
type Monatsstatistik struct {
|
||||
Monat string // z. B. "07.2025"
|
||||
Summe float64 // bezahlte
|
||||
SummeOffen float64 // noch nicht bezahlt
|
||||
Monat string // z. B. "07.2025"
|
||||
Summe float64 // bezahlte
|
||||
SummeOffen float64 // noch nicht bezahlt
|
||||
Prozent float64
|
||||
ProzentOffen float64
|
||||
Eintraege []Entry
|
||||
}
|
||||
|
||||
var tmpl = template.Must(template.New("form").Funcs(template.FuncMap{
|
||||
@@ -186,7 +194,37 @@ func reverse(s string) string {
|
||||
|
||||
func isAuthenticated(r *http.Request) bool {
|
||||
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() {
|
||||
@@ -216,6 +254,8 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
hashedPassword = hashPassword(password)
|
||||
|
||||
var pois []POI
|
||||
if err := json.Unmarshal(data, &pois); err != nil {
|
||||
panic(err)
|
||||
@@ -242,34 +282,86 @@ func main() {
|
||||
}
|
||||
|
||||
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 {
|
||||
r.ParseForm()
|
||||
user := r.FormValue("username")
|
||||
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{
|
||||
Name: "session",
|
||||
Value: "authenticated",
|
||||
Path: "/",
|
||||
Name: "session",
|
||||
Value: token,
|
||||
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)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
// GET: Login-Formular
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(loginForm))
|
||||
})
|
||||
|
||||
http.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "session",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
})
|
||||
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{
|
||||
Name: "session",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
})
|
||||
|
||||
@@ -282,6 +374,11 @@ func main() {
|
||||
if 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)
|
||||
})
|
||||
|
||||
@@ -295,6 +392,11 @@ func main() {
|
||||
|
||||
// Auto-Increment-Zähler zurücksetzen
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -308,6 +410,11 @@ func main() {
|
||||
if 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)
|
||||
})
|
||||
|
||||
@@ -321,6 +428,11 @@ func main() {
|
||||
if 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)
|
||||
})
|
||||
|
||||
@@ -348,10 +460,26 @@ func main() {
|
||||
http.Error(w, "Fehler beim Einfügen", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
cacheMutex.Lock()
|
||||
cache.LastComputed = time.Time{} // auf null zurücksetzen
|
||||
cacheMutex.Unlock()
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
http.Error(w, "Fehler beim Abrufen", http.StatusInternalServerError)
|
||||
@@ -437,11 +565,14 @@ func main() {
|
||||
if _, ok := monatsMap[monatKey]; !ok {
|
||||
monatsMap[monatKey] = &Monatsstatistik{Monat: monatKey}
|
||||
}
|
||||
monatsMap[monatKey].Eintraege = append(monatsMap[monatKey].Eintraege, e)
|
||||
if e.Bezahlt {
|
||||
monatsMap[monatKey].Summe += e.Abgabe
|
||||
} else {
|
||||
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
|
||||
@@ -458,7 +589,7 @@ func main() {
|
||||
//test
|
||||
// Dynamische Abteilungen – frei anpassbar
|
||||
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: "Racing", Anteil: 3, Beispiel: "LumaCore - Power Plant", WertItem: 69300},
|
||||
{Name: "Medical", Anteil: 5, Beispiel: "ParaMed Medical Device", WertItem: 1250},
|
||||
@@ -471,7 +602,7 @@ func main() {
|
||||
{Name: "Basebuilding (+10)", 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: "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: "Kunstflug", Anteil: 3, Beispiel: "Beacon Undersuit Crimson", WertItem: 1000},
|
||||
}
|
||||
@@ -481,7 +612,28 @@ func main() {
|
||||
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
|
||||
Summe float64
|
||||
OffeneSumme float64
|
||||
@@ -507,7 +659,7 @@ func main() {
|
||||
Orte: orte,
|
||||
Schiffe: schiffe,
|
||||
Waren: waren,
|
||||
})
|
||||
})*/
|
||||
|
||||
})
|
||||
|
||||
@@ -515,6 +667,29 @@ func main() {
|
||||
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) {
|
||||
_, err := db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS eintraege (
|
||||
@@ -593,15 +768,15 @@ const htmlTemplate = `
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container mt-5">
|
||||
<div class="d-flex justify-content-end">
|
||||
{{if .LoggedIn}}
|
||||
<form action="/logout" method="POST">
|
||||
<button type="submit" class="btn btn-sm btn-secondary">Logout</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<a href="/login" class="btn btn-sm btn-outline-primary">Login</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
{{if .LoggedIn}}
|
||||
<form action="/logout" method="POST">
|
||||
<button type="submit" class="btn btn-sm btn-secondary">Logout</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<a href="/login" class="btn btn-sm btn-outline-primary">Login</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<h1 class="mb-4">Beitrag zur Community vom Mitglied der Trading-Staffel ({{.Member}})</h1>
|
||||
|
||||
<div class="alert alert-info">
|
||||
@@ -628,7 +803,7 @@ const htmlTemplate = `
|
||||
<div class="col">
|
||||
<label class="form-label">Prozentwert</label>
|
||||
<select name="prozentwert" class="form-select">
|
||||
<option value="30">30%</option>
|
||||
<option value="30">30% (Standard)</option>
|
||||
<option value="10">10%</option>
|
||||
<option value="15">15%</option>
|
||||
<option value="20">20%</option>
|
||||
@@ -684,92 +859,8 @@ const htmlTemplate = `
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Berechnen & Speichern</button>
|
||||
</form>
|
||||
{{end}}
|
||||
|
||||
|
||||
<h2 class="mb-3">Gespeicherte Einträge</h2>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Datum</th>
|
||||
<th>UEC Anfang</th>
|
||||
<th>UEC Ende</th>
|
||||
<th>UEC Profit</th>
|
||||
<th>Prozent</th>
|
||||
<th>UEC Abgabe</th>
|
||||
<th>Status</th>
|
||||
{{if .LoggedIn}}<th>Aktion</th>{{else}}<th>Erweitert</th>{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="eintragsTabelle">
|
||||
{{range .Entries}}
|
||||
<tr>
|
||||
<td>{{.ID}}</td>
|
||||
<td>{{formatDate .CreatedAt}}</td>
|
||||
<td>{{formatNumber .Anfangsbestand}}</td>
|
||||
<td>{{formatNumber .Endbestand}}</td>
|
||||
<td>{{formatNumber .Gesamtwert}}</td>
|
||||
<td>{{formatNumber .Prozentwert}}%</td>
|
||||
<td>{{formatNumber .Abgabe}}</td>
|
||||
<td>
|
||||
{{if .Bezahlt}}
|
||||
{{if $.LoggedIn}}
|
||||
<a href="/unmarkaspaid?id={{.ID}}" class="btn btn-sm btn-outline-success">Als unverteilt markieren</a>
|
||||
{{else}}
|
||||
<span class="badge bg-success">✓ verteilt</span>
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{if $.LoggedIn}}
|
||||
<a href="/markaspaid?id={{.ID}}" class="btn btn-sm btn-outline-success">Als verteilt markieren</a>
|
||||
{{else}}
|
||||
<span class="badge bg-danger">✗ nicht verteilt</span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</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>
|
||||
{{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>
|
||||
|
||||
<hr />
|
||||
{{end}}
|
||||
|
||||
<h2 class="mb-3">Auswertungen</h2>
|
||||
|
||||
@@ -802,6 +893,8 @@ const htmlTemplate = `
|
||||
<th>Monat</th>
|
||||
<th>Abgaben verteilt</th>
|
||||
<th>Abgaben offen</th>
|
||||
<th>Statistik</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -810,6 +903,107 @@ const htmlTemplate = `
|
||||
<td>{{.Monat}}</td>
|
||||
<td>{{formatNumber .Summe}} 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>
|
||||
<td colspan="9" class="p-0 border-0">
|
||||
<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">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Datum</th>
|
||||
<th>UEC Anfang</th>
|
||||
<th>UEC Ende</th>
|
||||
<th>UEC Profit</th>
|
||||
<th>Prozent</th>
|
||||
<th>UEC Abgabe</th>
|
||||
<th>Status</th>
|
||||
{{if $.LoggedIn}}<th>Aktion</th>{{else}}<th>Erweitert</th>{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="eintragsTabelle">
|
||||
{{range .Eintraege}}
|
||||
<tr>
|
||||
<td>{{.ID}}</td>
|
||||
<td>{{formatDate .CreatedAt}}</td>
|
||||
<td>{{formatNumber .Anfangsbestand}}</td>
|
||||
<td>{{formatNumber .Endbestand}}</td>
|
||||
<td>{{formatNumber .Gesamtwert}}</td>
|
||||
<td>{{formatNumber .Prozentwert}}%</td>
|
||||
<td>{{formatNumber .Abgabe}}</td>
|
||||
<td>
|
||||
{{if .Bezahlt}}
|
||||
{{if $.LoggedIn}}
|
||||
<a href="/unmarkaspaid?id={{.ID}}" class="btn btn-sm btn-outline-danger">✗ stornieren</a>
|
||||
{{else}}
|
||||
<span class="badge bg-success">✓ Erledigt</span>
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{if $.LoggedIn}}
|
||||
<a href="/markaspaid?id={{.ID}}" class="btn btn-sm btn-outline-success">✓ abgeben</a>
|
||||
{{else}}
|
||||
<span class="badge bg-danger">✗ Offen</span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</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>
|
||||
{{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>
|
||||
{{end}}
|
||||
</tbody>
|
||||
@@ -892,6 +1086,11 @@ const htmlTemplate = `
|
||||
{{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>
|
||||
|
||||
|
BIN
static/img/footerlower.webp
Normal file
BIN
static/img/footerlower.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
static/img/footerupper.webp
Normal file
BIN
static/img/footerupper.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 128 KiB |
Reference in New Issue
Block a user