D1 Part 2: settings.html split into four pages + sidebar restructure

- settings.html (1451 lines) deleted; sections moved verbatim into
  settings_system.html (Rendszer konfiguráció, Verzió és frissítés,
  Vezérlő/Kiszolgáló újraindítása + update/restart JS),
  settings_notifications.html (Értesítések, Alkalmazás-email),
  settings_security.html (Jelszó módosítás, Földrajzi korlátozás + geo
  JS, Vészhelyzeti információk — heading + section copy accents fixed),
  storage.html (Adattárolók, NAS, migrate progress, agent view + all
  storage JS; wizard entry links now /storage/init|attach with sprite
  icons instead of emoji). The NAS + migrate sections were nested inside
  {{if .StoragePaths}} in the monolith and vanished with zero drives —
  now unconditional on /storage.
- layout.html: Tárhely main-nav item (hard-drive icon) + the
  'Beállítások' sidebar group with Rendszer / Értesítések / Biztonság és
  hozzáférés sub-links (active-state per page key); orphaned
  .sidebar-settings-link CSS deleted (grep-zero), .nav-group-label /
  .nav-links-sub added.
- Handlers wired to their own builders + templates; the legacy
  settingsData() merge deleted.
- scripts/template_id_gate.py: the §10 JS element-ID integrity gate
  (getElementById/querySelector('#…') must resolve in the SAME template;
  JS-created + template-parameterized IDs handled; layout modal IDs
  allowlisted). Red-proven: a storage function planted in the
  notifications template failed the gate with 'static #migrate-progress
  not defined'.
- Tests: per-page section markers + cross-leak assertions, h3 section
  inventory (all 11 old headings accounted for; typo rename asserted).
This commit is contained in:
2026-07-02 19:07:32 +02:00
parent d50a919404
commit f8e18a9ec9
9 changed files with 1062 additions and 890 deletions
+91
View File
@@ -0,0 +1,91 @@
# -*- 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()