# -*- coding: utf-8 -*- """v0.126.0 shared app-row dedup gate — the app-list row markup exists ONCE. The canonical row (icon + name left, action block right) is defined in templates/app_row.html (app_list_row / app_list_row_end). Every list surface renders THROUGH it; hand-rolled copies are the defect this gate extinguishes (the pre-0.126.0 state: three visually diverging row structures across four surfaces). Asserts: 1. The row-opening markup (`class="app-row"...`) appears in app_row.html ONLY. 2. The row-icon markup (`app-row-icon`) appears only in app_row.html plus the ONE allowlisted aligned copy: the backups_apps.html expander header (it owns the expand/collapse toggle, so it is aligned to the grammar, not rendered through the partial). 3. The old duplicated structures are gone from the converted surfaces: storage-path-item rows on the backups pages, stack-card rows on the dashboard. 4. Each converted surface actually references the partial. Run from controller/: python scripts/app_row_dedup_gate.py Exit 1 on any violation. """ import io, os, re, sys TPL = os.path.join("internal", "web", "templates") # (file, forbidden-pattern, why) FORBIDDEN = [ ("backups_remote.html", r"storage-path-item", "toggle list must render through app_list_row"), ("backups_remote.html", r"storage-path-header", "old row structure"), ("backups_restore.html", r"storage-path-item", ".fab + restore-to-verify lists must render through app_list_row"), ("backups_restore.html", r"storage-path-header", "old row structure"), ("dashboard.html", r"stack-card", "dashboard rows must render through app_list_row"), ("dashboard.html", r"stack-info", "old row structure"), ("dashboard.html", r'stack-logo"', "old row logo (stack-logo-lg on stacks.html is the card, not a list row)"), ] # files that MUST reference the shared partial MUST_USE = ["backups_remote.html", "backups_restore.html", "dashboard.html"] # `class="app-row"` / `class="app-row {{...}}"` opening markup — [^-] excludes the # derived class names (app-row-list, app-row-icon, ...). ROW_OPEN_RE = re.compile(r'class="app-row[^-]') ICON_RE = re.compile(r'app-row-icon') ICON_ALLOW = {"app_row.html", "backups_apps.html"} # backups_apps: the aligned expander header failures = [] if not os.path.isdir(TPL): print("run from controller/ (internal/web/templates not found)") sys.exit(2) files = {f: io.open(os.path.join(TPL, f), encoding="utf-8").read() for f in sorted(os.listdir(TPL)) if f.endswith(".html")} if "app_row.html" not in files: failures.append("templates/app_row.html is missing — the canonical row partial") for fname, src in files.items(): n_open = len(ROW_OPEN_RE.findall(src)) if fname == "app_row.html": if n_open != 1: failures.append("app_row.html: expected the row-opening markup exactly once, found %d" % n_open) elif n_open: failures.append("%s: hand-rolled app-row markup (%d occurrence(s)) — render through the app_list_row partial" % (fname, n_open)) if ICON_RE.search(src) and fname not in ICON_ALLOW: failures.append("%s: app-row-icon outside the partial/allowlist — do not copy the icon markup" % fname) for fname, pat, why in FORBIDDEN: src = files.get(fname, "") if re.search(pat, src): failures.append("%s: forbidden old structure %r survives (%s)" % (fname, pat, why)) for fname in MUST_USE: if '{{template "app_list_row"' not in files.get(fname, ""): failures.append("%s: does not render through the app_list_row partial" % fname) if failures: print("app_row_dedup_gate: FAIL") for f in failures: print(" - " + f) sys.exit(1) print("app_row_dedup_gate: OK (row markup single-sourced in app_row.html; %d templates scanned)" % len(files))