localization

This commit is contained in:
2026-08-16 14:33:27 +02:00
parent 9c634b819e
commit fb09b2b7a1
65 changed files with 1621 additions and 1679 deletions

View File

@@ -19,9 +19,9 @@ namespace {
WebServer server(80);
// Nur reine Dateinamen/Labels aus Formularfeldern akzeptieren - kein
// "/" und kein ".." - damit ueber die WebUI kein Ausbruch aus dem
// jeweiligen SD-Verzeichnis moeglich ist (Pfad-Traversal).
// Only accept plain filenames/labels from form fields - no
// "/" and no ".." - so that no escape from the respective
// SD directory is possible via the WebUI (path traversal).
bool isSafeName(const String& name) {
if (name.length() == 0 || name.length() > 40) return false;
if (name.indexOf('/') >= 0 || name.indexOf('\\') >= 0) return false;
@@ -29,14 +29,14 @@ namespace {
return true;
}
// Absichtliche Duplikate der gleichnamigen (lokalen/statischen) Funktionen
// aus radar_screen.cpp - dort nicht exportiert, und nach der im Projekt
// etablierten Konvention "jeder Screen/jedes Modul dupliziert seine
// eigenen kleinen Helfer statt eines gemeinsamen Moduls" (siehe z.B.
// timeout_screen.cpp) bewusst hier erneut definiert statt radar_screen.cpp
// umzubauen. Bei Aenderungen an der Logik in radar_screen.cpp bitte diese
// Kopie hier synchron halten, damit das WebUI-Radar dieselben Ringe/
// Markierungen zeigt wie das Geraete-Display.
// Intentional duplicates of the same-named (local/static) functions
// from radar_screen.cpp - not exported there, and following the project's
// established convention "each screen/module duplicates its own small
// helpers instead of a shared module" (see e.g. timeout_screen.cpp),
// deliberately redefined here instead of refactoring radar_screen.cpp.
// If the logic in radar_screen.cpp changes, please keep this copy
// in sync so that the WebUI radar shows the same rings/markers as the
// device display.
bool isEmergencySquawkWeb(const char* squawk) {
if (!squawk[0]) return false;
for (uint8_t i = 0; i < Config::EMERGENCY_SQUAWK_COUNT; i++) {
@@ -45,15 +45,15 @@ namespace {
return false;
}
// "Auffaellig" (oranger Ring) beschraenkt sich aktuell auf gar nichts -
// der Teil ueber Militaer-/Regierungs-Rufzeichen-Praefixe
// (Config::NOTABLE_CALLSIGN_PREFIXES) ist NICHT umgesetzt, weil diese
// Konstante nirgends im Projekt existiert (auch nicht in
// radar_screen.cpp) - das zugehoerige Feature wurde bisher nie
// tatsaechlich spezifiziert/implementiert. "notable" wird deshalb unten
// in handleRadarJson() fest auf false gesetzt. Sobald es eine echte
// Praefixliste gibt, hier eine isNotableCallsignWeb()-Funktion analog zu
// isEmergencySquawkWeb() ergaenzen.
// "Notable" (orange ring) is currently limited to nothing at all -
// the part about military/government callsign prefixes
// (Config::NOTABLE_CALLSIGN_PREFIXES) is NOT implemented, because this
// constant does not exist anywhere in the project (not even in
// radar_screen.cpp) - the associated feature has never actually been
// specified/implemented. "notable" is therefore hardcoded to false
// below in handleRadarJson(). As soon as there is a real prefix list,
// add an isNotableCallsignWeb() function analogous to
// isEmergencySquawkWeb() here.
bool isHeavyCategoryWeb(const char* category) {
return category[0] == 'A' && category[1] == '5';
}
@@ -73,10 +73,9 @@ namespace {
html += "button{background:#0a0f0d;color:#ff3b3b;border:1px solid #ff3b3b;border-radius:4px;padding:3px 10px;font-family:inherit;cursor:pointer;}";
html += "button:hover{background:#ff3b3b;color:#0a0f0d;}";
html += ".dl{color:#39ff14;text-decoration:none;border:1px solid #39ff14;border-radius:4px;padding:3px 10px;margin-right:6px;display:inline-block;}";
// Gruen statt Rot fuer "Hinzufuegen"-Buttons - die roten button{}-Regeln
// oben bleiben fuer alle destruktiven "Entfernen/Loeschen"-Buttons
// unveraendert, .addbtn ist ausschliesslich fuer die neuen Listen-
// Formulare (siehe handleLists()) gedacht.
// Green instead of red for "Add" buttons - the red button{} rules
// above stay unchanged for all destructive "Remove/Delete" buttons.
// .addbtn is exclusively for the new list forms (see handleLists()).
html += ".addbtn{background:#0a0f0d;color:#39ff14;border:1px solid #39ff14;border-radius:4px;padding:3px 10px;font-family:inherit;cursor:pointer;}";
html += ".addbtn:hover{background:#39ff14;color:#0a0f0d;}";
html += "input[type=text]{background:#0a0f0d;color:#39ff14;border:1px solid #39ff14;border-radius:4px;padding:5px 8px;font-family:inherit;}";
@@ -92,19 +91,19 @@ namespace {
return html;
}
// Live-Radar-Ansicht fuer die Startseite: ein <canvas>, das per JavaScript
// alle paar Sekunden /radar.json abruft und die Flugzeuge polar (Peilung/
// Distanz, genau wie auf dem Geraete-Display) zeichnet. Bewusst per
// fetch()-Polling statt WebSocket/SSE gehalten - deutlich weniger Code
// und Speicherbedarf auf dem ESP32, und fuer eine gelegentlich vom Handy
// aus aufgerufene Seite voellig ausreichend.
// Live radar view for the start page: a <canvas> that uses JavaScript
// to fetch /radar.json every few seconds and draw the aircraft polar
// (bearing/distance, exactly like on the device display). Intentionally
// kept with fetch()-polling instead of WebSocket/SSE - significantly less
// code and memory usage on the ESP32, and perfectly adequate for a page
// occasionally opened from a phone.
//
// Der Reichweiten-Waehler (<select>) ist rein clientseitig/pro Seiten-
// aufruf - er aendert NICHT die Geraete-Einstellung (SettingsStore::
// rangeIndex()), sondern wird als "range_km"-Query-Parameter an
// /radar.json mitgeschickt (siehe handleRadarJson()) und erlaubt so ein
// unabhaengiges Herein-/Herauszoomen auf dem Handy, ohne das Geraete-
// Display zu beeinflussen. Default ist die aktuelle Geraete-Reichweite.
// The range selector (<select>) is purely client-side/per page view -
// it does NOT change the device setting (SettingsStore::
// rangeIndex()), but is sent as a "range_km" query parameter to
// /radar.json (see handleRadarJson()) and allows independent zooming
// in/out on the phone without affecting the device display. Default is
// the current device range.
void appendRadarSection(String& html) {
float deviceRangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()];
@@ -131,10 +130,10 @@ namespace {
html += "var markers=[];";
html += "var selectedHex=null;";
// Hintergrund-Sterne AUSSERHALB des Radarkreises - gleiches Prinzip
// wie updateBgStars()/initBgStarsIfNeeded() in radar_screen.cpp
// (Rejection-Sampling, damit kein Stern innerhalb des Kreises
// landet), hier per Canvas/JS statt TFT_eSPI nachgebaut.
// Background stars OUTSIDE the radar circle - same principle
// as updateBgStars()/initBgStarsIfNeeded() in radar_screen.cpp
// (rejection sampling, so no star lands inside the circle),
// replicated here via Canvas/JS instead of TFT_eSPI.
html += "var stars=[];";
html += "(function(){var minDistSq=(R+6)*(R+6);for(var i=0;i<24;i++){var x,y,tries=0;";
html += "do{x=4+Math.random()*(W-8);y=4+Math.random()*(H-8);tries++;}";
@@ -155,9 +154,9 @@ namespace {
html += "for(var ring=1;ring<=3;ring++){var r=R*ring/3;ctx.beginPath();ctx.arc(cx,cy,r,0,Math.PI*2);ctx.stroke();";
html += "ctx.fillText(Math.round(data.range_km*ring/3)+' km',cx+4,cy-r+10);}";
html += "ctx.strokeStyle='#12261a';ctx.beginPath();ctx.moveTo(cx-R,cy);ctx.lineTo(cx+R,cy);ctx.moveTo(cx,cy-R);ctx.lineTo(cx,cy+R);ctx.stroke();";
// Alle vier Himmelsrichtungen (N/E/S/W), genau wie
// drawStaticBackground() in radar_screen.cpp - vorher stand hier nur
// "N", was auf Nachfrage ergaenzt wurde.
// All four cardinal directions (N/E/S/W), exactly like
// drawStaticBackground() in radar_screen.cpp - previously only
// "N" was shown here, which was expanded upon request.
html += "ctx.fillStyle='#39ff14';ctx.textAlign='center';ctx.fillText('N',cx,cy-R-8);";
html += "ctx.fillText('S',cx,cy+R+16);";
html += "ctx.textAlign='left';ctx.fillText('E',cx+R+4,cy+3);";
@@ -180,26 +179,26 @@ namespace {
html += "if(a.emergency){ctx.strokeStyle='#ff3b3b';ctx.beginPath();ctx.arc(x,y,9,0,Math.PI*2);ctx.stroke();}";
html += "else if(a.watched){ctx.strokeStyle='#00e5ff';ctx.beginPath();ctx.arc(x,y,9,0,Math.PI*2);ctx.stroke();}";
html += "else if(a.notable){ctx.strokeStyle='#ff9f1a';ctx.beginPath();ctx.arc(x,y,9,0,Math.PI*2);ctx.stroke();}";
// Ausgewaehltes Flugzeug (per Klick/Tap, siehe unten) bekommt einen
// weissen Auswahlring, gleiches Prinzip wie isSelected auf dem
// Geraete-Display (radar_screen.cpp render()).
// Selected aircraft (by click/tap, see below) gets a
// white selection ring, same principle as isSelected on the
// device display (radar_screen.cpp render()).
html += "if(a.hex===selectedHex){ctx.strokeStyle='#ffffff';ctx.beginPath();ctx.arc(x,y,11,0,Math.PI*2);ctx.stroke();}";
html += "ctx.fillStyle=color;ctx.textAlign='center';ctx.fillText(a.callsign,x,y-8);";
html += "markers.push({x:x,y:y,a:a});";
html += "});";
html += "status.textContent=(data.aircraft||[]).length+' aircraft \\u00b7 range '+data.range_km+' km';";
// Falls das ausgewaehlte Flugzeug in dieser Aktualisierung nicht
// mehr vorkommt (z.B. aus der Reichweite geflogen), Infobox wieder
// schliessen statt veraltete Daten stehen zu lassen.
// If the selected aircraft no longer appears in this update
// (e.g. flew out of range), close the info box instead of
// showing stale data.
html += "if(selectedHex){var found=markers.filter(function(m){return m.a.hex===selectedHex;})[0];";
html += "if(found){showInfo(found.a);}else{selectedHex=null;hideInfo();}}";
html += "}";
// Info-Panel fuer ein angetipptes Flugzeug - bewusst eine eigene,
// stehenbleibende Box (kein Tooltip/Popup, das beim naechsten
// Neuzeichnen einfach verschwindet), mit explizitem Schliessen-Link,
// gleiches Grundprinzip wie infoScreen() am Geraet: der Nutzer soll
// aktiv entscheiden, wann die Info wieder verschwindet.
// Info panel for a tapped aircraft - intentionally a dedicated,
// persistent box (not a tooltip/popup that disappears on the next
// redraw), with an explicit close link, same basic principle as
// infoScreen() on the device: the user should actively decide when
// the info disappears.
html += "function fmtNum(n,d){return (typeof n==='number')?n.toFixed(d):'?';}";
html += "function showInfo(a){";
html += "var lines=[];";
@@ -233,11 +232,11 @@ namespace {
html += "}";
html += "poll();";
html += "setInterval(poll,3000);";
// Schnellerer, rein lokaler Redraw-Takt (alle 150ms, ohne Netzwerk-
// Anfrage) nur fuer das Sternenfunkeln + die Auswahlmarkierung -
// gleiches Grundprinzip wie tick() vs. render() auf dem
// Geraete-Display: Flugzeugpositionen aktualisieren sich weiterhin
// nur alle 3s per poll(), die Sterne twinkeln aber fluessig dazwischen.
// Faster, purely local redraw interval (every 150ms, without network
// request) only for star twinkling + the selection marker -
// same basic principle as tick() vs. render() on the
// device display: aircraft positions still update
// only every 3s via poll(), but stars twinkle smoothly in between.
html += "setInterval(function(){draw(lastData);},150);";
html += "})();</script>";
}
@@ -297,12 +296,12 @@ namespace {
if (line.length() == 0) continue;
server.sendContent(String(days[i].date) + "," + line + "\n");
// Wichtig: Ohne regelmaessiges Abgeben der CPU haengt dieser
// Task (Core 0, Prioritaet 1) die Idle-Task aus, die den
// Task-Watchdog fuettert. Bei groesseren Logbuechern fuehrt
// das nach ca. 5s ohne Yield zu einem Watchdog-Reset -
// genau der Reboot mitten im CSV-Download, den der Nutzer
// beobachtet hat. delay(1) erzwingt einen Kontextwechsel.
// Important: Without regularly yielding the CPU, this
// task (Core 0, priority 1) starves the idle task that feeds
// the task watchdog. With larger logbooks, this causes a
// watchdog reset after approx. 5s without yield -
// exactly the reboot during CSV download that the user
// observed. delay(1) forces a context switch.
if (++linesSent % 10 == 0) {
delay(1);
}
@@ -348,14 +347,14 @@ namespace {
server.send(303);
}
// Verwaltung von Airline-Filter und Beobachtungsliste per Browser -
// beide Backend-Module (AirlineFilter/AircraftWatchlist) sind seit
// dieser Erweiterung mutex-geschuetzt (siehe dort), da sie jetzt sowohl
// von Core 1 (Radar-/Menue-Screens) als auch von hier aus - Core 0,
// WebExportServer laeuft innerhalb von NetTask - aufgerufen werden.
// Praktisch vor allem fuer laengere Eingaben (Rufzeichen, ICAO-Codes),
// die sich per Handy-Tastatur deutlich bequemer eintippen lassen als
// ueber die kleine Bildschirmtastatur des Geraets.
// Management of airline filter and watchlist via browser -
// both backend modules (AirlineFilter/AircraftWatchlist) are now
// mutex-protected (see there), since they are called both
// from Core 1 (radar/menu screens) and from here - Core 0,
// WebExportServer runs within NetTask.
// Practically most useful for longer inputs (callsigns, ICAO codes)
// that are much easier to type on a phone keyboard than
// via the device's small on-screen keyboard.
void handleLists() {
String html = htmlHeader("Leos Flightradar - Lists");
@@ -449,19 +448,18 @@ namespace {
server.send(303);
}
// Datenquelle fuer das Live-Radar auf der Startseite (siehe
// appendRadarSection()). Wendet dieselben Filter/Prioritaeten an wie das
// Geraete-Display (render() in radar_screen.cpp): Reichweite, "Boden-
// fahrzeuge ausblenden", Airline-Filter, sowie Notfall/Beobachtungsliste/
// "auffaellig" als sich gegenseitig ausschliessende Ring-Markierungen in
// genau dieser Prioritaet.
// Data source for the live radar on the start page (see
// appendRadarSection()). Applies the same filters/priorities as the
// device display (render() in radar_screen.cpp): range, "hide ground
// vehicles", airline filter, and emergency/watchlist/"notable" as
// mutually exclusive ring markers in exactly this priority.
void handleRadarJson() {
float rangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()];
// Erlaubt der Web-Ansicht ein eigenes, unabhaengiges Zoomen (siehe
// Reichweiten-Waehler in appendRadarSection()), OHNE die Geraete-
// Einstellung zu veraendern. Nur einen der bekannten
// Config::RANGE_STEPS_KM-Werte akzeptieren - ein fehlender oder
// nicht erkannter Parameter faellt auf die Geraete-Reichweite zurueck.
// Allows the web view its own independent zooming (see
// range selector in appendRadarSection()), WITHOUT changing the
// device setting. Only accept one of the known
// Config::RANGE_STEPS_KM values - a missing or
// unknown parameter falls back to the device range.
if (server.hasArg("range_km")) {
float requested = server.arg("range_km").toFloat();
for (uint8_t i = 0; i < Config::RANGE_STEP_COUNT; i++) {
@@ -494,13 +492,13 @@ namespace {
bool isHeavy = isHeavyCategoryWeb(a.category);
bool isEmergency = emergencyOn && isEmergencySquawkWeb(a.squawk);
bool isWatched = watchOn && AircraftWatchlist::isWatched(a.callsign);
// "notable" (oranger Ring) ist fuer auffaellige Rufzeichen
// (Militaer/Regierung) reserviert - Heavy-Flugzeuge bekommen
// stattdessen die eigene Markerform (siehe "heavy" oben). Es
// gibt aktuell aber keine Militaer-/Regierungs-Praefixliste im
// Projekt (auch nicht am Geraete-Display, siehe
// radar_screen.cpp) - "notable" bleibt daher bis auf Weiteres
// immer false.
// "notable" (orange ring) is reserved for conspicuous callsigns
// (military/government) - heavy aircraft instead get their
// own marker shape (see "heavy" above). However, there is
// currently no military/government prefix list in the
// project (not even on the device display, see
// radar_screen.cpp) - "notable" therefore remains
// always false for now.
bool isNotable = false;
JsonObject o = arr.add<JsonObject>();
@@ -516,10 +514,10 @@ namespace {
o["emergency"] = isEmergency;
o["watched"] = isWatched;
o["notable"] = isNotable;
// Zusaetzliche Felder nur fuer das Info-Panel bei Klick/Tap auf
// ein Flugzeug (siehe showInfo() in appendRadarSection()) - beide
// stehen bereits verlustfrei im Aircraft-Snapshot, kein
// zusaetzlicher Netzwerk-/SD-Zugriff noetig.
// Additional fields only for the info panel on click/tap on
// an aircraft (see showInfo() in appendRadarSection()) - both
// are already losslessly in the Aircraft snapshot, no
// additional network/SD access needed.
o["speed_kt"] = a.groundSpeedKt;
o["squawk"] = a.squawk;
}