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 } type Abteilung struct { Name string Anteil float64 // in Prozent Wert float64 // berechnet: Anteil * Summe / 100 WertOffen float64 // berechnet: Anteil * Summe / 100 (von offen) } var tmpl = template.Must(template.New("form").Funcs(template.FuncMap{ "formatNumber": formatNumber, }).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("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("/", 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) VALUES (?, ?, ?, ?)`, 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 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) 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}, {Name: "Bodenkampf", Anteil: 8}, {Name: "Racing", Anteil: 3}, {Name: "Medical", Anteil: 5}, {Name: "Exploration", Anteil: 3}, {Name: "Rettung", Anteil: 5}, {Name: "Logistik", Anteil: 8}, {Name: "Mining", Anteil: 3}, {Name: "Salvaging", Anteil: 3}, {Name: "Trading", Anteil: 3}, {Name: "Basebuilding", Anteil: 10}, {Name: "Crafting", Anteil: 8}, {Name: "Forschung", Anteil: 5}, {Name: "Events", Anteil: 15}, {Name: "Roleplay", Anteil: 3}, {Name: "Kunstflug", Anteil: 3}, } 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://localhost: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) } } const loginForm = ` Login

Login

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

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

{{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}} ✓ verteilt {{else}} {{if $.LoggedIn}} Als verteilt markieren {{else}} ✗ nicht verteilt {{end}} {{end}} Löschen
Offen zur Abgabe: {{formatNumber .OffeneSumme}} UEC
Summe aller Abgaben an die Knebel-Community: {{formatNumber .Summe}} UEC

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
{{if .LoggedIn}}
{{end}}
`