Ä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 # HTTPS-Callouts in Alpine brauchen ca-certificates
RUN apk add --no-cache ca-certificates RUN apk add --no-cache ca-certificates
RUN mkdir /data RUN mkdir /data
RUN mkdir /dynamicsrc
RUN mkdir /tempsrc RUN mkdir /tempsrc
COPY --from=builder /bin/sctradingtool /bin/sctradingtool COPY --from=builder /bin/sctradingtool /bin/sctradingtool
COPY ./static /data/static COPY ./static /data/static
COPY ./static /tempsrc/static COPY ./static /tempsrc/static
COPY ./dynamicsrc /dynamicsrc
# Default listens on :8080 siehe main.go # Default listens on :8080 siehe main.go
EXPOSE 8080 EXPOSE 8080
# Environment defaults; können per compose überschrieben werden # Environment defaults; können per compose überschrieben werden
ENV REDIS_ADDR=redis:6379 \ ENV KT_USERNAME=admin \
BLOCKLIST_MODE=slave \ KT_PASSWORD=admin \
HASH_NAME=bl:manual \ KT_MEMBER=guest \
MASTER_URL=https://flod-proxy.send.nrw KT_HASIMPRESSUM=true \
KT_IMPRESSUM=https://www.google.de \
KT_PRODUCTIVE=true
ENTRYPOINT ["/bin/sctradingtool"] 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

298
main.go
View File

@@ -2,6 +2,8 @@ package main
import ( import (
"database/sql" "database/sql"
"encoding/json"
"fmt"
"html/template" "html/template"
"log" "log"
"math" "math"
@@ -15,6 +17,9 @@ import (
_ "modernc.org/sqlite" // statt github.com/mattn/go-sqlite3 _ "modernc.org/sqlite" // statt github.com/mattn/go-sqlite3
) )
/* Quellen */
/* https://starmap.space/api/v3/oc/index.php */
func GetENV(k, d string) string { func GetENV(k, d string) string {
if v := os.Getenv(k); v != "" { if v := os.Getenv(k); v != "" {
return v return v
@@ -37,8 +42,74 @@ var (
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{}
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 { type Entry struct {
ID int ID int
Anfangsbestand float64 Anfangsbestand float64
@@ -48,6 +119,11 @@ type Entry struct {
Gesamtwert float64 Gesamtwert float64
Bezahlt bool Bezahlt bool
CreatedAt string CreatedAt string
Startort string
Zielort string
Schiff string
Ware string
Zeitaufwand float64
} }
type Abteilung struct { type Abteilung struct {
@@ -113,11 +189,37 @@ func main() {
var ( var (
db *sql.DB db *sql.DB
err error err error
data []byte
) )
if productive { if productive {
db, err = sql.Open("sqlite", "/data/data.db") 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 { } else {
db, err = sql.Open("sqlite", "./data.db") 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 { if err != nil {
@@ -228,8 +330,13 @@ func main() {
prozent, _ := strconv.ParseFloat(r.FormValue("prozentwert"), 64) prozent, _ := strconv.ParseFloat(r.FormValue("prozentwert"), 64)
diff := ende - anfang diff := ende - anfang
abgabe := (diff / 100) * prozent 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 { if err != nil {
http.Error(w, "Fehler beim Einfügen", http.StatusInternalServerError) http.Error(w, "Fehler beim Einfügen", http.StatusInternalServerError)
return return
@@ -238,7 +345,7 @@ func main() {
return 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 { if err != nil {
http.Error(w, "Fehler beim Abrufen", http.StatusInternalServerError) http.Error(w, "Fehler beim Abrufen", http.StatusInternalServerError)
return return
@@ -252,7 +359,13 @@ func main() {
var e Entry var e Entry
var bezahlt int var bezahlt int
var createdAt sql.NullString 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 { if err != nil {
log.Println("Fehler beim Scan:", err) log.Println("Fehler beim Scan:", err)
continue continue
@@ -271,6 +384,36 @@ func main() {
} else { } else {
e.CreatedAt = "unbekannt" 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) eintraege = append(eintraege, e)
@@ -341,6 +484,9 @@ func main() {
Member string Member string
HasImpressum bool HasImpressum bool
Impressum string Impressum string
Orte []string
Schiffe []string
Waren []string
}{ }{
Entries: eintraege, Entries: eintraege,
Summe: summe, Summe: summe,
@@ -351,7 +497,11 @@ func main() {
Member: membername, Member: membername,
HasImpressum: hasimpressum, HasImpressum: hasimpressum,
Impressum: impressum, Impressum: impressum,
Orte: orte,
Schiffe: schiffe,
Waren: waren,
}) })
}) })
log.Println("Server läuft auf http://0.0.0.0:8080") log.Println("Server läuft auf http://0.0.0.0:8080")
@@ -366,22 +516,34 @@ func createTable(db *sql.DB) {
endbestand REAL, endbestand REAL,
prozentwert REAL, prozentwert REAL,
abgabe 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
// Falls die Tabelle schon existiert, aber die Spalte "bezahlt" fehlt (z.B. nach Update) // Ergänze ALTER TABLE nur für Migration bestehender Tabellen
_, err = db.Exec(`ALTER TABLE eintraege ADD COLUMN bezahlt INTEGER DEFAULT 0;`) addColumn := func(column, colType string) {
_, err := db.Exec(`ALTER TABLE eintraege ADD COLUMN ` + column + ` ` + colType)
if err != nil && !strings.Contains(err.Error(), "duplicate column") { if err != nil && !strings.Contains(err.Error(), "duplicate column") {
log.Fatal(err) log.Fatalf("Fehler beim Hinzufügen der Spalte %s: %v", column, err)
} }
_, err = db.Exec(`ALTER TABLE eintraege ADD COLUMN created_at TEXT`)
if err != nil && !strings.Contains(err.Error(), "duplicate column") {
log.Fatal(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 = ` const loginForm = `
@@ -419,6 +581,7 @@ const htmlTemplate = `
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Abgabe-Berechnung</title> <title>Abgabe-Berechnung</title>
<link href="/static/css/bootstrap.min.css" rel="stylesheet"> <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"> <link rel="icon" href="/static/favicon.ico" type="image/x-icon">
</head> </head>
<body class="bg-light"> <body class="bg-light">
@@ -471,6 +634,47 @@ const htmlTemplate = `
</select> </select>
</div> </div>
</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> <button type="submit" class="btn btn-primary">Berechnen & Speichern</button>
</form> </form>
{{end}} {{end}}
@@ -488,10 +692,10 @@ const htmlTemplate = `
<th>Prozent</th> <th>Prozent</th>
<th>UEC Abgabe</th> <th>UEC Abgabe</th>
<th>Status</th> <th>Status</th>
{{if .LoggedIn}}<th>Aktion</th>{{end}} {{if .LoggedIn}}<th>Aktion</th>{{else}}<th>Erweitert</th>{{end}}
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="eintragsTabelle">
{{range .Entries}} {{range .Entries}}
<tr> <tr>
<td>{{.ID}}</td> <td>{{.ID}}</td>
@@ -500,9 +704,7 @@ const htmlTemplate = `
<td>{{formatNumber .Endbestand}}</td> <td>{{formatNumber .Endbestand}}</td>
<td>{{formatNumber .Gesamtwert}}</td> <td>{{formatNumber .Gesamtwert}}</td>
<td>{{formatNumber .Prozentwert}}%</td> <td>{{formatNumber .Prozentwert}}%</td>
<td> <td>{{formatNumber .Abgabe}}</td>
{{formatNumber .Abgabe}}
</td>
<td> <td>
{{if .Bezahlt}} {{if .Bezahlt}}
{{if $.LoggedIn}} {{if $.LoggedIn}}
@@ -518,11 +720,43 @@ const htmlTemplate = `
{{end}} {{end}}
{{end}} {{end}}
</td> </td>
{{if $.LoggedIn}}
<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> <a href="/delete?id={{.ID}}" class="btn btn-sm btn-danger" onclick="return confirm('Eintrag wirklich löschen?')">Löschen</a>
</td>
{{end}} {{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> </tr>
{{end}} {{end}}
</tbody> </tbody>
@@ -652,6 +886,36 @@ const htmlTemplate = `
<hr /> <hr />
</div> </div>
<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>
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> <script>
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
const tabKey = "lastActiveTab"; 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