Files
trading/main.go
2025-07-23 17:49:11 +02:00

525 lines
14 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
var createdAt sql.NullString
err := rows.Scan(&e.ID, &e.Anfangsbestand, &e.Endbestand, &e.Prozentwert, &e.Abgabe, &bezahlt, &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
}
if createdAt.Valid {
e.CreatedAt = createdAt.String
} else {
e.CreatedAt = "unbekannt"
}
}
// 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 = `
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Login</title>
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container mt-5">
<h2>Login</h2>
<form method="POST" class="card p-4 shadow-sm">
<div class="mb-3">
<label class="form-label">Benutzername</label>
<input type="text" name="username" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Passwort</label>
<input type="password" name="password" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
</div>
</body>
</html>
`
const htmlTemplate = `
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Abgabe-Berechnung</title>
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
</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>
<h1 class="mb-4">Beitrag zur Community vom Mitglied der Trading-Staffel ({{.Member}})</h1>
<div class="alert alert-light">
<strong>Folgender Wert wurde erwirtschaftet und wird bald zur Verfügung gestellt:</strong> {{formatNumber .OffeneSumme}} UEC
</div>
<div class="alert alert-success">
<strong>Summe aller getätigten Abgaben an die Community:</strong> {{formatNumber .Summe}} UEC
</div>
<hr />
{{if .LoggedIn}}
<form method="POST" class="card p-4 mb-4 shadow-sm">
<div class="row mb-3">
<div class="col">
<label class="form-label">Anfangsbestand</label>
<input type="number" step="1" name="anfangsbestand" class="form-control" required>
</div>
<div class="col">
<label class="form-label">Endbestand</label>
<input type="number" step="1" name="endbestand" class="form-control" required>
</div>
<div class="col">
<label class="form-label">Prozentwert</label>
<select name="prozentwert" class="form-select">
<option value="30">30%</option>
<option value="10">10%</option>
<option value="15">15%</option>
<option value="20">20%</option>
<option value="25">25%</option>
<option value="30">30%</option>
<option value="40">40%</option>
<option value="50">50%</option>
<option value="75">75%</option>
<option value="100">100%</option>
</select>
</div>
</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>Anfang</th>
<th>Ende</th>
<th>Profit</th>
<th>Prozent</th>
<th>Abgabe</th>
<th>Status</th>
{{if .LoggedIn}}<th>Aktion</th>{{end}}
</tr>
</thead>
<tbody>
{{range .Entries}}
<tr>
<td>{{.ID}}</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>
{{if $.LoggedIn}}
<td>
<a href="/delete?id={{.ID}}" class="btn btn-sm btn-danger" onclick="return confirm('Eintrag wirklich löschen?')">Löschen</a>
</td>
{{end}}
</tr>
{{end}}
</tbody>
</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>
<h4 class="mt-4">Verteilung auf Abteilungen:</h4>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Abteilung</th>
<th>Verteilungsschlüssel</th>
<th>Summe verteilt</th>
<th>Summe offen</th>
</tr>
</thead>
<tbody>
{{range .Abteilungen}}
<tr>
<td>{{.Name}}</td>
<td>{{formatNumber .Anteil}}%</td>
<td>{{formatNumber .Wert}} UEC</td>
<td>{{formatNumber .WertOffen}} UEC</td>
</tr>
{{end}}
</tbody>
</table>
<h4 class="mt-4">Gegenwert in Items:</h4>
<table class="table table-bordered">
<thead>
<tr>
<th>Abteilung</th>
<th>Beispiel</th>
<th>Wert pro Item</th>
<th>Summe verteilt</th>
<th>Menge</th>
</tr>
</thead>
<tbody>
{{range .Abteilungen}}
<tr>
<td>{{.Name}}</td>
<td>{{.Beispiel}}</td>
<td>{{formatNumber .WertItem}} UEC</td>
<td>{{formatNumber .Wert}} UEC</td>
<td>
{{if gt .WertItem 0.0}}
{{formatNumber (div .Wert .WertItem)}}
{{else}}
-
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
{{if .LoggedIn}}
<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>
</form>
{{end}}
</div>
</body>
</html>
`