Part A: shared app_list_row partial — ONE row grammar on four surfaces (v0.126.0)

- templates/app_row.html: app_list_row/app_list_row_end (layout_start/_end idiom) — icon+name
  (+optional secondary) left, caller action block right; compact 44px row; logoURL→PNG→
  optional FallbackIcon→hidden onerror chain
- applied: Távoli mentés toggle list, Visszaállítás restore-to-verify + .fab lists, dashboard
  Telepített alkalmazások (state edge + data-href preserved); Alkalmazások collapsed headers
  ALIGNED (icon+name left, status dot moved right before chevron; expander untouched)
- funcmap: dict + appHref; OffboxAppRow/AppBackupRow gain Slug
- style.css: .app-row family; stack-card/stack-info/stack-logo/stack-name/stack-desc/
  stack-actions rules retired (dashboard rows now shared); .app-backup-row-header matched
  to the shared row height
- scripts/app_row_dedup_gate.py: row markup single-sourced (red-proven: pasted old
  storage-path-item block → exit 1); render tests per surface (app_row_test.go)
- NO behavior change: toggle/download/expander actions byte-identical
This commit is contained in:
2026-07-13 11:26:48 +02:00
parent 7b3e7f2e71
commit 3eacb6c326
11 changed files with 380 additions and 87 deletions
+82
View File
@@ -0,0 +1,82 @@
# -*- 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))