Files
Kaj SchittecatandClaude Opus 4.8 d787d77365 docs: web control panel + Remote/VNC guide, refreshed device shots
- New "Remote & web app" docs section covering the browser control panel
  (Chats / Contacts / Discovered / Settings / Terminal), Screen mirror (VNC)
  and Remote UI, with a 7-shot phone gallery of the real web app.
- Refreshed all device screenshots via the DOC_CAPTURE tour (now also
  captures the Remote screen) and added the MQTT bridge settings page.
- New automated web-capture harness (scripts/doc/web-demo/): extracts the
  real page from firmware, serves it with mock data, screenshots via Playwright.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 17:18:20 +02:00

127 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""
Extract the wadamesh web control-panel SPA from the firmware C source.
The page is the C string literal `WS_HTML_TERMINAL_PAGE[]` in
src/helpers/esp32/WebSocketCompanionServer.cpp
built from many adjacent "..." fragments. This re-joins the fragments,
un-escapes the C escapes, strips the leading HTTP/1.1 response headers, and
writes a standalone scripts/doc/web-demo/index.html.
Re-run whenever the page changes:
python3 scripts/doc/web-demo/extract.py
"""
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.abspath(os.path.join(HERE, "..", "..", ".."))
SRC = os.path.join(REPO, "src", "helpers", "esp32", "WebSocketCompanionServer.cpp")
OUT = os.path.join(HERE, "index.html")
DECL = "static const char WS_HTML_TERMINAL_PAGE[] ="
# C escape sequences that appear in the literal. Order matters: handle the
# backslash-escapes we know about; leave any others (\uXXXX etc. don't occur here)
# untouched. We decode via a single regex pass so a literal "\\n" (escaped
# backslash then n) is NOT turned into a newline.
_C_ESCAPES = {
"\\n": "\n",
"\\r": "\r",
"\\t": "\t",
'\\"': '"',
"\\\\": "\\",
"\\'": "'",
"\\a": "\a",
"\\b": "\b",
"\\f": "\f",
"\\v": "\v",
"\\0": "\0",
}
_ESC_RE = re.compile(r"\\(?:[abfnrtv0'\"\\]|x[0-9A-Fa-f]{2})")
def _unescape(s):
def repl(m):
tok = m.group(0)
if tok.startswith("\\x"):
return chr(int(tok[2:], 16))
return _C_ESCAPES.get(tok, tok)
return _ESC_RE.sub(repl, s)
def _extract_string_fragments(block):
"""Yield the decoded content of every "..." fragment in `block`, in order.
Parses char-by-char so escaped quotes (\") inside a fragment don't end it,
and text between fragments (whitespace / newlines in the C source) is ignored.
"""
out = []
i, n = 0, len(block)
in_str = False
frag = []
while i < n:
ch = block[i]
if not in_str:
if ch == '"':
in_str = True
frag = []
elif ch == ";":
break # end of the C statement
i += 1
continue
# inside a string literal
if ch == "\\" and i + 1 < n:
frag.append(block[i:i + 2])
i += 2
continue
if ch == '"':
out.append(_unescape("".join(frag)))
in_str = False
i += 1
continue
frag.append(ch)
i += 1
return "".join(out)
def extract(src_path=SRC):
with open(src_path, "r", encoding="utf-8") as f:
text = f.read()
start = text.find(DECL)
if start < 0:
raise SystemExit("could not find %s in %s" % (DECL, src_path))
start += len(DECL)
# The statement ends at the first ';' that terminates the literal. Because
# fragments can contain ';' we cannot naively split; instead let the fragment
# parser stop at the first ';' found OUTSIDE a string.
joined = _extract_string_fragments(text[start:])
# Strip the HTTP response head: everything up to and including the blank line
# (\r\n\r\n) that precedes the HTML body.
marker = "\r\n\r\n"
idx = joined.find(marker)
if idx >= 0 and joined[:idx].startswith("HTTP/"):
joined = joined[idx + len(marker):]
else:
# fall back: chop to the doctype if the header shape ever changes
d = joined.find("<!DOCTYPE")
if d > 0:
joined = joined[d:]
return joined
def main():
html = extract()
with open(OUT, "w", encoding="utf-8") as f:
f.write(html)
print("wrote %s (%d bytes)" % (OUT, len(html)))
# sanity: the reconstructed page must have the key anchors
for needle in ("<!DOCTYPE html>", "WADAMESH", "/term", "id=w", "showTab"):
if needle not in html:
print(" WARNING: expected %r not found in output" % needle, file=sys.stderr)
if __name__ == "__main__":
main()