# -*- coding: utf-8 -*- """D1 §10 JS element-ID integrity gate. For every template under internal/web/templates and internal/setup/templates, 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"), os.path.join("internal", "setup", "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 def main(): bad = [] for root in ROOTS: for fn in sorted(os.listdir(root)): if not fn.endswith(".html"): continue bad += check(os.path.join(root, fn)) 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()