Files
felhom-controller/controller/scripts/template_id_gate.py
T
admin faef8f8a91 v0.308.0 (R-554, held-back-able): recovery-info.txt points at the dashboard's Restore page and the recovery code; the first-boot setup wizard is deleted
Operator ruling 2026-10-12 (S27, 09 §3 decision 222). The operator approves the Hungarian text before
release. A box whose first ingestion failed now serves one bilingual waiting page and retries every
minute instead of the wizard. Own commit so the lead can hold it back.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-12 15:38:28 +02:00

101 lines
4.5 KiB
Python

# -*- coding: utf-8 -*-
"""D1 §10 JS element-ID integrity gate.
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 /
insertAdjacentHTML string containing id="X" / id='X'), or is explicitly allowlisted below with
a justification.
Exit 1 on any unresolved reference. Run from the repo's controller/ directory:
python scripts/template_id_gate.py
"""
import io, os, re, sys
ROOTS = [
os.path.join("internal", "web", "templates"),
]
# Dynamic-ID exceptions: (template, id-prefix-or-name) -> justification.
# Suffix-parameterized IDs (id + variable) are handled generically below; these are the rest.
ALLOW = {
# layout.html builds the alert/delete/remove modals entirely in JS and later looks them up.
("layout.html", "alert-modal"): "created by showAlert() via innerHTML in the same file",
("layout.html", "delete-modal"): "created by deleteOrphanStack() via innerHTML",
("layout.html", "remove-modal"): "created by removeStack() via innerHTML",
("layout.html", "confirm-delete-btn"): "created inside the delete-modal innerHTML",
("layout.html", "confirm-remove-btn"): "created inside the remove-modal innerHTML",
("layout.html", "delete-hdd-check"): "created inside the delete-modal innerHTML",
("layout.html", "remove-hdd-check"): "created inside the remove-modal innerHTML",
("layout.html", "remove-backup-check"): "created inside the remove-modal innerHTML",
("layout.html", "remove-hdd-keep-warning"): "created inside the remove-modal innerHTML",
("layout.html", "sync-btn"): "lives on stacks.html; syncTemplates() is shared layout JS guarded by if(!btn)return",
("layout.html", "sync-toast"): "lives on stacks.html; guarded null-check",
}
GET_RE = re.compile(r"getElementById\(\s*['\"]([A-Za-z0-9_-]+)['\"]\s*\)")
GET_DYN_RE = re.compile(r"getElementById\(\s*['\"]([A-Za-z0-9_-]+)['\"]\s*\+")
QS_RE = re.compile(r"querySelector\(\s*['\"]#([A-Za-z0-9_-]+)['\"]\s*\)")
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):
fname = os.path.basename(path)
src = io.open(path, encoding="utf-8").read()
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_prefixes = tuple(d.split("{{")[0] for d in defined if "{{" in d or d.endswith("-"))
problems = []
for ref in sorted(static_refs):
if ref in defined:
continue
# a template-parameterized id like id="field-{{.EnvVar}}" legitimately renders
# ids such as field-SUBDOMAIN — match static refs against those prefixes
if defined_prefixes and ref.startswith(defined_prefixes):
continue
if (fname, ref) in ALLOW:
continue
problems.append("static #%s not defined in %s" % (ref, fname))
for pref in sorted(dyn_prefixes):
# a dynamic lookup 'x-' + var needs SOME id starting with that prefix (template- or JS-created)
if any(d.startswith(pref) for d in defined) or pref in defined_prefixes:
continue
if (fname, pref) in ALLOW:
continue
problems.append("dynamic prefix #%s* not defined in %s" % (pref, fname))
return problems
# R-421 (2026-09-01): any depth, was os.listdir (one level). No template subdirectory exists
# today, so this was green and correct — and would have stayed green the moment anyone added
# templates/partials/. Measured: a planted file there passed every listdir-based gate.
def _html_at_any_depth(root):
out = []
for dirpath, _dirs, names in os.walk(root):
for fn in sorted(names):
if fn.endswith('.html'):
out.append(os.path.join(dirpath, fn))
return sorted(out)
def main():
bad = []
for root in ROOTS:
for _p in _html_at_any_depth(root):
bad += check(_p)
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")
if __name__ == "__main__":
main()