Compare commits

...

18 Commits

Author SHA1 Message Date
1ba4f865de .gitea/workflows/release.yml hinzugefügt
All checks were successful
release-tag / release-image (push) Successful in 2m13s
build-binaries / build (, amd64, linux) (push) Successful in 11m24s
build-binaries / build (, arm, 7, linux) (push) Successful in 10m42s
build-binaries / build (, arm64, linux) (push) Successful in 10m41s
build-binaries / build (.exe, amd64, windows) (push) Successful in 10m46s
build-binaries / release (push) Successful in 38s
2025-08-06 17:36:56 +00:00
acd9fda529 Bugfix: Discord-Webhook Anzeige von UEC-Profit ist immer 0
All checks were successful
release-tag / release-image (push) Successful in 2m6s
2025-08-02 19:11:46 +02:00
f63e04f7b0 Discord-Push hinzugefügt
All checks were successful
release-tag / release-image (push) Successful in 2m11s
2025-08-02 19:02:25 +02:00
ff2f37f718 Bug behoben: Wenn Einträge bearbeitet werden, wird man visuell abgemeldet. Das stimmt jedoch nicht, und mit eine F5 ist man wieder angemeldet.
All checks were successful
release-tag / release-image (push) Successful in 2m30s
2025-08-01 21:00:10 +02:00
134e601d57 Debug-Schritte entfernt
All checks were successful
release-tag / release-image (push) Successful in 2m13s
2025-07-31 23:13:52 +02:00
842c541c13 Hotfix - Login-Bug
Some checks failed
release-tag / release-image (push) Has been cancelled
2025-07-31 23:12:11 +02:00
41cee8af1d Berechnungs-Cache eingebaut, um Anzeige zu beschleunigen. Standard-Wert für Cache ist 6 Stunden und ist In-Memory
All checks were successful
release-tag / release-image (push) Successful in 2m18s
2025-07-31 22:32:19 +02:00
190cded4e5 Kompakte Ansicht finalisiert
All checks were successful
release-tag / release-image (push) Successful in 2m19s
2025-07-28 23:24:30 +02:00
4ecdd40613 Update-1 2025-07-28 23:03:07 +02:00
7c73ac7749 Anpassung UI
All checks were successful
release-tag / release-image (push) Successful in 2m15s
2025-07-28 17:44:15 +02:00
afc6bcae83 Images entfernt
All checks were successful
release-tag / release-image (push) Successful in 5m19s
2025-07-28 15:23:28 +02:00
ecf5cf9773 fix
All checks were successful
release-tag / release-image (push) Successful in 2m37s
2025-07-28 15:19:05 +02:00
33b2a2d966 fix max-width
All checks were successful
release-tag / release-image (push) Successful in 2m13s
2025-07-28 15:15:54 +02:00
e12f631c4a fix
All checks were successful
release-tag / release-image (push) Successful in 2m8s
2025-07-28 15:09:25 +02:00
3c71785849 Image hinzugefügt - Test
All checks were successful
release-tag / release-image (push) Successful in 2m47s
2025-07-28 15:03:31 +02:00
9308b914ff Waren hinzugefügt
All checks were successful
release-tag / release-image (push) Successful in 2m30s
2025-07-28 14:50:11 +02:00
4fccd54805 Verteilung angepasst, kleine Designanpassungen
All checks were successful
release-tag / release-image (push) Successful in 2m18s
2025-07-28 13:10:13 +02:00
83d8644ef9 Anpassungen
All checks were successful
release-tag / release-image (push) Successful in 2m53s
2025-07-28 12:26:17 +02:00
9 changed files with 532 additions and 130 deletions

View File

@@ -0,0 +1,123 @@
# Git(tea) Actions workflow: Build and publish standalone binaries **plus** bundled `static/` assets
# ────────────────────────────────────────────────────────────────────
# ✧ Builds the Gobased WoL server for four targets **and** packt das Verzeichnis
# `static` zusammen mit der Binary, sodass es relativ zur ausführbaren Datei
# liegt (wichtig für die eingebauten BootstrapAssets & favicon).
#
# • linux/amd64 → wol-server-linux-amd64.tar.gz
# • linux/arm64 → wol-server-linux-arm64.tar.gz
# • linux/arm/v7 → wol-server-linux-armv7.tar.gz
# • windows/amd64 → wol-server-windows-amd64.zip
#
# ✧ Artefakte landen im Workflow und bei TagPush (vX.Y.Z) als ReleaseAssets.
#
# Secrets/variables:
# GITEA_TOKEN optional, falls default token keine ReleaseRechte hat.
# ────────────────────────────────────────────────────────────────────
name: build-binaries
on:
push:
branches: [ "main" ]
tags: [ "v*" ]
jobs:
build:
runs-on: ubuntu-fast
strategy:
matrix:
include:
- goos: linux
goarch: amd64
ext: ""
- goos: linux
goarch: arm64
ext: ""
- goos: linux
goarch: arm
goarm: "7"
ext: ""
- goos: windows
goarch: amd64
ext: ".exe"
env:
GO_VERSION: "1.24"
BINARY_NAME: trading-server
steps:
- name: Checkout source
uses: actions/checkout@v3
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Build ${{ matrix.goos }}/${{ matrix.goarch }}${{ matrix.goarm && format('/v{0}', matrix.goarm) || '' }}
shell: bash
run: |
set -e
mkdir -p dist/package
if [ -n "${{ matrix.goarm }}" ]; then export GOARM=${{ matrix.goarm }}; fi
CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -trimpath -ldflags "-s -w" \
-o "dist/package/${BINARY_NAME}${{ matrix.ext }}" .
# Assets: statisches Verzeichnis beilegen
cp -r static dist/package/
- name: Package archive with static assets
shell: bash
run: |
set -e
cd dist
if [ "${{ matrix.goos }}" == "windows" ]; then
ZIP_NAME="${BINARY_NAME}-windows-amd64.zip"
(cd package && zip -r "../$ZIP_NAME" .)
else
ARCH_SUFFIX="${{ matrix.goarch }}"
if [ "${{ matrix.goarch }}" == "arm" ]; then ARCH_SUFFIX="armv${{ matrix.goarm }}"; fi
TAR_NAME="${BINARY_NAME}-${{ matrix.goos }}-${ARCH_SUFFIX}.tar.gz"
tar -czf "$TAR_NAME" -C package .
fi
- name: Upload workflow artifact
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm && format('v{0}', matrix.goarm) || '' }}
path: dist/*.tar.gz
if-no-files-found: ignore
- uses: actions/upload-artifact@v3
with:
name: windows-amd64
path: dist/*.zip
if-no-files-found: ignore
# Release Schritt für TagPushes
release:
if: startsWith(github.ref, 'refs/tags/')
needs: build
runs-on: ubuntu-fast
permissions:
contents: write
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
path: ./dist
- name: Create / Update release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN || github.token }}
with:
name: "Release ${{ github.ref_name }}"
tag_name: ${{ github.ref_name }}
draft: false
prerelease: false
files: |
dist/**/${BINARY_NAME}-*.tar.gz
dist/**/${BINARY_NAME}-*.zip

BIN
data.db

Binary file not shown.

BIN
dynamicsrc/footerlower.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
dynamicsrc/footerupper.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

3
go.mod
View File

@@ -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
View File

@@ -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=

378
main.go
View File

@@ -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
)
@@ -42,28 +46,29 @@ var (
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",
}
)
@@ -143,6 +148,9 @@ type Monatsstatistik struct {
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",
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) {
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,10 +392,16 @@ 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)
})
http.HandleFunc("/markaspaid", func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Method", "/markaspaid", r.Method)
if !isAuthenticated(r) {
http.Error(w, "Nicht autorisiert", http.StatusUnauthorized)
return
@@ -308,10 +411,16 @@ 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)
})
http.HandleFunc("/unmarkaspaid", func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Method", "/unmarkaspaid", r.Method)
if !isAuthenticated(r) {
http.Error(w, "Nicht autorisiert", http.StatusUnauthorized)
return
@@ -321,10 +430,16 @@ 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)
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Method", "/", r.Method)
if r.Method == http.MethodPost {
if !isAuthenticated(r) {
http.Error(w, "Nicht autorisiert", http.StatusUnauthorized)
@@ -343,15 +458,47 @@ func main() {
wareStr := strings.Join(ware, ", ")
zeitaufwand, _ := strconv.ParseFloat(r.FormValue("zeitaufwand"), 64)
e := Entry{
Anfangsbestand: anfang,
Endbestand: ende,
Gesamtwert: diff,
Prozentwert: prozent,
Abgabe: abgabe,
Startort: startort,
Zielort: zielort,
Schiff: schiff,
Ware: wareStr,
Zeitaufwand: zeitaufwand,
}
go sendDiscordWebhook(e)
_, err := db.Exec(`INSERT INTO eintraege (anfangsbestand, endbestand, prozentwert, abgabe, created_at, startort, zielort, schiff, ware, zeitaufwand) VALUES (?, ?, ?, ?, datetime('now'), ?, ?, ?, ?, ?)`, anfang, ende, prozent, abgabe, startort, zielort, schiff, wareStr, zeitaufwand)
if err != nil {
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()
fmt.Println("validCache:", validCache)
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 +584,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 +608,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 +621,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 +631,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()
computed.LoggedIn = isAuthenticated(r)
tmpl.Execute(w, computed)
/*tmpl.Execute(w, struct {
Entries []Entry
Summe float64
OffeneSumme float64
@@ -507,7 +678,7 @@ func main() {
Orte: orte,
Schiffe: schiffe,
Waren: waren,
})
})*/
})
@@ -515,6 +686,63 @@ 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
var discordWebhook = GetENV("DISCORD_WEBHOOK_URL", "")
func sendDiscordWebhook(entry Entry) {
if discordWebhook == "" {
return
}
message := fmt.Sprintf(
"📦 **Neuer Abgabe-Eintrag**\n"+
"**UEC:** %s → %s (%s UEC Profit)\n"+
"**Abgabe:** %s UEC (%s%%)\n"+
"**Route:** %s → %s mit %s\n"+
"**Ware:** %s\n"+
"**Dauer:** %.0f Minuten",
formatNumber(entry.Anfangsbestand),
formatNumber(entry.Endbestand),
formatNumber(entry.Gesamtwert),
formatNumber(entry.Abgabe),
formatNumber(entry.Prozentwert),
entry.Startort,
entry.Zielort,
entry.Schiff,
entry.Ware,
entry.Zeitaufwand,
)
payload := map[string]string{
"content": message,
}
jsonData, _ := json.Marshal(payload)
http.Post(discordWebhook, "application/json", strings.NewReader(string(jsonData)))
}
func createTable(db *sql.DB) {
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS eintraege (
@@ -628,7 +856,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,10 +912,68 @@ const htmlTemplate = `
</div>
<button type="submit" class="btn btn-primary">Berechnen & Speichern</button>
</form>
<hr />
{{end}}
<h2 class="mb-3">Auswertungen</h2>
<h2 class="mb-3">Gespeicherte Einträge</h2>
<ul class="nav nav-tabs" id="auswertungTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="monat-tab" data-bs-toggle="tab" data-bs-target="#monat" type="button" role="tab" aria-controls="monat" aria-selected="true">
Monatliche Übersicht
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="abteilung-tab" data-bs-toggle="tab" data-bs-target="#abteilung" type="button" role="tab" aria-controls="abteilung" aria-selected="false">
Verteilung auf Abteilungen
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="items-tab" data-bs-toggle="tab" data-bs-target="#items" type="button" role="tab" aria-controls="items" aria-selected="false">
Gegenwert in Items
</button>
</li>
</ul>
<div class="tab-content border border-top-0 p-4 bg-white" id="auswertungTabsContent">
<!-- Monatliche Übersicht -->
<div class="tab-pane fade show active" id="monat" role="tabpanel" aria-labelledby="monat-tab">
<h5 class="mb-3">Monatliche Übersicht</h5>
<table class="table table-bordered">
<thead>
<tr>
<th>Monat</th>
<th>Abgaben verteilt</th>
<th>Abgaben offen</th>
<th>Statistik</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{{range .Monatsstatistik}}
<tr>
<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>
@@ -699,11 +985,11 @@ const htmlTemplate = `
<th>Prozent</th>
<th>UEC Abgabe</th>
<th>Status</th>
{{if .LoggedIn}}<th>Aktion</th>{{else}}<th>Erweitert</th>{{end}}
{{if $.LoggedIn}}<th>Aktion</th>{{else}}<th>Erweitert</th>{{end}}
</tr>
</thead>
<tbody id="eintragsTabelle">
{{range .Entries}}
{{range .Eintraege}}
<tr>
<td>{{.ID}}</td>
<td>{{formatDate .CreatedAt}}</td>
@@ -715,15 +1001,15 @@ const htmlTemplate = `
<td>
{{if .Bezahlt}}
{{if $.LoggedIn}}
<a href="/unmarkaspaid?id={{.ID}}" class="btn btn-sm btn-outline-success">Als unverteilt markieren</a>
<a href="/unmarkaspaid?id={{.ID}}" class="btn btn-sm btn-outline-danger">✗ stornieren</a>
{{else}}
<span class="badge bg-success">✓ verteilt</span>
<span class="badge bg-success">✓ Erledigt</span>
{{end}}
{{else}}
{{if $.LoggedIn}}
<a href="/markaspaid?id={{.ID}}" class="btn btn-sm btn-outline-success">Als verteilt markieren</a>
<a href="/markaspaid?id={{.ID}}" class="btn btn-sm btn-outline-success">✓ abgeben</a>
{{else}}
<span class="badge bg-danger">✗ nicht verteilt</span>
<span class="badge bg-danger">✗ Offen</span>
{{end}}
{{end}}
</td>
@@ -768,48 +1054,9 @@ const htmlTemplate = `
{{end}}
</tbody>
</table>
<hr />
<h2 class="mb-3">Auswertungen</h2>
<ul class="nav nav-tabs" id="auswertungTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="monat-tab" data-bs-toggle="tab" data-bs-target="#monat" type="button" role="tab" aria-controls="monat" aria-selected="true">
Monatliche Übersicht
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="abteilung-tab" data-bs-toggle="tab" data-bs-target="#abteilung" type="button" role="tab" aria-controls="abteilung" aria-selected="false">
Verteilung auf Abteilungen
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="items-tab" data-bs-toggle="tab" data-bs-target="#items" type="button" role="tab" aria-controls="items" aria-selected="false">
Gegenwert in Items
</button>
</li>
</ul>
<div class="tab-content border border-top-0 p-4 bg-white" id="auswertungTabsContent">
<!-- Monatliche Übersicht -->
<div class="tab-pane fade show active" id="monat" role="tabpanel" aria-labelledby="monat-tab">
<h5 class="mb-3">Monatliche Übersicht</h5>
<table class="table table-bordered">
<thead>
<tr>
<th>Monat</th>
<th>Abgaben verteilt</th>
<th>Abgaben offen</th>
</tr>
</thead>
<tbody>
{{range .Monatsstatistik}}
<tr>
<td>{{.Monat}}</td>
<td>{{formatNumber .Summe}} UEC</td>
<td>{{formatNumber .SummeOffen}} UEC</td>
</div>
</div>
</td>
</tr>
{{end}}
</tbody>
@@ -892,6 +1139,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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
static/img/footerupper.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB