package main import ( "database/sql" "html/template" "log" "net/http" "os" "strconv" "strings" _ "modernc.org/sqlite" // statt github.com/mattn/go-sqlite3 ) func GetENV(k, d string) string { if v := os.Getenv(k); v != "" { return v } return d } func Enabled(k string, def bool) bool { b, err := strconv.ParseBool(strings.ToLower(os.Getenv(k))) if err != nil { return def } return b } var ( username = GetENV("KT_USERNAME", "root") password = GetENV("KT_PASSWORD", "root") membername = GetENV("KT_MEMBER", "guest") ) type Entry struct { ID int Anfangsbestand float64 Endbestand float64 Prozentwert float64 Abgabe float64 Gesamtwert float64 Bezahlt bool CreatedAt string } type Abteilung struct { Name string Anteil float64 // in Prozent Wert float64 // berechnet: Anteil * Summe / 100 WertOffen float64 // berechnet: Anteil * Summe / 100 (von offen) Beispiel string WertItem float64 } var tmpl = template.Must(template.New("form").Funcs(template.FuncMap{ "formatNumber": formatNumber, "div": func(a, b float64) float64 { if b == 0 { return 0 } return a / b }, }).Parse(htmlTemplate)) // Tausendertrenner für deutsche Zahlendarstellung (z. B. 12345 → "12.345") func formatNumber(n float64) string { intVal := int64(n + 0.5) // runden s := strconv.FormatInt(intVal, 10) nStr := "" for i, r := range reverse(s) { if i > 0 && i%3 == 0 { nStr = "." + nStr } nStr = string(r) + nStr } return nStr } func reverse(s string) string { r := []rune(s) for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 { r[i], r[j] = r[j], r[i] } return string(r) } func isAuthenticated(r *http.Request) bool { cookie, err := r.Cookie("session") return err == nil && cookie.Value == "authenticated" } func main() { db, err := sql.Open("sqlite", "/data/data.db") if err != nil { log.Fatal(err) } defer db.Close() createTable(db) http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("/data/static")))) http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { r.ParseForm() user := r.FormValue("username") pass := r.FormValue("password") if user == username && pass == password { http.SetCookie(w, &http.Cookie{ Name: "session", Value: "authenticated", Path: "/", }) http.Redirect(w, r, "/", http.StatusSeeOther) return } http.Error(w, "Login fehlgeschlagen", http.StatusUnauthorized) return } 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, }) http.Redirect(w, r, "/", http.StatusSeeOther) }) http.HandleFunc("/delete", func(w http.ResponseWriter, r *http.Request) { if !isAuthenticated(r) { http.Error(w, "Nicht autorisiert", http.StatusUnauthorized) return } id := r.URL.Query().Get("id") if id != "" { db.Exec("DELETE FROM eintraege WHERE id = ?", id) } http.Redirect(w, r, "/", http.StatusSeeOther) }) http.HandleFunc("/reset", func(w http.ResponseWriter, r *http.Request) { if !isAuthenticated(r) { http.Error(w, "Nicht autorisiert", http.StatusUnauthorized) return } // Tabelle leeren db.Exec("DELETE FROM eintraege") // Auto-Increment-Zähler zurücksetzen db.Exec("DELETE FROM sqlite_sequence WHERE name='eintraege'") http.Redirect(w, r, "/", http.StatusSeeOther) }) http.HandleFunc("/markaspaid", func(w http.ResponseWriter, r *http.Request) { if !isAuthenticated(r) { http.Error(w, "Nicht autorisiert", http.StatusUnauthorized) return } id := r.URL.Query().Get("id") if id != "" { db.Exec("UPDATE eintraege SET bezahlt = 1 WHERE id = ?", id) } http.Redirect(w, r, "/", http.StatusSeeOther) }) http.HandleFunc("/unmarkaspaid", func(w http.ResponseWriter, r *http.Request) { if !isAuthenticated(r) { http.Error(w, "Nicht autorisiert", http.StatusUnauthorized) return } id := r.URL.Query().Get("id") if id != "" { db.Exec("UPDATE eintraege SET bezahlt = 0 WHERE id = ?", id) } http.Redirect(w, r, "/", http.StatusSeeOther) }) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { if !isAuthenticated(r) { http.Error(w, "Nicht autorisiert", http.StatusUnauthorized) return } r.ParseForm() anfang, _ := strconv.ParseFloat(r.FormValue("anfangsbestand"), 64) ende, _ := strconv.ParseFloat(r.FormValue("endbestand"), 64) prozent, _ := strconv.ParseFloat(r.FormValue("prozentwert"), 64) diff := ende - anfang abgabe := (diff / 100) * prozent _, err := db.Exec(`INSERT INTO eintraege (anfangsbestand, endbestand, prozentwert, abgabe, created_at) VALUES (?, ?, ?, ?, datetime('now'))`, anfang, ende, prozent, abgabe) if err != nil { http.Error(w, "Fehler beim Einfügen", http.StatusInternalServerError) return } http.Redirect(w, r, "/", http.StatusSeeOther) return } rows, err := db.Query(`SELECT id, anfangsbestand, endbestand, prozentwert, abgabe, bezahlt, created_at FROM eintraege`) if err != nil { http.Error(w, "Fehler beim Abrufen", http.StatusInternalServerError) return } defer rows.Close() var eintraege []Entry var summe float64 var offeneSumme float64 for rows.Next() { var e Entry var bezahlt int err := rows.Scan(&e.ID, &e.Anfangsbestand, &e.Endbestand, &e.Prozentwert, &e.Abgabe, &bezahlt, &e.CreatedAt) if err != nil { log.Println("Fehler beim Scan:", err) continue } e.Gesamtwert = e.Endbestand - e.Anfangsbestand e.Bezahlt = bezahlt == 1 eintraege = append(eintraege, e) if !e.Bezahlt { offeneSumme += e.Abgabe } else { summe += e.Abgabe } } // Dynamische Abteilungen – frei anpassbar abteilungen := []Abteilung{ {Name: "Raumkampf", Anteil: 15, Beispiel: "CF-337 Panther", WertItem: 36308}, {Name: "Bodenkampf", Anteil: 8, Beispiel: "P4-AR Rifle (10x)", WertItem: 59000}, {Name: "Racing", Anteil: 3, Beispiel: "LumaCore - Power Plant", WertItem: 69300}, {Name: "Medical", Anteil: 5, Beispiel: "Drake Cutlass Red", WertItem: 2857680}, {Name: "Exploration", Anteil: 3, Beispiel: "Anvil Terrapin", WertItem: 5433120}, {Name: "Rettung", Anteil: 5, Beispiel: "GSX-HP Fuel-Pod", WertItem: 115200}, {Name: "Logistik", Anteil: 8, Beispiel: "MaxLift Tractor Beam", WertItem: 19175}, {Name: "Mining", Anteil: 3, Beispiel: "Helix II", WertItem: 108000}, {Name: "Salvaging", Anteil: 3, Beispiel: "Abrade Scraper Module", WertItem: 21250}, {Name: "Trading", Anteil: 3, Beispiel: "MaxLift Tractor Beam", WertItem: 19175}, {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: "Roleplay", Anteil: 3, Beispiel: "Clothing (10x)", WertItem: 84000}, {Name: "Kunstflug", Anteil: 3, Beispiel: "Esperia Talon", WertItem: 3260250}, } for i := range abteilungen { abteilungen[i].Wert = (abteilungen[i].Anteil / 100) * summe abteilungen[i].WertOffen = (abteilungen[i].Anteil / 100) * offeneSumme } tmpl.Execute(w, struct { Entries []Entry Summe float64 OffeneSumme float64 Abteilungen []Abteilung LoggedIn bool Member string }{ Entries: eintraege, Summe: summe, OffeneSumme: offeneSumme, Abteilungen: abteilungen, LoggedIn: isAuthenticated(r), Member: membername, }) }) log.Println("Server läuft auf http://0.0.0.0:8080") http.ListenAndServe(":8080", nil) } func createTable(db *sql.DB) { _, err := db.Exec(` CREATE TABLE IF NOT EXISTS eintraege ( id INTEGER PRIMARY KEY AUTOINCREMENT, anfangsbestand REAL, endbestand REAL, prozentwert REAL, abgabe REAL, bezahlt INTEGER DEFAULT 0 ); `) if err != nil { log.Fatal(err) } // Falls die Tabelle schon existiert, aber die Spalte "bezahlt" fehlt (z. B. nach Update) _, err = db.Exec(`ALTER TABLE eintraege ADD COLUMN bezahlt INTEGER DEFAULT 0;`) if err != nil && !strings.Contains(err.Error(), "duplicate column") { log.Fatal(err) } _, err = db.Exec(`ALTER TABLE eintraege ADD COLUMN created_at TEXT`) if err != nil && !strings.Contains(err.Error(), "duplicate column") { log.Fatal(err) } } const loginForm = ` Login

Login

` const htmlTemplate = ` Abgabe-Berechnung
{{if .LoggedIn}}
{{else}} Login {{end}}

Beitrag zur Community vom Mitglied der Trading-Staffel ({{.Member}})

Folgender Wert wurde erwirtschaftet und wird bald zur Verfügung gestellt: {{formatNumber .OffeneSumme}} UEC
Summe aller getätigten Abgaben an die Community: {{formatNumber .Summe}} UEC

{{if .LoggedIn}}
{{end}}

Gespeicherte Einträge

{{if .LoggedIn}}{{end}} {{range .Entries}} {{if $.LoggedIn}} {{end}} {{end}}
# Anfang Ende Profit Prozent Abgabe StatusAktion
{{.ID}} {{formatNumber .Anfangsbestand}} {{formatNumber .Endbestand}} {{formatNumber .Gesamtwert}} {{formatNumber .Prozentwert}}% {{formatNumber .Abgabe}} {{if .Bezahlt}} {{if $.LoggedIn}} Als unverteilt markieren {{else}} ✓ verteilt {{end}} {{else}} {{if $.LoggedIn}} Als verteilt markieren {{else}} ✗ nicht verteilt {{end}} {{end}} Löschen

Die tatsächlichen Werte können abweichen. Die dargestellten Werte sind meine Vorstellung einer sinnvollen Verteilung.
Die Summe wird an die Orga-Leitung entrichtet. Die entgültige Entscheidung über die Verteilung obliegt der Orga-Leitung.

Verteilung auf Abteilungen:

{{range .Abteilungen}} {{end}}
Abteilung Verteilungsschlüssel Summe verteilt Summe offen
{{.Name}} {{formatNumber .Anteil}}% {{formatNumber .Wert}} UEC {{formatNumber .WertOffen}} UEC

Gegenwert in Items:

{{range .Abteilungen}} {{end}}
Abteilung Beispiel Wert pro Item Summe verteilt Menge
{{.Name}} {{.Beispiel}} {{formatNumber .WertItem}} UEC {{formatNumber .Wert}} UEC {{if gt .WertItem 0.0}} {{formatNumber (div .Wert .WertItem)}} {{else}} - {{end}}
{{if .LoggedIn}}
{{end}}
`