gates: site gate 22 - the media-aware CSS leak check the phone bug needed
gates / gates (push) Failing after 12m29s
gates / gates (push) Failing after 12m29s
The two defects the operator found on his own phone are fixed and live since
5aeb36b5; this is what pins them so they cannot come back silently.
site_gates.py gate 22 parses site.css and shell.css into
(media context, selector, declarations) and fails on any property site.css
declares on a selector shell.css also styles that nothing overrides - neither
shell.css itself nor a `.shell ...`-scoped reset. site.css loads first, so
"later wins" is per PROPERTY, not per rule. Today: 689 site rules vs 119 shell
rules, 9 shared selectors, 0 leaks. .hamburger-box / .hamburger-line are an
explicit allow-list with the reason beside them.
It is media-aware precisely because the check it replaces was not. The STOP-1
leak diff excluded @media blocks and reported "exactly six" leaking
declarations; the real number is forty, and the worst of them
(@media (max-width: 900px) .nav-links { position: fixed; right: -100% })
parked the drawer's menu off-canvas and shipped.
Two decoys in test_gate_decoys.py, both the exact shape that went live, both
seen to convict:
site/shell-css-leak-in-media the body:not(.shell) scope taken back off that
one rule - the gate names the media query, the
selector and `right: -280px`
site/footer-outside-main <main> closed early on gyik.html - gate 3b's line
They plant and restore BYTES, with the anchor translated to the file's own line
ending: these files are CRLF in a Windows worktree and LF in a Linux one, and
replace_in's text-mode round trip would have rewritten a whole file on one side.
Also: CHANGELOG entries for both areas, the M3 report corrected where it quoted
its own wrong "exactly six", and one plain-language STATUS line.
This commit is contained in:
+32
-3
@@ -75,7 +75,8 @@ deliberately not built** (your brief) — the shell should settle first.
|
||||
paragraph. Found by a word-level diff.
|
||||
4. **The sidebar rendered as a horizontal row** across the content column, because „later wins" is
|
||||
per property and the controller's `.nav-links` never declares `display`. Diffing the two files'
|
||||
declaration sets found six such leaks; only this one was visible.
|
||||
declaration sets found six such leaks; only this one was visible. **That diff was wrong, and the
|
||||
section below says how** — the real number was forty.
|
||||
5. **The contact pages' header slid under the sidebar** (content column at x=642 instead of 240,
|
||||
form at two-thirds width) because those pages keep their header outside `<main>`.
|
||||
|
||||
@@ -87,6 +88,32 @@ deliberately not built** (your brief) — the shell should settle first.
|
||||
*controller's* `.btn-primary`, which the shell imports; it measured 4.01:1 again. Your existing
|
||||
remedy applied. `.nav-group-label` was 3.14:1 and moved to `--text-2`.
|
||||
|
||||
## Two defects a real phone found, after the switch
|
||||
|
||||
Reported by you, from your own phone, hours after it went live. Both were mine. Fixed, live as
|
||||
`?v=23`, and read back on all 20 pages at 390 / 860 / 1440 px.
|
||||
|
||||
1. **The phone menu was empty.** The drawer slid open correctly and had nothing in it. `site.css`'s
|
||||
old mobile nav still reached the shell's sidebar: under `@media (max-width: 900px)` it sets
|
||||
`.nav-links { position: fixed; right: -100% }`, which parked the menu off-canvas *inside* the open
|
||||
drawer. Every old-chrome selector (29 occurrences) is now scoped `body:not(.shell)` — scoped, not
|
||||
deleted, so a page without the shell still works.
|
||||
2. **The footer rendered as a second column** at the top right of 16 pages. `body` is a flex row on a
|
||||
shell page, and my insert moved the footer inside `<main>` only on pages that *ended* with
|
||||
`</main>`; a page with a `<script>` after it kept the footer as a direct child of `body`.
|
||||
|
||||
**The first one is a failure of my own check, not bad luck.** The leak diff I ran at STOP 1 — the one
|
||||
whose „exactly six" this report quoted above — **excluded `@media` blocks**. Media-aware, the number
|
||||
is **forty**, and the worst of them only apply below 900 px, which is exactly why the desktop screens
|
||||
I took looked right. A check that silently skips part of its subject is not a check: it returned a
|
||||
confident, wrong number, and I believed it rather than the phone.
|
||||
|
||||
**Both are now gated, each red-proofed by putting the live defect back:** gate 3b requires every shell
|
||||
page's footer to sit inside `<main class="content">`, and the new **gate 22** compares the two
|
||||
stylesheets **media-aware** and fails on any declaration of `site.css`'s that still reaches a shell
|
||||
page. Today it reads 689 site rules against 119 shell rules, 9 shared selectors, **0 leaks**. The two
|
||||
decoys (`site/shell-css-leak-in-media`, `site/footer-outside-main`) are the exact shapes that shipped.
|
||||
|
||||
## Still open
|
||||
|
||||
- **R-931** — felhom.eu still serves **nothing compressed**. `shell.css` adds 15 KB per page, all of
|
||||
@@ -98,5 +125,7 @@ deliberately not built** (your brief) — the shell should settle first.
|
||||
|
||||
## Gates and register
|
||||
|
||||
`site_gates.py` green throughout; full `repo_gates.py --fast` green on DooPlex. Register unchanged at
|
||||
128 — nothing filed, because everything found was either fixed in the session or already has a row.
|
||||
`site_gates.py` green throughout, now with gate 22; full `repo_gates.py --fast` green on DooPlex,
|
||||
decoy suite included. Register unchanged at 128 — nothing filed, because everything found was either
|
||||
fixed in the session or already has a row, and the two live defects were found, fixed and pinned the
|
||||
same day.
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
contact form sat under the menu.
|
||||
- **Two phone bugs that were already live are fixed**: the download page and the privacy page used to
|
||||
scroll sideways on a phone.
|
||||
- **Two things you found on your own phone after it went live, both mine, both now fixed** (reload
|
||||
once if the old version is still cached): the slide-out menu opened empty, and on 16 pages the
|
||||
footer sat as a narrow second column at the top right instead of at the bottom. The menu one is the
|
||||
worse of the two — the check I ran before switching was not looking at the phone-only part of the
|
||||
stylesheet, so it told me six things were wrong when forty were, and the six it did find were all
|
||||
visible on a big screen. Both now have a check that was watched failing before it was trusted.
|
||||
- **Waiting on you — nothing breaks if you do nothing:** the website still sends **everything
|
||||
uncompressed** (R-931). That is one setting on the server, it helps every page, and it is now the
|
||||
cheapest improvement left by a wide margin. Also: Search Console — the addresses did not change, so
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
## 2026-10-11 (evening) — site gate 22: no CSS leak from site.css onto a shell page, MEDIA-AWARE
|
||||
|
||||
- `site_gates.py` **gate 22** parses both stylesheets into (media context, selector, declarations) and
|
||||
fails when `site.css` declares a property on a selector `shell.css` also styles that nothing
|
||||
overrides — neither `shell.css` itself nor a `.shell …`-scoped reset. `site.css` loads first, so
|
||||
„later wins" is per **property**, not per rule. Today: 689 site rules vs 119 shell rules, 9 shared
|
||||
selectors, 0 leaking declarations. `.hamburger-box` / `.hamburger-line` are an explicit allow-list
|
||||
with the reason beside them (the shell's burger is the website's own button markup).
|
||||
- **Why it exists.** The morning's STOP-1 leak diff excluded `@media` blocks and reported six leaks
|
||||
where the media-aware number was forty. The worst of them, `@media (max-width: 900px) .nav-links {
|
||||
position: fixed; right: -100% }`, parked the drawer's menu off-canvas and shipped: the operator
|
||||
found an empty phone menu on the live site. The gate parses the media context precisely because the
|
||||
check it replaces did not.
|
||||
- **Two decoys** in `test_gate_decoys.py`, both the exact shape that went live, both seen to convict:
|
||||
`site/shell-css-leak-in-media` (the `body:not(.shell)` scope taken off that one rule → the gate
|
||||
names the media query, the selector and `right: -280px`) and `site/footer-outside-main` (`<main>`
|
||||
closed early on `gyik.html` → gate 3b's own line). They plant and restore **bytes**, with the anchor
|
||||
translated to the file's own line ending: these files are CRLF in a Windows worktree and LF in a
|
||||
Linux one, and `replace_in`'s text-mode round trip would have rewritten a whole file on one of them.
|
||||
|
||||
## 2026-10-10 (afternoon) — site gate 21: every catalog app has its logo under its slug (R-912)
|
||||
|
||||
- `site_gates.py` gate 21 reads `app-catalog-felhom.eu/templates/*/.felhom.yml` (the `slug:`, else the directory) and
|
||||
|
||||
+104
-1
@@ -31,6 +31,12 @@ Gates (all must pass; non-zero exit on any failure):
|
||||
19. dash-lang — a page shows dashboard pictures only in its own language (`dashboard-*-hu.webp` / `-en.webp`)
|
||||
20. viewer — data-gallery openers are <a href> to existing assets; gallery.js (with ?v=) exactly where openers are;
|
||||
no inline viewer copy
|
||||
22. shell leak — MEDIA-AWARE: no declaration in site.css reaches a shell page on a selector that
|
||||
shell.css also styles. site.css loads FIRST, so a property shell.css never sets
|
||||
survives — „later wins” is per PROPERTY, not per rule. Earned live 2026-10-11:
|
||||
@media (max-width: 900px) .nav-links { position: fixed; right: -100% } parked the
|
||||
drawer's items off-canvas and the phone menu was empty. The check that missed it
|
||||
EXCLUDED @media blocks and reported six leaks where the media-aware number was 40
|
||||
21. app logos — every catalog app (`app-catalog-felhom.eu/templates/*/.felhom.yml`, by its `slug:`) has
|
||||
website/assets/<slug>-logo.svg or .png — the dashboard asks for exactly that name and shows a grey
|
||||
placeholder otherwise (R-912: five apps had theirs under other names). The sibling catalog clone
|
||||
@@ -631,8 +637,105 @@ else:
|
||||
else:
|
||||
print(" app logos: %d catalog apps, each with its <slug>-logo file" % len(_slugs))
|
||||
|
||||
# gate 22 (2026-10-11): no CSS leak from site.css onto a shell page — MEDIA-AWARE.
|
||||
#
|
||||
# site.css loads BEFORE shell.css, so for a selector both files style, shell.css wins only on the
|
||||
# properties it actually declares; everything else survives from site.css. That is how the phone menu
|
||||
# shipped empty: site.css's old mobile nav still said, under @media (max-width: 900px),
|
||||
# `.nav-links { position: fixed; right: -100% }`, which parked the drawer's items off-canvas. It was
|
||||
# invisible at desktop width AND invisible to the check that was supposed to find it — that diff
|
||||
# skipped @media blocks and reported six leaking declarations where the media-aware number was forty.
|
||||
#
|
||||
# A leak is: site.css declares property P on selector S, shell.css also styles S, and nothing
|
||||
# overrides P for S — neither shell.css itself nor a `.shell …`-scoped reset in site.css. Scoping a
|
||||
# rule `body:not(.shell)` (the fix) takes its selector out of the comparison, which is the point.
|
||||
def css_rules(css):
|
||||
"""-> [(media_context, selector, [(prop, value), ...])].
|
||||
|
||||
@media / @supports / @layer nest and their context is CARRIED, so a declaration that applies only
|
||||
on a phone is still seen. @keyframes and @font-face hold no page selectors and are skipped.
|
||||
"""
|
||||
css = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
|
||||
out = []
|
||||
|
||||
def walk(text, media):
|
||||
i, n = 0, len(text)
|
||||
while i < n:
|
||||
j = text.find("{", i)
|
||||
if j < 0:
|
||||
break
|
||||
prelude = text[i:j].strip()
|
||||
depth, k = 1, j + 1
|
||||
while k < n and depth:
|
||||
if text[k] == "{":
|
||||
depth += 1
|
||||
elif text[k] == "}":
|
||||
depth -= 1
|
||||
k += 1
|
||||
body = text[j + 1:k - 1]
|
||||
if prelude.startswith("@"):
|
||||
if prelude.split()[0].lower() in ("@media", "@supports", "@layer"):
|
||||
walk(body, (media + " AND " + prelude) if media else prelude)
|
||||
else:
|
||||
decls = []
|
||||
for d in body.split(";"):
|
||||
if ":" in d and "{" not in d:
|
||||
prop, val = d.split(":", 1)
|
||||
if prop.strip():
|
||||
decls.append((prop.strip().lower(), val.strip()))
|
||||
for sel in prelude.split(","):
|
||||
sel = re.sub(r"\s+", " ", sel).strip()
|
||||
if sel:
|
||||
out.append((media, sel, decls))
|
||||
i = k
|
||||
|
||||
walk(css, "")
|
||||
return out
|
||||
|
||||
|
||||
# Shared ON PURPOSE: the shell's hamburger is the website's own button markup, so site.css styles the
|
||||
# same two class names the controller does. Another name here needs the same kind of sentence.
|
||||
_LEAK_ALLOW = {".hamburger-box", ".hamburger-line"}
|
||||
_site_css_p = os.path.join(W, "assets", "site.css")
|
||||
_shell_css_p = os.path.join(W, "assets", "shell.css")
|
||||
if not (os.path.isfile(_site_css_p) and os.path.isfile(_shell_css_p)):
|
||||
fail("shell leak: site.css or shell.css is missing — the gate checked nothing")
|
||||
else:
|
||||
_site_r = css_rules(io.open(_site_css_p, encoding="utf-8-sig").read())
|
||||
_shell_r = css_rules(io.open(_shell_css_p, encoding="utf-8-sig").read())
|
||||
if len(_site_r) < 200 or len(_shell_r) < 50:
|
||||
fail("shell leak: parsed %d site rules and %d shell rules — too few to be the real files, so "
|
||||
"nothing was compared" % (len(_site_r), len(_shell_r)))
|
||||
_shell_sel = set(sel for _, sel, _ in _shell_r)
|
||||
_overridden = set((sel, prop) for _, sel, decls in _shell_r for prop, _ in decls)
|
||||
for _, _sel, _decls in _site_r: # a `.shell X { … }` reset in site.css overrides X too
|
||||
if ".shell" in _sel:
|
||||
for _prop, _ in _decls:
|
||||
_overridden.add((_sel.split(" ")[-1], _prop))
|
||||
_leaks = []
|
||||
for _media, _sel, _decls in _site_r:
|
||||
if _sel in _LEAK_ALLOW or ".shell" in _sel or "body:not(.shell)" in _sel:
|
||||
continue
|
||||
if _sel not in _shell_sel:
|
||||
continue
|
||||
for _prop, _val in _decls:
|
||||
if (_sel, _prop) not in _overridden:
|
||||
_leaks.append((_media, _sel, _prop, _val))
|
||||
if _leaks:
|
||||
for _media, _sel, _prop, _val in _leaks[:12]:
|
||||
fail("shell leak: site.css %s`%s { %s: %s }` reaches a shell page — shell.css styles %s but "
|
||||
"never sets %s, so this one survives. Scope the rule body:not(.shell), or reset the "
|
||||
"property under .shell" % ((_media + " ") if _media else "", _sel, _prop, _val[:40],
|
||||
_sel, _prop))
|
||||
if len(_leaks) > 12:
|
||||
fail("shell leak: and %d more leaking declaration(s), not listed" % (len(_leaks) - 12))
|
||||
else:
|
||||
print(" shell leak: %d site rules vs %d shell rules, %d shared selector(s), 0 leaking "
|
||||
"declarations (media-aware)" % (len(_site_r), len(_shell_r),
|
||||
len(set(s for _, s, _ in _site_r) & _shell_sel)))
|
||||
|
||||
if fails:
|
||||
print("\nSITE GATES FAILED: %d problem(s)" % len(fails))
|
||||
sys.exit(1)
|
||||
print("site gates OK — BOM, emoji=0, nav/footer per language, analytics, no CDN, no legacy tokens, no <style>, "
|
||||
"cache-busted assets, twins + hreflang + globe, lang, same apps, no Hungarian in English, FAQ JSON-LD, clean tail, dashboard pictures per language, one picture viewer")
|
||||
"cache-busted assets, twins + hreflang + globe, lang, same apps, no Hungarian in English, FAQ JSON-LD, clean tail, dashboard pictures per language, one picture viewer, no shell CSS leak")
|
||||
|
||||
@@ -91,7 +91,7 @@ COVERS = {
|
||||
"switch at the wrong twin, lang=\"hu\" on an English page, an app missing from one apps page, "
|
||||
"ASCII-only Hungarian in English text and in a script message, a 'please', an English "
|
||||
"retrieval promise the Hungarian does not make, a FAQ JSON-LD drifting from the visible "
|
||||
"answer, text after </html>, a Hungarian dashboard picture on an English page, and four picture-viewer faults (an opener without href, an opener to a missing file, openers with no gallery.js, the inline viewer put back), and (R-912) a catalog app's logo under another name than its slug — each must convict WITH its own failure line"),
|
||||
"answer, text after </html>, a Hungarian dashboard picture on an English page, and four picture-viewer faults (an opener without href, an opener to a missing file, openers with no gallery.js, the inline viewer put back), and (R-912) a catalog app's logo under another name than its slug — each must convict WITH its own failure line; and (2026-10-11, both of them reported from a real phone AFTER they went live) site.css's old mobile nav reaching the shell's sidebar from inside an @media block — the leak the STOP-1 check missed because it skipped @media entirely — and a <footer> left outside <main class=\"content\">, which on a shell page renders as a second column"),
|
||||
"script-tests": ("R-885: FIVE cases in scripts/test_script_tests_gate.py, run from here: a suite that PRINTS "
|
||||
"OK and exits 1 (label without fact), a failing suite three levels deep (scope is a walk), "
|
||||
"an empty scripts/ tree (checked nothing), the genuine article (must pass), the nested mark"),
|
||||
@@ -614,6 +614,55 @@ decoy("site/text-after-html", "site_gates.py",
|
||||
replace_in(_EN("apps.html"), "</body>\n</html>", "</body>\n</html>\n=====\nSummary: 13 files included\n"),
|
||||
must="en/apps.html: text after </html>")
|
||||
|
||||
# -- site: the two defects that reached a real phone (2026-10-11) --------------------------------
|
||||
#
|
||||
# Both shipped in the M3 switch and were reported by the operator from his own phone; neither was
|
||||
# found here. `replace_in` cannot build them: it round-trips through text mode, and these files are
|
||||
# CRLF in a Windows worktree and LF in a Linux one, so a text-mode restore would rewrite the whole
|
||||
# file on one of the two. These plant and restore BYTES, with the anchors translated to the file's
|
||||
# own line ending.
|
||||
def replace_bytes(path, pairs):
|
||||
"""Mutate one or more exact spans of a real file IN BYTES; restore it byte-for-byte.
|
||||
|
||||
Anchors are written with \\n and translated at plant time. A span that is no longer there makes
|
||||
the decoy UNBUILDABLE, which is a failure of this suite — never a quietly skipped case."""
|
||||
def _plant():
|
||||
backup = io.open(path, "rb").read()
|
||||
nl = b"\r\n" if b"\r\n" in backup else b"\n"
|
||||
cur = backup
|
||||
for old, new in pairs:
|
||||
o = old.encode("utf-8").replace(b"\n", nl)
|
||||
n = new.encode("utf-8").replace(b"\n", nl)
|
||||
assert cur.count(o) == 1, ("%s: the decoy's anchor %r is gone or ambiguous — rebuild the "
|
||||
"decoy" % (path, old[:60]))
|
||||
cur = cur.replace(o, n, 1)
|
||||
io.open(path, "wb").write(cur)
|
||||
return lambda: io.open(path, "wb").write(backup)
|
||||
return _plant
|
||||
|
||||
|
||||
# 1. THE PHONE MENU WAS EMPTY. site.css's old mobile nav still reached the shell's sidebar: under
|
||||
# @media (max-width: 900px) it parked `.nav-links` off-canvas (position: fixed; right: -280px) —
|
||||
# inside the open drawer. Desktop looked right, so only a phone showed it, and the STOP-1 check
|
||||
# that should have caught it diffed the two stylesheets with @media blocks EXCLUDED: it reported
|
||||
# six leaking declarations where the media-aware number was forty. The decoy takes the
|
||||
# `body:not(.shell)` scope off that one rule, which is exactly the state that went live.
|
||||
decoy("site/shell-css-leak-in-media", "site_gates.py",
|
||||
replace_bytes(os.path.join(_WEB, "assets", "site.css"),
|
||||
[(" body:not(.shell) .nav-links {\n display: flex;\n flex-direction: column;",
|
||||
" .nav-links {\n display: flex;\n flex-direction: column;")]),
|
||||
must="shell leak: site.css @media (max-width: 900px) `.nav-links { right: -280px }`")
|
||||
|
||||
# 2. THE FOOTER RENDERED AS A SECOND COLUMN at the top right of 16 pages: body is a flex row on a
|
||||
# shell page, so a <footer> left outside <main class="content"> is a flex SIBLING of the sidebar
|
||||
# and the content, not the bottom of the page. The decoy closes <main> early and leaves the footer
|
||||
# outside it — the shape the insert produced on every page that had a <script> after </main>.
|
||||
decoy("site/footer-outside-main", "site_gates.py",
|
||||
replace_bytes(os.path.join(_WEB, "gyik.html"),
|
||||
[(" <footer>\n", " </main>\n <footer>\n"),
|
||||
("</footer>\n </main>\n", "</footer>\n")]),
|
||||
must='gyik.html: <footer> is outside <main class="content">')
|
||||
|
||||
# ── hostinstall (R-426, 2026-10-06) ──────────────────────────────────────────────────────────────
|
||||
#
|
||||
# Each decoy is a one-line edit a real session would make to felhom-host-install.sh, leaving the LABEL
|
||||
|
||||
+27
-1
@@ -1,4 +1,30 @@
|
||||
## 2026-10-11 (M3) — the whole site moves into the controller's shell
|
||||
## 2026-10-11 (evening) — the two M3 defects a real phone found, and the gate that now holds them
|
||||
|
||||
Both were mine, both shipped in the morning's switch, and both were reported by the operator from his
|
||||
own phone rather than caught here. `site.css?v=23` / `shell.css?v=23`, live and read back on all 20
|
||||
pages at 390 / 860 / 1440 px.
|
||||
|
||||
- **The phone menu was empty.** The drawer slid open correctly and had nothing in it: `site.css`'s old
|
||||
mobile nav still reached the shell's sidebar, because under `@media (max-width: 900px)` it sets
|
||||
`.nav-links { position: fixed; right: -100% }` — which parked the menu off-canvas *inside* the open
|
||||
drawer. Fixed at the source: every selector belonging to the website's OLD chrome (`nav`,
|
||||
`.nav-links*`, `.mobile-overlay`, `.hamburger`, `.logo*`, `.lang-globe-li` — 29 occurrences) is now
|
||||
scoped `body:not(.shell)`. Scoped, not deleted, so a page without the shell still works.
|
||||
`.hamburger-box` and `.hamburger-line` stay unscoped on purpose: the shell's burger uses them.
|
||||
- **Why the morning's check missed it.** The STOP-1 leak diff compared the two stylesheets'
|
||||
declaration sets **with `@media` blocks excluded**, and reported „exactly six" leaking declarations.
|
||||
Media-aware, the number is **forty** — and the worst of them only apply below 900 px, which is why
|
||||
the desktop looked right and a phone did not. A check that silently skips part of its subject is not
|
||||
a check; this one returned a confident wrong number and was believed.
|
||||
- **The footer rendered as a second column** at the top right of 16 pages. `body` is a flex row on a
|
||||
shell page, and the insert moved the footer inside `<main>` only on pages that *ended* with
|
||||
`</main>`; a page with a `<script>` after it kept the footer as a direct child of `body` — a flex
|
||||
sibling of the sidebar. Moved inside `<main class="content">` on all 16, each move asserted to have
|
||||
landed inside.
|
||||
- **Both are now gated, each red-proofed by putting the live defect back** (`site_gates.py` gate 3b
|
||||
and the new gate 22; decoys `site/footer-outside-main` and `site/shell-css-leak-in-media`).
|
||||
|
||||
## 2026-10-11 (M3) — the whole site moves into the controller's shell
|
||||
|
||||
The operator's own direction, after three mockups were rejected: the **controller's frame** (left
|
||||
sidebar, phone top bar, off-canvas drawer) around **the website's own content**. Switched in one
|
||||
|
||||
Reference in New Issue
Block a user