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/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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/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/libc v1.65.10 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // 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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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= 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/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 h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc=
modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po= 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 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 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 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= 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 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE= 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=

506
main.go
View File

@@ -1,7 +1,9 @@
package main package main
import ( import (
"crypto/rand"
"database/sql" "database/sql"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"html/template" "html/template"
@@ -12,8 +14,10 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"golang.org/x/crypto/bcrypt"
_ "modernc.org/sqlite" // statt github.com/mattn/go-sqlite3 _ "modernc.org/sqlite" // statt github.com/mattn/go-sqlite3
) )
@@ -36,34 +40,35 @@ func Enabled(k string, def bool) bool {
} }
var ( var (
username = GetENV("KT_USERNAME", "root") username = GetENV("KT_USERNAME", "root")
password = GetENV("KT_PASSWORD", "root") password = GetENV("KT_PASSWORD", "root")
membername = GetENV("KT_MEMBER", "demo") membername = GetENV("KT_MEMBER", "demo")
productive = Enabled("KT_PRODUCTIVE", false) productive = Enabled("KT_PRODUCTIVE", false)
hasimpressum = Enabled("KT_HASIMPRESSUM", false) hasimpressum = Enabled("KT_HASIMPRESSUM", false)
impressum = GetENV("KT_IMPRESSUM", "") impressum = GetENV("KT_IMPRESSUM", "")
orte = []string{} hashedPassword = ""
schiffe = []string{ orte = []string{}
schiffe = []string{
"", "100i", "125a", "135c", "Arrow", "Aurora CL", "Aurora ES", "Aurora LN", "Aurora LX", "Aurora MR", "", "100i", "125a", "135c", "Arrow", "Aurora CL", "Aurora ES", "Aurora LN", "Aurora LX", "Aurora MR",
"Avenger Stalker", "Avenger Titan", "Avenger Titan Renegade", "Avenger Warlock", "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", "C8X Pisces Expedition", "Carrack", "Caterpillar", "Constellation Andromeda",
"Constellation Aquila", "Constellation Phoenix", "Constellation Taurus", "Corsair", "Constellation Aquila", "Constellation Phoenix", "Constellation Taurus", "Corsair",
"Cutlass Black", "Cutlass Blue", "Cutlass Red", "Cutter", "Defender", "Eclipse", "Cutlass Black", "Cutlass Blue", "Cutlass Red", "Cutter", "Defender", "Eclipse",
"Freelancer", "Freelancer DUR", "Freelancer MAX", "Gladiator", "Gladius", "Glaive", "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", "Khartu-Al", "Kraken", "M50", "Merchantman", "Mercury Star Runner", "Mustang Alpha",
"Mustang Beta", "Mustang Delta", "Mustang Gamma", "Mustang Omega", "Nomad", "Mustang Beta", "Mustang Delta", "Mustang Gamma", "Mustang Omega", "Nomad",
"Orion", "P-52 Merlin", "P-72 Archimedes", "Prospector", "Prowler", "Prowler Utility", "Raft", "Orion", "P-52 Merlin", "P-72 Archimedes", "Prospector", "Prowler", "Prowler Utility", "Raft",
"Reclaimer", "Redeemer", "Reliant Kore", "Reliant Mako", "Reliant Sen", "Reliant Tana", "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", "Terrapin", "Vulture", "Hull-A", "Hull-C", "Zeus ES", "Zeus CL",
// …weitere Capital- und Concept-Schiffe sind ebenfalls bekannt! // …weitere Capital- und Concept-Schiffe sind ebenfalls bekannt!
} }
waren = []string{ waren = []string{
"", "Laranite", "Titanium", "Medical Supplies", "Gold", "Agricium", "Hydrogen", "Nitrogen", "Astatine", "", "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", "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", "Corundum", "Diamond", "Dolivine", "Hadanite", "Hephaestanite", "Laranite", "Quartz", "Taranite", "Stims", "Carbon", "Slam", "Distilled Spirits", "Maze", "Gasping Weevil Eggs", "E'tam", "Iron", "Methane",
} }
) )
@@ -140,9 +145,12 @@ type Abteilung struct {
} }
type Monatsstatistik struct { type Monatsstatistik struct {
Monat string // z.B. "07.2025" Monat string // z.B. "07.2025"
Summe float64 // bezahlte Summe float64 // bezahlte
SummeOffen float64 // noch nicht bezahlt SummeOffen float64 // noch nicht bezahlt
Prozent float64
ProzentOffen float64
Eintraege []Entry
} }
var tmpl = template.Must(template.New("form").Funcs(template.FuncMap{ 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 { func isAuthenticated(r *http.Request) bool {
cookie, err := r.Cookie("session") 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() { func main() {
@@ -216,6 +254,8 @@ func main() {
} }
} }
hashedPassword = hashPassword(password)
var pois []POI var pois []POI
if err := json.Unmarshal(data, &pois); err != nil { if err := json.Unmarshal(data, &pois); err != nil {
panic(err) panic(err)
@@ -242,34 +282,86 @@ func main() {
} }
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { 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 { if r.Method == http.MethodPost {
r.ParseForm() r.ParseForm()
user := r.FormValue("username") user := r.FormValue("username")
pass := r.FormValue("password") 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{ http.SetCookie(w, &http.Cookie{
Name: "session", Name: "session",
Value: "authenticated", Value: token,
Path: "/", 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) http.Redirect(w, r, "/", http.StatusSeeOther)
return 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) http.Error(w, "Login fehlgeschlagen", http.StatusUnauthorized)
return return
} }
// GET: Login-Formular
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
w.Write([]byte(loginForm)) w.Write([]byte(loginForm))
}) })
http.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{ cookie, err := r.Cookie("session")
Name: "session", if err == nil {
Value: "", token := cookie.Value
Path: "/", // Token aus dem serverseitigen Store löschen
MaxAge: -1, 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) http.Redirect(w, r, "/", http.StatusSeeOther)
}) })
@@ -282,6 +374,11 @@ func main() {
if id != "" { if id != "" {
db.Exec("DELETE FROM eintraege WHERE id = ?", 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) http.Redirect(w, r, "/", http.StatusSeeOther)
}) })
@@ -295,10 +392,16 @@ func main() {
// Auto-Increment-Zähler zurücksetzen // Auto-Increment-Zähler zurücksetzen
db.Exec("DELETE FROM sqlite_sequence WHERE name='eintraege'") 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.Redirect(w, r, "/", http.StatusSeeOther)
}) })
http.HandleFunc("/markaspaid", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/markaspaid", func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Method", "/markaspaid", r.Method)
if !isAuthenticated(r) { if !isAuthenticated(r) {
http.Error(w, "Nicht autorisiert", http.StatusUnauthorized) http.Error(w, "Nicht autorisiert", http.StatusUnauthorized)
return return
@@ -308,10 +411,16 @@ func main() {
if id != "" { if id != "" {
db.Exec("UPDATE eintraege SET bezahlt = 1 WHERE id = ?", 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.Redirect(w, r, "/", http.StatusSeeOther)
}) })
http.HandleFunc("/unmarkaspaid", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/unmarkaspaid", func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Method", "/unmarkaspaid", r.Method)
if !isAuthenticated(r) { if !isAuthenticated(r) {
http.Error(w, "Nicht autorisiert", http.StatusUnauthorized) http.Error(w, "Nicht autorisiert", http.StatusUnauthorized)
return return
@@ -321,10 +430,16 @@ func main() {
if id != "" { if id != "" {
db.Exec("UPDATE eintraege SET bezahlt = 0 WHERE id = ?", 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.Redirect(w, r, "/", http.StatusSeeOther)
}) })
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Method", "/", r.Method)
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
if !isAuthenticated(r) { if !isAuthenticated(r) {
http.Error(w, "Nicht autorisiert", http.StatusUnauthorized) http.Error(w, "Nicht autorisiert", http.StatusUnauthorized)
@@ -343,15 +458,47 @@ func main() {
wareStr := strings.Join(ware, ", ") wareStr := strings.Join(ware, ", ")
zeitaufwand, _ := strconv.ParseFloat(r.FormValue("zeitaufwand"), 64) 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) _, 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 { if err != nil {
http.Error(w, "Fehler beim Einfügen", http.StatusInternalServerError) http.Error(w, "Fehler beim Einfügen", http.StatusInternalServerError)
return return
} }
cacheMutex.Lock()
cache.LastComputed = time.Time{} // auf null zurücksetzen
cacheMutex.Unlock()
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return 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`) rows, err := db.Query(`SELECT id, anfangsbestand, endbestand, prozentwert, abgabe, bezahlt, created_at, startort, zielort, schiff, ware, zeitaufwand FROM eintraege`)
if err != nil { if err != nil {
http.Error(w, "Fehler beim Abrufen", http.StatusInternalServerError) http.Error(w, "Fehler beim Abrufen", http.StatusInternalServerError)
@@ -437,11 +584,14 @@ func main() {
if _, ok := monatsMap[monatKey]; !ok { if _, ok := monatsMap[monatKey]; !ok {
monatsMap[monatKey] = &Monatsstatistik{Monat: monatKey} monatsMap[monatKey] = &Monatsstatistik{Monat: monatKey}
} }
monatsMap[monatKey].Eintraege = append(monatsMap[monatKey].Eintraege, e)
if e.Bezahlt { if e.Bezahlt {
monatsMap[monatKey].Summe += e.Abgabe monatsMap[monatKey].Summe += e.Abgabe
} else { } else {
monatsMap[monatKey].SummeOffen += e.Abgabe 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 var monatsStat []Monatsstatistik
@@ -458,7 +608,7 @@ func main() {
//test //test
// Dynamische Abteilungen frei anpassbar // Dynamische Abteilungen frei anpassbar
abteilungen := []Abteilung{ 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: "Bodenkampf", Anteil: 8, Beispiel: "P4-AR Rifle", WertItem: 5900},
{Name: "Racing", Anteil: 3, Beispiel: "LumaCore - Power Plant", WertItem: 69300}, {Name: "Racing", Anteil: 3, Beispiel: "LumaCore - Power Plant", WertItem: 69300},
{Name: "Medical", Anteil: 5, Beispiel: "ParaMed Medical Device", WertItem: 1250}, {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: "Basebuilding (+10)", Anteil: 0, Beispiel: "CF-337 Panther", WertItem: 36308},
{Name: "Crafting (+8)", 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: "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: "Roleplay", Anteil: 3, Beispiel: "Clothing", WertItem: 8400},
{Name: "Kunstflug", Anteil: 3, Beispiel: "Beacon Undersuit Crimson", WertItem: 1000}, {Name: "Kunstflug", Anteil: 3, Beispiel: "Beacon Undersuit Crimson", WertItem: 1000},
} }
@@ -481,7 +631,28 @@ func main() {
abteilungen[i].WertOffen = (abteilungen[i].Anteil / 100) * offeneSumme 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 Entries []Entry
Summe float64 Summe float64
OffeneSumme float64 OffeneSumme float64
@@ -507,7 +678,7 @@ func main() {
Orte: orte, Orte: orte,
Schiffe: schiffe, Schiffe: schiffe,
Waren: waren, Waren: waren,
}) })*/
}) })
@@ -515,6 +686,63 @@ func main() {
http.ListenAndServe(":8080", nil) 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) { func createTable(db *sql.DB) {
_, err := db.Exec(` _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS eintraege ( CREATE TABLE IF NOT EXISTS eintraege (
@@ -593,15 +821,15 @@ const htmlTemplate = `
</head> </head>
<body class="bg-light"> <body class="bg-light">
<div class="container mt-5"> <div class="container mt-5">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
{{if .LoggedIn}} {{if .LoggedIn}}
<form action="/logout" method="POST"> <form action="/logout" method="POST">
<button type="submit" class="btn btn-sm btn-secondary">Logout</button> <button type="submit" class="btn btn-sm btn-secondary">Logout</button>
</form> </form>
{{else}} {{else}}
<a href="/login" class="btn btn-sm btn-outline-primary">Login</a> <a href="/login" class="btn btn-sm btn-outline-primary">Login</a>
{{end}} {{end}}
</div> </div>
<h1 class="mb-4">Beitrag zur Community vom Mitglied der Trading-Staffel ({{.Member}})</h1> <h1 class="mb-4">Beitrag zur Community vom Mitglied der Trading-Staffel ({{.Member}})</h1>
<div class="alert alert-info"> <div class="alert alert-info">
@@ -628,7 +856,7 @@ const htmlTemplate = `
<div class="col"> <div class="col">
<label class="form-label">Prozentwert</label> <label class="form-label">Prozentwert</label>
<select name="prozentwert" class="form-select"> <select name="prozentwert" class="form-select">
<option value="30">30%</option> <option value="30">30% (Standard)</option>
<option value="10">10%</option> <option value="10">10%</option>
<option value="15">15%</option> <option value="15">15%</option>
<option value="20">20%</option> <option value="20">20%</option>
@@ -684,92 +912,8 @@ const htmlTemplate = `
</div> </div>
<button type="submit" class="btn btn-primary">Berechnen & Speichern</button> <button type="submit" class="btn btn-primary">Berechnen & Speichern</button>
</form> </form>
{{end}}
<h2 class="mb-3">Gespeicherte Einträge</h2>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>#</th>
<th>Datum</th>
<th>UEC Anfang</th>
<th>UEC Ende</th>
<th>UEC Profit</th>
<th>Prozent</th>
<th>UEC Abgabe</th>
<th>Status</th>
{{if .LoggedIn}}<th>Aktion</th>{{else}}<th>Erweitert</th>{{end}}
</tr>
</thead>
<tbody id="eintragsTabelle">
{{range .Entries}}
<tr>
<td>{{.ID}}</td>
<td>{{formatDate .CreatedAt}}</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>
<td>
<button class="btn btn-sm btn-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#details-{{.ID}}" aria-expanded="false" aria-controls="details-{{.ID}}">
Details
</button>
{{if $.LoggedIn}}
<a href="/delete?id={{.ID}}" class="btn btn-sm btn-danger" onclick="return confirm('Eintrag wirklich löschen?')">Löschen</a>
{{end}}
</td>
</tr>
<tr>
<td colspan="9" class="p-0 border-0">
<div class="collapse" id="details-{{.ID}}">
<div class="bg-light p-3 border-top">
<strong>Interne Infos (Details):</strong>
<table class="table table-sm table-bordered mb-0">
<thead>
<tr>
<th>Startort</th>
<th>Zielort</th>
<th>Schiff</th>
<th>Ware</th>
<th>Zeit (min)</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{.Startort}}</td>
<td>{{.Zielort}}</td>
<td>{{.Schiff}}</td>
<td>{{.Ware}}</td>
<td>{{formatNumber .Zeitaufwand}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
<hr /> <hr />
{{end}}
<h2 class="mb-3">Auswertungen</h2> <h2 class="mb-3">Auswertungen</h2>
@@ -802,6 +946,8 @@ const htmlTemplate = `
<th>Monat</th> <th>Monat</th>
<th>Abgaben verteilt</th> <th>Abgaben verteilt</th>
<th>Abgaben offen</th> <th>Abgaben offen</th>
<th>Statistik</th>
<th>Aktionen</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -810,6 +956,107 @@ const htmlTemplate = `
<td>{{.Monat}}</td> <td>{{.Monat}}</td>
<td>{{formatNumber .Summe}} UEC</td> <td>{{formatNumber .Summe}} UEC</td>
<td>{{formatNumber .SummeOffen}} 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>
<th>#</th>
<th>Datum</th>
<th>UEC Anfang</th>
<th>UEC Ende</th>
<th>UEC Profit</th>
<th>Prozent</th>
<th>UEC Abgabe</th>
<th>Status</th>
{{if $.LoggedIn}}<th>Aktion</th>{{else}}<th>Erweitert</th>{{end}}
</tr>
</thead>
<tbody id="eintragsTabelle">
{{range .Eintraege}}
<tr>
<td>{{.ID}}</td>
<td>{{formatDate .CreatedAt}}</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-danger">✗ stornieren</a>
{{else}}
<span class="badge bg-success">✓ Erledigt</span>
{{end}}
{{else}}
{{if $.LoggedIn}}
<a href="/markaspaid?id={{.ID}}" class="btn btn-sm btn-outline-success">✓ abgeben</a>
{{else}}
<span class="badge bg-danger">✗ Offen</span>
{{end}}
{{end}}
</td>
<td>
<button class="btn btn-sm btn-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#details-{{.ID}}" aria-expanded="false" aria-controls="details-{{.ID}}">
Details
</button>
{{if $.LoggedIn}}
<a href="/delete?id={{.ID}}" class="btn btn-sm btn-danger" onclick="return confirm('Eintrag wirklich löschen?')">Löschen</a>
{{end}}
</td>
</tr>
<tr>
<td colspan="9" class="p-0 border-0">
<div class="collapse" id="details-{{.ID}}">
<div class="bg-light p-3 border-top">
<strong>Interne Infos (Details):</strong>
<table class="table table-sm table-bordered mb-0">
<thead>
<tr>
<th>Startort</th>
<th>Zielort</th>
<th>Schiff</th>
<th>Ware</th>
<th>Zeit (min)</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{.Startort}}</td>
<td>{{.Zielort}}</td>
<td>{{.Schiff}}</td>
<td>{{.Ware}}</td>
<td>{{formatNumber .Zeitaufwand}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</td>
</tr> </tr>
{{end}} {{end}}
</tbody> </tbody>
@@ -892,6 +1139,11 @@ const htmlTemplate = `
{{end}} {{end}}
<hr /> <hr />
</div> </div>
<style>
.max {
max-width: 100%;
}
</style>
<script src="/static/js/bootstrap.bundle.min.js"></script> <script src="/static/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/tom-select.complete.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