Fix crash when desktop config JSON is deleted: self-heal from board

server.py and desktop_viewer.py read-only mode required the desktop
config JSONs to exist and crashed/showed "Config-Datei fehlt" if one
was deleted, even though the board already holds the config durably
in NVM. Add versapad_combined.fetch_from_board()/load_or_fetch(): a
missing combined JSON is now transparently rebuilt from the board via
serial and cached back to disk, falling back to the legacy per-profile
JSONs (or a clear error) only when the board is unreachable.
This commit is contained in:
cjjohn 2026-08-08 17:06:53 +02:00
parent 8a2a73c68d
commit 540ce5b1eb
4 changed files with 104 additions and 10 deletions

View file

@ -1,7 +1,10 @@
"""
VersaPad Viewer -- Browser-Variante.
Liest bei jedem Request live die 3 Config-JSONs vom Desktop und rendert
die Steuermatrix (4x5 Grid + 4 Encoder) je Profil als HTML.
Liest bei jedem Request die kombinierte Config-JSON vom Desktop und
rendert die Steuermatrix (4x5 Grid + 4 Encoder) je Profil als HTML. Fehlt
die JSON (z.B. geloescht), wird sie automatisch per Serial vom Board neu
aufgebaut und als neuer Cache gespeichert -- das Board ist die eigentliche
Quelle der Wahrheit, siehe versapad_combined.load_or_fetch().
Kein Build-Schritt, kein externes Framework -- nur stdlib.
Start: python server.py [--port 8765]
@ -12,6 +15,7 @@ import webbrowser
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse, parse_qs
import versapad_combined as vcomb
import versapad_data as vp
PAGE_CSS = """
@ -87,7 +91,12 @@ def render_encoder(enc):
def render_page(profile):
cfg = vp.load_profile(profile)
combined = vcomb.load_or_fetch()
raw = combined["profiles"][profile]
cfg = vp.annotate_profile({
"buttons": [dict(b) for b in raw["buttons"]],
"encoders": [dict(e) for e in raw["encoders"]],
})
tabs = "".join(
f'<a class="tab {"active" if p == profile else ""}" href="/?profile={p}">{html.escape(vp.PROFILE_NAMES[p])}</a>'
for p in sorted(vp.PROFILE_NAMES)
@ -107,7 +116,7 @@ def render_page(profile):
<div class="grid">{cells}</div>
<h2>Encoder</h2>
<div class="encoders">{encoders}</div>
<footer>Quelle: {html.escape(vp.CONFIG_PATHS[profile])}</footer>
<footer>Quelle: {html.escape(vcomb.DEFAULT_PATH)}</footer>
</body></html>"""
@ -130,10 +139,10 @@ class Handler(BaseHTTPRequestHandler):
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
except FileNotFoundError as e:
except (FileNotFoundError, RuntimeError) as e:
self.send_response(500)
self.end_headers()
self.wfile.write(f"Config-Datei fehlt: {e}".encode("utf-8"))
self.wfile.write(f"Config nicht verfuegbar: {e}".encode("utf-8"))
def main():