Änderungen an UI, POIs hinzugefügt, Suchlisten, Waren, Schiffsauswahl, Start-, Zielort und Zeitaufwand hinzugefügt.
All checks were successful
release-tag / release-image (push) Successful in 2m47s

This commit is contained in:
2025-07-26 15:36:58 +02:00
parent d6d5b4b647
commit ca7eadfcb2
6 changed files with 5328 additions and 35 deletions

View File

@@ -14,18 +14,23 @@ FROM alpine:3.20
# HTTPS-Callouts in Alpine brauchen ca-certificates
RUN apk add --no-cache ca-certificates
RUN mkdir /data
RUN mkdir /dynamicsrc
RUN mkdir /tempsrc
COPY --from=builder /bin/sctradingtool /bin/sctradingtool
COPY ./static /data/static
COPY ./static /tempsrc/static
COPY ./dynamicsrc /dynamicsrc
# 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
ENV KT_USERNAME=admin \
KT_PASSWORD=admin \
KT_MEMBER=guest \
KT_HASIMPRESSUM=true \
KT_IMPRESSUM=https://www.google.de \
KT_PRODUCTIVE=true
ENTRYPOINT ["/bin/sctradingtool"]

BIN
data.db

Binary file not shown.

1
dynamicsrc/pois.json Normal file

File diff suppressed because one or more lines are too long

326
main.go
View File

@@ -2,6 +2,8 @@ package main
import (
"database/sql"
"encoding/json"
"fmt"
"html/template"
"log"
"math"
@@ -15,6 +17,9 @@ import (
_ "modernc.org/sqlite" // statt github.com/mattn/go-sqlite3
)
/* Quellen */
/* https://starmap.space/api/v3/oc/index.php */
func GetENV(k, d string) string {
if v := os.Getenv(k); v != "" {
return v
@@ -37,8 +42,74 @@ var (
productive = Enabled("KT_PRODUCTIVE", false)
hasimpressum = Enabled("KT_HASIMPRESSUM", false)
impressum = GetENV("KT_IMPRESSUM", "")
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",
"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",
"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",
"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"}
)
type POI struct {
ItemID int `json:"item_id"`
System string `json:"System"`
Planet string `json:"Planet"`
PoiName string `json:"PoiName"`
Type string `json:"Type"`
Classification string `json:"Classification"`
Latitude float64 `json:"Latitude"`
Longitude float64 `json:"Longitude"`
Longitude360 float64 `json:"Longitude360"`
Height float64 `json:"Height"`
XCoord float64 `json:"XCoord"`
YCoord float64 `json:"YCoord"`
ZCoord float64 `json:"ZCoord"`
QTMarker int `json:"QTMarker"`
NextPOI string `json:"NextPOI"`
NextQTMarker string `json:"NextQTMarker"`
Comment string `json:"Comment"`
Submitted string `json:"Submitted"` // oder time.Time, falls du umwandelst
Introduced string `json:"Introduced"` // z.B. "Unknown"
GUID string `json:"GUID"`
POISize *string `json:"POI_Size"`
POIType *string `json:"POI_Type"`
POIEntries string `json:"POI_Entries"`
POIAccessableFoot string `json:"POI_Accessable_Foot"`
POIAccessableVehicle string `json:"POI_Accessable_Vehicle"`
POIAccessableShip string `json:"POI_Accessable_Ship"`
POIDefenses string `json:"POI_Defenses"`
POILandingPads string `json:"POI_LandingPads"`
POIVehiclePads string `json:"POI_VehiclePads"`
POITerminals string `json:"POI_Terminals"`
POIMedBay string `json:"POI_MedBay"`
POIServices string `json:"POI_Services"`
POIAtmosphere *string `json:"POI_Atmosphere"`
POINPCs string `json:"POI_NPCs"`
ZoneArmistice int `json:"Zone_Armistice"`
ZoneNoFly int `json:"Zone_NoFly"`
ZoneTrespassing int `json:"Zone_Trespassing"`
ZoneBiome string `json:"Zone_Biome"`
ZoneGravitation int `json:"Zone_Gravitation"`
ZoneTemperatureMin string `json:"Zone_Temperature_Min"`
ZoneTemperatureMax string `json:"Zone_Temperature_Max"`
Minerals *string `json:"Minerals"`
SpecialLoot string `json:"SpecialLoot"`
Missions string `json:"Missions"`
}
type Entry struct {
ID int
Anfangsbestand float64
@@ -48,6 +119,11 @@ type Entry struct {
Gesamtwert float64
Bezahlt bool
CreatedAt string
Startort string
Zielort string
Schiff string
Ware string
Zeitaufwand float64
}
type Abteilung struct {
@@ -111,13 +187,39 @@ func isAuthenticated(r *http.Request) bool {
func main() {
var (
db *sql.DB
err error
db *sql.DB
err error
data []byte
)
if productive {
db, err = sql.Open("sqlite", "/data/data.db")
if err != nil {
panic(err)
}
data, err = os.ReadFile("/dynamicsrc/pois.json")
if err != nil {
panic(err)
}
} else {
db, err = sql.Open("sqlite", "./data.db")
if err != nil {
panic(err)
}
data, err = os.ReadFile("./dynamicsrc/pois.json")
if err != nil {
panic(err)
}
}
var pois []POI
if err := json.Unmarshal(data, &pois); err != nil {
panic(err)
}
for _, poi := range pois {
formatted := fmt.Sprintf("%s - %s - %s (%s)", poi.System, poi.Planet, poi.PoiName, poi.Type)
orte = append(orte, formatted)
}
//
if err != nil {
@@ -228,8 +330,13 @@ func main() {
prozent, _ := strconv.ParseFloat(r.FormValue("prozentwert"), 64)
diff := ende - anfang
abgabe := (diff / 100) * prozent
startort := r.FormValue("startort")
zielort := r.FormValue("zielort")
schiff := r.FormValue("schiff")
ware := r.FormValue("ware")
zeitaufwand, _ := strconv.ParseFloat(r.FormValue("zeitaufwand"), 64)
_, err := db.Exec(`INSERT INTO eintraege (anfangsbestand, endbestand, prozentwert, abgabe, created_at) VALUES (?, ?, ?, ?, datetime('now'))`, anfang, ende, prozent, abgabe)
_, 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, ware, zeitaufwand)
if err != nil {
http.Error(w, "Fehler beim Einfügen", http.StatusInternalServerError)
return
@@ -238,7 +345,7 @@ func main() {
return
}
rows, err := db.Query(`SELECT id, anfangsbestand, endbestand, prozentwert, abgabe, bezahlt, created_at 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 {
http.Error(w, "Fehler beim Abrufen", http.StatusInternalServerError)
return
@@ -252,7 +359,13 @@ func main() {
var e Entry
var bezahlt int
var createdAt sql.NullString
err := rows.Scan(&e.ID, &e.Anfangsbestand, &e.Endbestand, &e.Prozentwert, &e.Abgabe, &bezahlt, &createdAt)
var startort sql.NullString
var zielort sql.NullString
var schiff sql.NullString
var ware sql.NullString
var zeitaufwand sql.NullFloat64
err := rows.Scan(&e.ID, &e.Anfangsbestand, &e.Endbestand, &e.Prozentwert, &e.Abgabe, &bezahlt, &createdAt, &startort, &zielort, &schiff, &ware, &zeitaufwand)
if err != nil {
log.Println("Fehler beim Scan:", err)
continue
@@ -271,6 +384,36 @@ func main() {
} else {
e.CreatedAt = "unbekannt"
}
/**/
if startort.Valid {
e.Startort = startort.String
} else {
e.Startort = "unbekannt"
}
if zielort.Valid {
e.Zielort = zielort.String
} else {
e.Zielort = "unbekannt"
}
if schiff.Valid {
e.Schiff = schiff.String
} else {
e.Schiff = "unbekannt"
}
if ware.Valid {
e.Ware = ware.String
} else {
e.Ware = "unbekannt"
}
if zeitaufwand.Valid {
e.Zeitaufwand = zeitaufwand.Float64
} else {
e.Zeitaufwand = 0
}
eintraege = append(eintraege, e)
@@ -341,6 +484,9 @@ func main() {
Member string
HasImpressum bool
Impressum string
Orte []string
Schiffe []string
Waren []string
}{
Entries: eintraege,
Summe: summe,
@@ -351,7 +497,11 @@ func main() {
Member: membername,
HasImpressum: hasimpressum,
Impressum: impressum,
Orte: orte,
Schiffe: schiffe,
Waren: waren,
})
})
log.Println("Server läuft auf http://0.0.0.0:8080")
@@ -366,22 +516,34 @@ func createTable(db *sql.DB) {
endbestand REAL,
prozentwert REAL,
abgabe REAL,
bezahlt INTEGER DEFAULT 0
bezahlt INTEGER DEFAULT 0,
created_at TEXT,
startort TEXT,
zielort TEXT,
schiff TEXT,
ware TEXT,
zeitaufwand INTEGER
);
`)
if err != nil {
log.Fatal(err)
}
// Falls die Tabelle schon existiert, aber die Spalte "bezahlt" fehlt (z.B. nach Update)
_, err = db.Exec(`ALTER TABLE eintraege ADD COLUMN bezahlt INTEGER DEFAULT 0;`)
if err != nil && !strings.Contains(err.Error(), "duplicate column") {
log.Fatal(err)
}
_, err = db.Exec(`ALTER TABLE eintraege ADD COLUMN created_at TEXT`)
if err != nil && !strings.Contains(err.Error(), "duplicate column") {
log.Fatal(err)
// Ergänze ALTER TABLE nur für Migration bestehender Tabellen
addColumn := func(column, colType string) {
_, err := db.Exec(`ALTER TABLE eintraege ADD COLUMN ` + column + ` ` + colType)
if err != nil && !strings.Contains(err.Error(), "duplicate column") {
log.Fatalf("Fehler beim Hinzufügen der Spalte %s: %v", column, err)
}
}
addColumn("bezahlt", "INTEGER DEFAULT 0")
addColumn("created_at", "TEXT")
addColumn("startort", "TEXT")
addColumn("zielort", "TEXT")
addColumn("schiff", "TEXT")
addColumn("ware", "TEXT")
addColumn("zeitaufwand", "INTEGER")
}
const loginForm = `
@@ -419,6 +581,7 @@ const htmlTemplate = `
<meta charset="UTF-8">
<title>Abgabe-Berechnung</title>
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/tom-select.default.min.css" rel="stylesheet">
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
</head>
<body class="bg-light">
@@ -471,6 +634,47 @@ const htmlTemplate = `
</select>
</div>
</div>
<div class="row mb-3">
<div class="col">
<label class="form-label">Startort</label>
<select id="startort" name="startort" class="form-select">
{{range .Orte}}
<option value="{{.}}">{{.}}</option>
{{end}}
</select>
</div>
<div class="col">
<label class="form-label">Zielort</label>
<select id="zielort" name="zielort" class="form-select">
{{range .Orte}}
<option value="{{.}}">{{.}}</option>
{{end}}
</select>
</div>
</div>
<div class="row mb-3">
<div class="col">
<label class="form-label">Schiff</label>
<select id="schiff" name="schiff" class="form-select">
{{range .Schiffe}}
<option value="{{.}}">{{.}}</option>
{{end}}
</select>
</div>
<div class="col">
<label class="form-label">Ware</label>
<select id="ware" name="ware" class="form-select">
{{range .Waren}}
<option value="{{.}}">{{.}}</option>
{{end}}
</select>
</div>
<div class="col">
<label class="form-label">Zeitaufwand (min)</label>
<input type="number" name="zeitaufwand" class="form-control" min="1" required>
</div>
</div>
<button type="submit" class="btn btn-primary">Berechnen & Speichern</button>
</form>
{{end}}
@@ -488,10 +692,10 @@ const htmlTemplate = `
<th>Prozent</th>
<th>UEC Abgabe</th>
<th>Status</th>
{{if .LoggedIn}}<th>Aktion</th>{{end}}
{{if .LoggedIn}}<th>Aktion</th>{{else}}<th>Erweitert</th>{{end}}
</tr>
</thead>
<tbody>
<tbody id="eintragsTabelle">
{{range .Entries}}
<tr>
<td>{{.ID}}</td>
@@ -500,29 +704,59 @@ const htmlTemplate = `
<td>{{formatNumber .Endbestand}}</td>
<td>{{formatNumber .Gesamtwert}}</td>
<td>{{formatNumber .Prozentwert}}%</td>
<td>
{{formatNumber .Abgabe}}
</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>
{{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}}
<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}}
{{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>
<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>
{{end}}
</tr>
{{end}}
</tbody>
@@ -652,6 +886,36 @@ const htmlTemplate = `
<hr />
</div>
<script src="/static/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/tom-select.complete.min.js"></script>
<script>
new TomSelect("#zielort", {
create: true, // erlaubt Freitext
sortField: "text"
});
</script>
<script>
new TomSelect("#startort", {
create: true, // erlaubt Freitext
sortField: "text"
});
</script>
<script>
new TomSelect("#schiff", {
create: true, // erlaubt Freitext
sortField: "text"
});
</script>
<script>
new TomSelect("#ware", {
create: true, // erlaubt Freitext
sortField: "text"
});
</script>
<script>
document.addEventListener("DOMContentLoaded", function () {
const tabKey = "lastActiveTab";

2
static/css/tom-select.default.min.css vendored Normal file

File diff suppressed because one or more lines are too long

5021
static/js/tom-select.complete.min.js vendored Normal file

File diff suppressed because it is too large Load Diff