This commit is contained in:
51
.gitea/workflows/registry.yml
Normal file
51
.gitea/workflows/registry.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: release-tag
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
jobs:
|
||||||
|
release-image:
|
||||||
|
runs-on: ubuntu-fast
|
||||||
|
env:
|
||||||
|
DOCKER_ORG: ${{ vars.DOCKER_ORG }}
|
||||||
|
DOCKER_LATEST: latest
|
||||||
|
RUNNER_TOOL_CACHE: /toolcache
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
with: # replace it with your local IP
|
||||||
|
config-inline: |
|
||||||
|
[registry."${{ vars.DOCKER_REGISTRY }}"]
|
||||||
|
http = true
|
||||||
|
insecure = true
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ${{ vars.DOCKER_REGISTRY }} # replace it with your local IP
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Get Meta
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
||||||
|
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: | # replace it with your local IP and tags
|
||||||
|
${{ vars.DOCKER_REGISTRY }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
|
||||||
|
${{ vars.DOCKER_REGISTRY }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
|
28
Dockerfile
Normal file
28
Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# -------- Dockerfile (Multi-Stage Build) --------
|
||||||
|
# 1. Builder-Stage
|
||||||
|
FROM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY go.* ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /bin/sctradingtool
|
||||||
|
|
||||||
|
# 2. Runtime-Stage
|
||||||
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
# HTTPS-Callouts in Alpine brauchen ca-certificates
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|
||||||
|
COPY --from=builder /bin/sctradingtool /bin/sctradingtool
|
||||||
|
|
||||||
|
# Default listens on :8080 – siehe main.go
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Environment defaults; können per compose überschrieben werden
|
||||||
|
ENV REDIS_ADDR=redis:6379 \
|
||||||
|
BLOCKLIST_MODE=slave \
|
||||||
|
HASH_NAME=bl:manual \
|
||||||
|
MASTER_URL=https://flod-proxy.send.nrw
|
||||||
|
|
||||||
|
ENTRYPOINT ["/bin/sctradingtool"]
|
82
License.md
Normal file
82
License.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# # CommonsProtect Lizenz – Nicht-kommerzielle Edition (CPL-NC)
|
||||||
|
|
||||||
|
**Version 1.0 – Stand: April 2025**
|
||||||
|
https://git.send.nrw/b1tsblog/trading
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Lizenztyp:** Quelloffen, aber nicht kommerziell nutzbar
|
||||||
|
**Autorenrecht:** © 2025 Jan Bergner
|
||||||
|
**Lizenzkürzel:** CPL-NC-1.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Diese Lizenz regelt die Bedingungen für Nutzung, Veränderung und Weiterverbreitung der in diesem Repository enthaltenen Software. Ziel ist es, offene Zusammenarbeit zu ermöglichen, gleichzeitig aber eine kommerzielle Ausbeutung ohne Erlaubnis zu verhindern.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Erlaubte Nutzung
|
||||||
|
|
||||||
|
Diese Software darf kostenlos genutzt, kopiert, verändert und weiterverbreitet werden – **ausschließlich für nicht-kommerzielle Zwecke**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Namensnennungspflicht
|
||||||
|
|
||||||
|
Wenn du den Quellcode dieser Software ganz oder in Teilen nutzt – sei es im Original oder verändert –, **musst du deutlich auf das ursprüngliche Projekt hinweisen**, inklusive:
|
||||||
|
|
||||||
|
- Name des ursprünglichen Autors oder Projekts
|
||||||
|
- Link zur Originalquelle (z.B. Git-Repository)
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
|
||||||
|
> "Diese Software basiert auf dem Trading-Produkt von B1tK1ll3r, verfügbar unter https://git.send.nrw/b1tsblog/trading."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Verbot kommerzieller Nutzung
|
||||||
|
|
||||||
|
Die Nutzung dieser Software oder abgeleiteter Werke ist **ausschließlich für nicht-kommerzielle Zwecke gestattet**. Eine kommerzielle Nutzung in jeglicher Form ist **streng untersagt**.
|
||||||
|
|
||||||
|
### Dies umfasst unter anderem:
|
||||||
|
|
||||||
|
- Den Verkauf, die Vermietung oder das Anbieten kostenpflichtiger Lizenzen oder Zugänge zur Software
|
||||||
|
- Die Integration oder Verwendung der Software in Produkten oder Dienstleistungen, die verkauft oder lizenziert werden
|
||||||
|
- Die Bereitstellung einer gehosteten Version (z.B. als Webservice oder API), bei der direkt oder indirekt Einnahmen erzielt werden (z.B. über Werbung, Abonnements, Spendenplattformen wie Patreon, Zugang gegen Bezahlung)
|
||||||
|
- Die Nutzung der Software zur Generierung von Einnahmen durch Content-Plattformen (z.B. Streaming, YouTube, Social Media), wenn das Projekt, die Plattform oder die Inhalte **auf irgendeine Weise monetarisiert** werden – sei es durch Werbung, Sponsoring, Bezahlabos, Spenden oder bezahlte Partnerschaften
|
||||||
|
|
||||||
|
**Auch teilweise Monetarisierung** – etwa durch freiwillige Spenden, bezahlten Premium-Zugang oder Monetarisierung begleitender Inhalte – fällt unter diese Regelung und ist **nicht erlaubt**.
|
||||||
|
|
||||||
|
### Ausnahmen:
|
||||||
|
|
||||||
|
Eine kommerzielle Nutzung kann nur mit ausdrücklicher, schriftlicher Erlaubnis des ursprünglichen Autors erfolgen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Keine Garantie und Haftungsausschluss
|
||||||
|
|
||||||
|
Diese Software wird **„wie besehen“** ohne ausdrückliche oder stillschweigende Garantien bereitgestellt. Insbesondere werden **keine Zusicherungen hinsichtlich Funktionalität, Eignung für einen bestimmten Zweck, Fehlerfreiheit oder Verfügbarkeit** gegeben.
|
||||||
|
|
||||||
|
Die Nutzung dieser Software erfolgt **auf eigenes Risiko**. Der Autor übernimmt keine Verantwortung für direkte oder indirekte Schäden, Datenverlust, Ausfallzeiten, Sicherheitsprobleme oder sonstige Folgen, die sich aus der Nutzung oder dem Missbrauch der Software ergeben.
|
||||||
|
|
||||||
|
### Produkthaftungsausschluss
|
||||||
|
|
||||||
|
**Insbesondere ausgeschlossen ist jegliche Haftung nach dem Produkthaftungsgesetz (§1 ProdHaftG)** oder vergleichbaren Regelungen in anderen Ländern. Diese Software wird **nicht als Produkt im Sinne der Produkthaftung bereitgestellt**, da sie ohne Gegenleistung, ohne Prüfverfahren und ohne Gewähr zur Verfügung steht.
|
||||||
|
|
||||||
|
### Nutzung in kritischen Systemen
|
||||||
|
|
||||||
|
Diese Software ist **nicht vorgesehen oder geeignet für sicherheitskritische Anwendungen** wie:
|
||||||
|
|
||||||
|
- Medizinische Geräte
|
||||||
|
- Luft- und Raumfahrttechnik
|
||||||
|
- Automatisierte Verkehrs- und Steuerungssysteme
|
||||||
|
- Industrieanlagen mit hohem Gefährdungspotenzial
|
||||||
|
- Systeme, bei denen ein Softwarefehler zu Personenschäden oder schweren Sachschäden führen könnte
|
||||||
|
|
||||||
|
Die Verwendung in solchen Bereichen erfolgt ausdrücklich auf eigene Gefahr.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hinweis
|
||||||
|
|
||||||
|
Diese Lizenz ist keine anerkannte Open-Source-Lizenz im Sinne der [Open Source Definition](https://opensource.org/osd). Sie stellt eine sogenannte **„source-available“-Lizenz** dar.
|
28
compose.yml
Normal file
28
compose.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
services:
|
||||||
|
api:
|
||||||
|
image: git.send.nrw/b1tsblog/trading:latest
|
||||||
|
container_name: trading
|
||||||
|
networks:
|
||||||
|
- tradingnw
|
||||||
|
environment:
|
||||||
|
# HIER BEARBEITEN
|
||||||
|
KT_USERNAME: Nutzername
|
||||||
|
KT_PASSWORD: Passwort
|
||||||
|
KT_MEMBER: InGame-Name
|
||||||
|
# Sollten Ports extern verfügbar gemacht werden müssen (nicht empfohlen)
|
||||||
|
#ports:
|
||||||
|
#- "8080:8080" # <host>:<container>
|
||||||
|
restart: unless-stopped
|
||||||
|
# Newt-Client für eine Pangolin-Integration
|
||||||
|
#newt:
|
||||||
|
#image: fosrl/newt
|
||||||
|
#container_name: newt
|
||||||
|
#networks:
|
||||||
|
#- tradingnw
|
||||||
|
#restart: unless-stopped
|
||||||
|
#environment:
|
||||||
|
#- PANGOLIN_ENDPOINT=
|
||||||
|
#- NEWT_ID=
|
||||||
|
#- NEWT_SECRET=
|
||||||
|
networks:
|
||||||
|
tradingnw:
|
18
go.mod
Normal file
18
go.mod
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
module git.send.nrw/b1tsblog/trading
|
||||||
|
|
||||||
|
go 1.24.4
|
||||||
|
|
||||||
|
require modernc.org/sqlite v1.38.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
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/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||||
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
|
modernc.org/libc v1.65.10 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
)
|
23
go.sum
Normal file
23
go.sum
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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/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=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
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/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/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=
|
||||||
|
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/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
|
||||||
|
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
|
451
main.go
Normal file
451
main.go
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
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.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 = `
|
||||||
|
<!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 von der Trading-Staffel ({{.Member}})</h1>
|
||||||
|
{{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="0.01" name="anfangsbestand" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label class="form-label">Endbestand</label>
|
||||||
|
<input type="number" step="0.01" 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="20">20%</option>
|
||||||
|
<option value="30">30%</option>
|
||||||
|
<option value="40">40%</option>
|
||||||
|
<option value="50">50%</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}}
|
||||||
|
<span class="badge bg-success">✓ verteilt</span>
|
||||||
|
{{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>
|
||||||
|
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<strong>Offen zur Abgabe:</strong> {{formatNumber .OffeneSumme}} UEC
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<strong>Summe aller Abgaben an die Knebel-Community:</strong> {{formatNumber .Summe}} UEC
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{{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>
|
||||||
|
`
|
6
static/css/bootstrap.min.css
vendored
Normal file
6
static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user