v0.315.0: one app page with four tabs; the guide the catalog can carry; no Raspberry Pi badge
gates / gates (push) Failing after 47s

An app was six pages — details, install/settings, logs, export, the per-app backup panel and a
details card for moving data — and every "Vissza" on them went to the app LIST, so there was no way
back to the app you were looking at. It is now ONE page with four tab URLs under /apps/{slug}/,
one shared header and one set of banners.

  settings  Mukodes (Start/Restart/Stop, Stop confirms) + the cards that want something from the
            household + the install-time and optional settings. The default for an installed app,
            RENDERED at the bare URL and never redirected to.
  data      the data folders, move to another drive, the nightly copy, the 2nd-copy setting, export
  guide     tagline, screenshots, use cases, first steps, the catalog's guide, the FAQ, the docs
            link — and the WHOLE page for an app that is not installed
  logs      the log, and the only tab whose poller runs

The tabs are links, not panels: a link in an alert has to land ON a tab, and the log poller must not
run on the other three. Start/Restart/Stop left the page header, where they read as menu buttons.

The old /stacks/{name}/{deploy,logs,export,backup} GETs answer 302 — never 301, which a browser
caches beyond anyone's reach — to the tab that now holds them, query string carried through, with
name->slug resolved through the stack list rather than assumed equal. Every POST on those paths is
untouched: the backup form, the app-email toggle and the reveal endpoints keep their URLs.

A value asked AFTER the app starts is now generic (D2). The catalog marks a field `after_start: true`
and supplies its label; the ACTION is Go, in a registry keyed by slug + env var. The catalog can
never declare what runs — a declared field with no registered action renders nothing, POSTs 404 and
logs one WARN with the env var KEY, never the value. A red-proof found the second half of that guard
unpinned: deleting the "the catalog must have DECLARED this field" check left the suite green, and a
POST for an undeclared field then reached Plex and opened the setup gate.
TestAfterStart_RegistryAloneIsNotAnInvitation pins it.

New catalog block app_info.guide (sections + faq + checked_version), parsed, localised field by
field and rendered with the household's own address filled in. It renders only when checked_version
names the image tag its author clicked through. No template carries one yet.

The Pi and "csak x86" badges are gone; PiCompatible and the catalog key stay this release, or a box
on 0.314.0 would read "csak x86" on every app (R-1067).

template_id_gate.py now follows {{template}} includes, with a scope decoy proving the walk did not
become vacuous.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
This commit is contained in:
2026-10-15 18:37:50 +02:00
parent 85f65c70d8
commit e66d4669d6
120 changed files with 16239 additions and 9866 deletions
+59 -7
View File
@@ -3,10 +3,20 @@
For every template under internal/web/templates (the setup wizard templates are deleted, R-554), extract each
getElementById('X') / querySelector('#X') literal used by the file's inline JS and assert an
id="X" exists in the SAME file — or that the ID is created by that file's own JS (innerHTML /
id="X" exists in the RENDERED PAGE — or that the ID is created by that file's own JS (innerHTML /
insertAdjacentHTML string containing id="X" / id='X'), or is explicitly allowlisted below with
a justification.
THE RENDERED PAGE, NOT THE FILE (v0.315.0). A page that does {{template "x" .}} renders the ids of
whatever defines "x", so the gate follows those includes transitively and unions the ids in. Until
the app page split its form into a partial shared by the install wizard and the settings tab, every
page was one file and the two readings were the same; a file-scoped check would now convict markup
that is demonstrably on the page, which is the fastest way to teach people to route around a gate.
Following the includes also makes the gate STRICTER in the direction that matters: an id referenced
by a page whose partial no longer defines it is still a failure, and a partial that references an id
nothing in its own chain defines is caught where before it was simply a separate file.
Exit 1 on any unresolved reference. Run from the repo's controller/ directory:
python scripts/template_id_gate.py
"""
@@ -40,14 +50,53 @@ ID_ATTR_RE = re.compile(r"""id=["']([A-Za-z0-9_{}\. $-]+)["']""")
ID_IN_JS_RE = re.compile(r"""id=\\?["']([A-Za-z0-9_-]+)\\?["']""")
def check(path):
DEFINE_RE = re.compile(r"""\{\{-?\s*define\s+["']([^"']+)["']""")
INCLUDE_RE = re.compile(r"""\{\{-?\s*template\s+["']([^"']+)["']""")
def _ids_in(src):
return set(ID_ATTR_RE.findall(src)) | set(ID_IN_JS_RE.findall(src))
def _index(paths):
"""Return (src_by_path, file_of_definition): which file defines each {{define}} name."""
src_by_path, file_of = {}, {}
for p in paths:
src = io.open(p, encoding="utf-8").read()
src_by_path[p] = src
for name in DEFINE_RE.findall(src):
file_of[name] = p
return src_by_path, file_of
def _reachable_ids(path, src_by_path, file_of):
"""Every id this page renders: its own, plus those of every template it includes, transitively.
A missing definition is NOT an error here — layout_start and friends are defined in files this
walk reaches anyway, and a genuinely undefined template name is html/template's error to raise
at parse time, which the render tests already catch.
"""
seen_files, queue, ids = {path}, [path], set()
while queue:
p = queue.pop()
src = src_by_path.get(p, "")
ids |= _ids_in(src)
for name in INCLUDE_RE.findall(src):
dep = file_of.get(name)
if dep and dep not in seen_files:
seen_files.add(dep)
queue.append(dep)
return ids
def check(path, src_by_path, file_of):
fname = os.path.basename(path)
src = io.open(path, encoding="utf-8").read()
src = src_by_path[path]
static_refs = set(GET_RE.findall(src)) | set(QS_RE.findall(src))
dyn_prefixes = set(GET_DYN_RE.findall(src))
# static refs regex also matches the dynamic form's literal — subtract prefixes used with '+'
static_refs -= dyn_prefixes
defined = set(ID_ATTR_RE.findall(src)) | set(ID_IN_JS_RE.findall(src))
defined = _reachable_ids(path, src_by_path, file_of)
defined_prefixes = tuple(d.split("{{")[0] for d in defined if "{{" in d or d.endswith("-"))
problems = []
@@ -85,15 +134,18 @@ def _html_at_any_depth(root):
def main():
bad = []
paths = []
for root in ROOTS:
for _p in _html_at_any_depth(root):
bad += check(_p)
paths += _html_at_any_depth(root)
src_by_path, file_of = _index(paths)
for _p in paths:
bad += check(_p, src_by_path, file_of)
if bad:
print("INTEGRITY GATE FAILED (%d):" % len(bad))
for b in bad:
print(" -", b)
sys.exit(1)
print("integrity gate OK — every JS element-ID reference resolves within its own template")
print("integrity gate OK — every JS element-ID reference resolves in the page it renders on")
if __name__ == "__main__":