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
`
const htmlTemplate = `
Abgabe-Berechnung
{{if .LoggedIn}}
{{else}}
Login
{{end}}
Beitrag zur Community von der Trading-Staffel ({{.Member}})
{{if .LoggedIn}}
{{end}}
Gespeicherte Einträge
#
Anfang
Ende
Profit
Prozent
Abgabe
Status
{{if .LoggedIn}}Aktion {{end}}
{{range .Entries}}
{{.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}}
{{if $.LoggedIn}}
Löschen
{{end}}
{{end}}
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:
Abteilung
Verteilungsschlüssel
Summe verteilt
Summe offen
{{range .Abteilungen}}
{{.Name}}
{{formatNumber .Anteil}}%
{{formatNumber .Wert}} UEC
{{formatNumber .WertOffen}} UEC
{{end}}
{{if .LoggedIn}}
{{end}}
`