Repository navigation
fix(panels,flags): name the page after its subscriber on PasarGuard and Rebecca; read alpha-2 codes - #9
Conversation
…lpha-2 codes
Two defects found while hardening 1.3.0 against the panels' own source, plus the
audit trail for both. Nothing changes for an existing 3X-UI install, and no
adapter or panel-adapter behaviour is involved.
PasarGuard: the page titled itself "Subscription"
PasarGuard's page context is `{user, links, announce, announce_url, apps}` plus
Jinja2's `now` -- the panel-wide subscription settings are NOT in it. The prelude
read `subTitle` from nothing, so every PasarGuard page fell back to the generic
word. The page is now named by the admin's configured profile title when there is
one, and by the subscriber (`user.username`) otherwise -- the order the panel
itself resolves a title in (`SubscriptionOperation._format_profile_title` prefers
the admin's `profile_title` over `sub_settings.profile_title`).
A profile title that still carries a PasarGuard format placeholder
(`{USERNAME}`, `{DATA_LIMIT}`, ...) is refused rather than printed literally: the
page cannot resolve those variables, and a literal placeholder is worse than the
subscriber's own name. `user.ip` is still never read.
`user.username` is the only deterministic subscriber identifier in the context
(`email` does not exist; `id`, `note`, `subscription_url` and
`auto_delete_in_days` are `exclude=True`), and `UserValidator.validate_username`
bounds it to 3-128 characters of `[a-zA-Z0-9-_@.]`.
A separate `subscriber_display_name` island field was designed and rejected on
measurement, not on taste: a fourteenth `data-*` attribute plus its model and
render paths costs ~157 B, the flag fix below costs 179 B, and `pulsenova` had
274 B of headroom against a ceiling that may not be raised. The two do not fit
together. The prelude route costs zero artifact bytes, leaves 3X-UI's Go view
model untouched (a missing Go map key would print the literal "<no value>" into
every 3X-UI page), and keeps the PasarGuard assumption out of the shared
rendering. The reasoning is recorded in the prelude header.
Country flags: a node name carrying an ISO alpha-2 code found no flag
`flagOf` only ever looked for a regional-indicator pair, so `TR | Istanbul`,
`RU-01`, `GB-LON-1`, `FI-01` and `DE` returned '' and the row fell to the
monogram -- `T-I`, `R-U`, `G-B`, `F-I` -- which reads as the code.
This is an INPUT extension only. The return shape is still the regional-indicator
pair, `cfg.flag` is still `'🇩🇪'`, `CODES` is still the validity source, the
monogram fallback is unchanged and `cleanName` is unchanged, so the Phase 0.6
correction ("no alpha-2 change", which is about the return value) still holds.
The gradient workstream's "flagOf() unchanged" row is not a freeze on the input
contract.
A two-letter token is read as a country only when it is UPPERCASE, stands alone
between non-letters, and names a real country in the SAME 258-code registry the
emoji path uses. `LON` out of `GB-LON-1`, `USA`, `A1B`, `ZZ-01` and the lowercase
English words that are themselves assigned codes (`no`, `it`, `us`, `in`) are all
refused. An explicit flag emoji still wins, and the displayed name is never
rewritten -- an emoji means a flag and nothing else, but two letters can be an
operator's own wording.
Country names and city names (`Turkey - Istanbul`, `Türkiye`, `Finland Helsinki`)
still draw the monogram. A name table is well over 1 KB against 95 B of remaining
headroom, and guessing from an ambiguous name is worse than a monogram; the tests
assert the refusal so the boundary is recorded rather than implied.
All 15 artifacts are re-baselined: +179 B each, because `flag.js` is in the
shared app script. `pulsenova` is 204,705 B with 95 B of headroom, inside the
204,800 B ceiling. The ceiling was not raised and no other bytes were reclaimed.
Tests
- `tests/flag.test.mjs` (+7): the reported cases; all 676 letter pairs against the
258-code registry; the uppercase/bounded/assigned refusals; emoji-beats-code; a
re-assertion that no previous answer moved; the country-name boundary. The
existing 13 tests pass verbatim.
- `tests/panels-engines.test.mjs` (+10): the subscriber name; the admin's profile
title over it; a format placeholder refused; a missing name keeping the generic
fallback; no admin at all; hostile HTML; a non-Latin name; a 128-character name;
and an assertion that the page context carries exactly
`{user, links, announce, announce_url, now}`.
- `tests/build.test.mjs`: the 4 individual byte-locks and the 13-row
`FROZEN_ARTIFACTS` table.
- `tools/engines.mjs`: the PasarGuard page context can now state the admin's own
columns (`native.admin`), because the page context is not the /info payload --
the headers carry the values the panel resolved, the context carries the
admin's columns and nothing else.
Docs
- `docs/design/PGCLOCK-AUDIT.md` (new): read-only audit of the third-party PGClock
template. It independently confirms `user.username` as the exposed subscriber
identity and the `.env` + `CUSTOM_TEMPLATES_DIRECTORY` install mechanism; it has
no service-name, support-URL or flag logic; and it is unlicensed (no LICENSE
file, no header), so nothing was derived from it and `PROVENANCE.md` is
unchanged.
- `docs/design/PASARGUARD-ADAPTER-AUDIT.md` §8: the page context is not the /info
payload.
- `docs/design/FLAG-RENDERER-AUDIT.md` §17: the alpha-2 path, its cost, and the
boundary it does not cross.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Fixes PasarGuard page titling so a page names itself by the admin's configured profile title, falling back to the subscriber (user.username) when there is none, and refuses a title still carrying an unresolved PasarGuard format placeholder rather than printing it literally. Extends flagOf with an input-side alpha-2 path so an uppercase two-letter token that stands alone between non-letters and is in the 258-code CODES registry (TR | Istanbul, RU-01, GB-LON-1, DE) resolves to a country flag, while lowercase words, city fragments, unassigned pairs, and country/city names still draw the monogram and the displayed name is never rewritten; an explicit emoji still wins. Re-baselines all 15 byte-locks (+179 B each, leaving pulsenova with 95 B headroom) and adds the PGClock, PasarGuard-page-context, and alpha-2 audit sections documenting that the return shape, CODES, and fallbacks are unchanged.
Worth a look
- v1.1.0 Row byte-lock was moved to a new artifact —
tests/build.test.mjs:140· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Alpha-2 token regex treats lowercase letters as separators —
docs/design/FLAG-RENDERER-AUDIT.md:1025· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 228 functions depend on the 173 functions this change touches.
Health — this change adds coupling hotspots:
- new:
setAttr()— 20 callers, 4 callees - new:
island()— 3 callers, 7 callees - new:
copyValue()— 3 callers, 6 callees - new:
island()— 3 callers, 6 callees - new:
applyLabels()— 2 callers, 8 callees - new:
applyRows()— 3 callers, 5 callees - new:
renderExplorer()— 5 callers, 3 callees - new:
render()— 2 callers, 7 callees - …and 19 more — each is listed as a finding
Verification — 228 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 205 function(s) in the blast radius were not formally verified this run
Docs that may be stale (advisory)
docs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ 2. Should we introduce a panel abstraction layer? (lines 73-101): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ 4. Which parts must remain panel-agnostic? (lines 130-146): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ 6. Estimated byte / build impact (lines 154-171): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ The two phases, priced (lines 172-187): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ B. Existing files that change — and what it costs (lines 254-266): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ C. Explicitly NOT to be touched (lines 267-274): references changed symbolsscriptsdocs/design/CUSTOM-TEMPLATES-PROPOSAL.md§ 3. Current architecture (as-is, factual) (lines 52-116): references changed symbolsscriptsdocs/design/CUSTOM-TEMPLATES-PROPOSAL.md§ 10. Compatibility with the existing runtime (lines 294-317): references changed symbolsscriptsdocs/design/DOCUMENTATION-DESIGN-SYSTEM-PROPOSAL.md§ 10.2 What must not change (lines 658-673): references changed symbolsscriptsdocs/design/DOCUMENTATION-IMPLEMENTATION-PLAN.md§ 3.2 The six protected surfaces (lines 74-84): references changed symbolsscripts
…and 10 more.
Formal verification
Could not verify: Could not verify flagOf.
The verifier did not have enough to check flagOf, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous
Could not verify: Could not verify b64.
The verifier did not have enough to check b64, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: could not isolate function
· 1 grounded finding(s) anchored inline below; 26 more finding(s) on lines outside this diff (see the check run).
| @@ -98,7 +111,8 @@ function letter(cp) { | |||
| spurious code — "🇿🇿🇺🇸" reads past the tofu pair and finds US, it does not | |||
| read the middle two as ZU. A lone trailing indicator matches nothing. */ | |||
| export function flagOf(text) { | |||
There was a problem hiding this comment.
flagOf()
high coupling complexity (Ca·Ce = 12).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Rebecca's page titled itself with the generic word "Subscription". Its render context carries no profile title at all: subscriptionHeaders() writes the panel-wide subscription_profile_title into the /info `profile-title` HEADER, and renderSubscriptionHTML -> renderSubscriptionPageTemplate never passes it, so the title the adapter reads from that header is invisible to the page. The one identity the page does receive is user.username, so that now names the page -- the same fallback the PasarGuard prelude takes when no profile title is configured. A value with no word in it at all (absent, empty, whitespace-only) is not a name and the page keeps its generic fallback; the value is never rewritten. pongo2 has no trim filter and no string filter, so the test is `wordcount`, which is 0 for every one of those forms and never errors. No brace guard is needed here, unlike PasarGuard: Rebecca never runs a title through str.format_map. PasarGuard and 3X-UI are untouched, and no artifact byte-lock moves: flag.js and the 3X-UI layouts are not involved, and no panel shell is size-locked. Tests: tests/panels-engines.test.mjs gains seven cases through the SHIPPED prelude on the real pongo2 -- the subscriber names the page; the /info header title does not reach the page; a missing name keeps the fallback; a non-Latin name is carried as text; a hostile name is escaped and never becomes markup; a long name is carried whole; and the page context really carries no profile title. rbExpected() no longer pins the page title to the header-derived ''. CI: .github/workflows/release-validation.yml runs the four release commands on ubuntu-latest, with the three tools the suite needs and a Node runtime does not provide -- Go, Python 3 with Jinja2, and ShellCheck. The existing Docs workflow never ran any of them, so a green check on a pull request said nothing about the suite. Docs: REBECCA-ADAPTER-AUDIT.md §2, PANEL-COMPATIBILITY-AUDIT.md §5 and the CHANGELOG record the page-vs-header distinction. FLAG-RENDERER-AUDIT.md §17.2 and §17.6 are corrected to the real test counts (13 -> 20, +7).
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify review — findings
No blocking issues surfaced.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 334 functions depend on the 279 functions this change touches.
Health — this change adds coupling hotspots:
- new:
setAttr()— 20 callers, 4 callees - new:
island()— 3 callers, 7 callees - new:
copyValue()— 3 callers, 6 callees - new:
island()— 3 callers, 6 callees - new:
applyLabels()— 2 callers, 8 callees - new:
applyRows()— 3 callers, 5 callees - new:
renderExplorer()— 5 callers, 3 callees - new:
render()— 2 callers, 7 callees - …and 19 more — each is listed as a finding
Verification — 334 functions in the blast radius were not formally verified this run (proofs are advisory here).
Health delta baseline: last indexed commit ebb10a5 (diverged from this PR's base — delta is approximate).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 311 function(s) in the blast radius were not formally verified this run
Docs that may be stale (advisory)
docs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ 2. Should we introduce a panel abstraction layer? (lines 73-101): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ 4. Which parts must remain panel-agnostic? (lines 130-146): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ 6. Estimated byte / build impact (lines 154-171): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ The two phases, priced (lines 172-187): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ B. Existing files that change — and what it costs (lines 254-266): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ C. Explicitly NOT to be touched (lines 267-274): references changed symbolsscriptsdocs/design/CUSTOM-TEMPLATES-PROPOSAL.md§ 3. Current architecture (as-is, factual) (lines 52-116): references changed symbolsscriptsdocs/design/CUSTOM-TEMPLATES-PROPOSAL.md§ 10. Compatibility with the existing runtime (lines 294-317): references changed symbolsscriptsdocs/design/DOCUMENTATION-DESIGN-SYSTEM-PROPOSAL.md§ 10.2 What must not change (lines 658-673): references changed symbolsscriptsdocs/design/DOCUMENTATION-IMPLEMENTATION-PLAN.md§ 3.2 The six protected surfaces (lines 74-84): references changed symbolsscripts
…and 10 more.
· 1 grounded finding(s) anchored inline below; 26 more finding(s) on lines outside this diff (see the check run).
| @@ -98,7 +111,8 @@ function letter(cp) { | |||
| spurious code — "🇿🇿🇺🇸" reads past the tofu pair and finds US, it does not | |||
| read the middle two as ZU. A lone trailing indicator matches nothing. */ | |||
| export function flagOf(text) { | |||
There was a problem hiding this comment.
flagOf()
high coupling complexity (Ca·Ce = 12).
Grounded coupling-delta finding (deterministic), not an LLM guess.
The only conflict was one CHANGELOG heading: main renamed "## [1.3.0] - 2026-09-26" to "## [1.3.0] - Unreleased" (1.3.0 has not shipped), while this branch had added a "## [Unreleased]" section directly above it. Both sides are kept: the branchs hardening section stays, and mains newer heading wins. Nothing else conflicted, and main changed no file under src/, tests/, tools/, installer/ or template/, so the branches test results stand unchanged. This also unblocks CI: GitHub does not run pull_request workflows while a pull request has a merge conflict, which is why the new Release validation workflow had not started.
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify review — findings
No blocking issues surfaced.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 334 functions depend on the 279 functions this change touches.
Health — this change adds coupling hotspots:
- new:
setAttr()— 20 callers, 4 callees - new:
island()— 3 callers, 7 callees - new:
copyValue()— 3 callers, 6 callees - new:
island()— 3 callers, 6 callees - new:
applyLabels()— 2 callers, 8 callees - new:
applyRows()— 3 callers, 5 callees - new:
renderExplorer()— 5 callers, 3 callees - new:
render()— 2 callers, 7 callees - …and 19 more — each is listed as a finding
Verification — 334 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 311 function(s) in the blast radius were not formally verified this run
Docs that may be stale (advisory)
docs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ 2. Should we introduce a panel abstraction layer? (lines 73-101): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ 4. Which parts must remain panel-agnostic? (lines 130-146): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ 6. Estimated byte / build impact (lines 154-171): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ The two phases, priced (lines 172-187): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ B. Existing files that change — and what it costs (lines 254-266): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ C. Explicitly NOT to be touched (lines 267-274): references changed symbolsscriptsdocs/design/CUSTOM-TEMPLATES-PROPOSAL.md§ 3. Current architecture (as-is, factual) (lines 52-116): references changed symbolsscriptsdocs/design/CUSTOM-TEMPLATES-PROPOSAL.md§ 10. Compatibility with the existing runtime (lines 294-317): references changed symbolsscriptsdocs/design/DOCUMENTATION-DESIGN-SYSTEM-PROPOSAL.md§ 10.2 What must not change (lines 658-673): references changed symbolsscriptsdocs/design/DOCUMENTATION-IMPLEMENTATION-PLAN.md§ 3.2 The six protected surfaces (lines 74-84): references changed symbolsscripts
…and 10 more.
· 1 grounded finding(s) anchored inline below; 26 more finding(s) on lines outside this diff (see the check run).
| @@ -98,7 +111,8 @@ function letter(cp) { | |||
| spurious code — "🇿🇿🇺🇸" reads past the tofu pair and finds US, it does not | |||
| read the middle two as ZU. A lone trailing indicator matches nothing. */ | |||
| export function flagOf(text) { | |||
There was a problem hiding this comment.
flagOf()
high coupling complexity (Ca·Ce = 12).
Grounded coupling-delta finding (deterministic), not an LLM guess.
A red npm test was hiding npm run verify, npm run lint:sh and npm run build:panels. The job is about the four results.
main gained two commits that put the released date back on the 1.3.0 heading (`addd4df`, `13fb6c8`): v1.3.0 is published, tag `v1.3.0` -> `e103f74`, and the heading had been changed to "Unreleased" by #10 after the release. CHANGELOG.md conflicted, because this branch adds an `## [Unreleased]` section of its own at the top of the file. Resolved minimally: this branch's `## [Unreleased]` section (the PasarGuard and Rebecca title fixes) stays at the top, and the 1.3.0 section keeps main's restored `## [1.3.0] - 2026-09-26` heading, so the file matches the tagged release again. No other file conflicted and no content was dropped from either side.
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify review — findings
No blocking issues surfaced.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 334 functions depend on the 279 functions this change touches.
Health — this change adds coupling hotspots:
- new:
setAttr()— 20 callers, 4 callees - new:
island()— 3 callers, 7 callees - new:
copyValue()— 3 callers, 6 callees - new:
island()— 3 callers, 6 callees - new:
applyLabels()— 2 callers, 8 callees - new:
applyRows()— 3 callers, 5 callees - new:
renderExplorer()— 5 callers, 3 callees - new:
render()— 2 callers, 7 callees - …and 19 more — each is listed as a finding
Verification — 334 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 311 function(s) in the blast radius were not formally verified this run
Docs that may be stale (advisory)
docs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ 2. Should we introduce a panel abstraction layer? (lines 73-101): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ 4. Which parts must remain panel-agnostic? (lines 130-146): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ 6. Estimated byte / build impact (lines 154-171): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ The two phases, priced (lines 172-187): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ B. Existing files that change — and what it costs (lines 254-266): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ C. Explicitly NOT to be touched (lines 267-274): references changed symbolsscriptsdocs/design/CUSTOM-TEMPLATES-PROPOSAL.md§ 3. Current architecture (as-is, factual) (lines 52-116): references changed symbolsscriptsdocs/design/CUSTOM-TEMPLATES-PROPOSAL.md§ 10. Compatibility with the existing runtime (lines 294-317): references changed symbolsscriptsdocs/design/DOCUMENTATION-DESIGN-SYSTEM-PROPOSAL.md§ 10.2 What must not change (lines 658-673): references changed symbolsscriptsdocs/design/DOCUMENTATION-IMPLEMENTATION-PLAN.md§ 3.2 The six protected surfaces (lines 74-84): references changed symbolsscripts
…and 10 more.
· 1 grounded finding(s) anchored inline below; 26 more finding(s) on lines outside this diff (see the check run).
| @@ -98,7 +111,8 @@ function letter(cp) { | |||
| spurious code — "🇿🇿🇺🇸" reads past the tofu pair and finds US, it does not | |||
| read the middle two as ZU. A lone trailing indicator matches nothing. */ | |||
| export function flagOf(text) { | |||
There was a problem hiding this comment.
flagOf()
high coupling complexity (Ca·Ce = 12).
Grounded coupling-delta finding (deterministic), not an LLM guess.
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify review — findings
No blocking issues surfaced.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 335 functions depend on the 280 functions this change touches.
Health — this change adds coupling hotspots:
- new:
setAttr()— 20 callers, 4 callees - new:
island()— 3 callers, 7 callees - new:
copyValue()— 3 callers, 6 callees - new:
island()— 3 callers, 6 callees - new:
applyLabels()— 2 callers, 8 callees - new:
applyRows()— 3 callers, 5 callees - new:
renderExplorer()— 5 callers, 3 callees - new:
render()— 2 callers, 7 callees - …and 19 more — each is listed as a finding
Verification — 335 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 312 function(s) in the blast radius were not formally verified this run
Docs that may be stale (advisory)
docs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ 2. Should we introduce a panel abstraction layer? (lines 73-101): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ 4. Which parts must remain panel-agnostic? (lines 130-146): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ 6. Estimated byte / build impact (lines 154-171): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ The two phases, priced (lines 172-187): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ B. Existing files that change — and what it costs (lines 254-266): references changed symbolsscriptsdocs/design/ARCHITECTURE-MULTIPANEL-PLAN.md§ C. Explicitly NOT to be touched (lines 267-274): references changed symbolsscriptsdocs/design/CUSTOM-TEMPLATES-PROPOSAL.md§ 3. Current architecture (as-is, factual) (lines 52-116): references changed symbolsscriptsdocs/design/CUSTOM-TEMPLATES-PROPOSAL.md§ 10. Compatibility with the existing runtime (lines 294-317): references changed symbolsscriptsdocs/design/DOCUMENTATION-DESIGN-SYSTEM-PROPOSAL.md§ 10.2 What must not change (lines 658-673): references changed symbolsscriptsdocs/design/DOCUMENTATION-IMPLEMENTATION-PLAN.md§ 3.2 The six protected surfaces (lines 74-84): references changed symbolsscripts
…and 10 more.
· 1 grounded finding(s) anchored inline below; 26 more finding(s) on lines outside this diff (see the check run).
| @@ -98,7 +111,8 @@ function letter(cp) { | |||
| spurious code — "🇿🇿🇺🇸" reads past the tofu pair and finds US, it does not | |||
| read the middle two as ZU. A lone trailing indicator matches nothing. */ | |||
| export function flagOf(text) { | |||
There was a problem hiding this comment.
flagOf()
high coupling complexity (Ca·Ce = 12).
Grounded coupling-delta finding (deterministic), not an LLM guess.
…lated
Both installer panel suites fail on ubuntu-latest and pass on the authoring
host, for two reasons that have nothing to do with the adapters they cover.
Both are harness defects, and neither is fixed by changing production code.
1. sqlite3 leaked in from the runner.
`sqlite: false` meant only "write no sqlite3 shim". The adapters decide
with `command -v sqlite3` (rt_panel_rebecca_db_ready), and ubuntu-latest
ships a real sqlite3, so the gate opened anyway, activation took the
automatic path, and the manual-activation test read
inactive
outcome=auto
...
(Automatic activation needs the sqlite3 command and Rebecca's SQLite database.)
where it expected `manual`. Shadowing sqlite3 with a shim that fails
cannot fix this: the gate asks whether the command exists, not whether it
works.
`sqlite: false` now removes every directory that holds a sqlite3 from the
child's PATH, replacing each with a mirror of itself (the same entries,
symlinked, minus sqlite3) so nothing else on the host stops resolving.
A new test plants a real-looking sqlite3 -- plus a second tool, to prove
the substituted directory still carries what it held -- and asserts that
`command -v sqlite3` fails in the child while the other tool resolves.
2. The network leaked in from the payload.
The payload was two designs. RT_TEMPLATES_AVAILABLE names seventeen, so
rt_complete_install always judged the install short of what its version
ships and downloaded the release pinned to that version. On a runner with
network the download succeeded, the template store was rebuilt from the
PUBLISHED v1.3.0 release, and verify then compared that release's artifact
against this working tree's:
Completing the Row-Template 1.3.0 installation (designs and installer files)...
ok Installation completed: 17 design(s) available.
FAIL canonical artifact does not match the selected template (row).
The suites were therefore validating a published release against the
branch, and doing it over the network -- which is the opposite of what
they are for, and it made them depend on whatever v1.3.0 happened to
contain.
The payload is now the complete release (availableTemplateIds(), the same
seventeen ids as RT_TEMPLATES_AVAILABLE), so rt_complete_install has
nothing to complete and is never reached. curl and wget are denied in
every fake host -- they record the attempt and fail -- so a regression
fails the suite loudly instead of silently reaching the internet. The two
life-cycle tests assert the recorded attempts are empty, so "this suite
needs no network" is measured rather than assumed.
Production behaviour is untouched: the installer, the adapters and the
release tooling are byte-identical. The payload is a real release, and the
only thing that changed is that the tests now hand the installer all of it.
Two defects in the previous commit, both in test code. Found by the Linux
run: three failures down to one, and the one was this commit's own new
test.
bashRun spreads its first argument, so a string became one line per
character:
"set -Eeuo pipefail\ns\n=\n0\n;\n \ne\nc\nh\no\n \nA"
The new sqlite3 test passed a joined string, so its script died under
`set -e` -- the first one-character line is a command that does not exist
-- before printing anything, and the assertion read an empty stdout
(`actual: ''`). Every other caller passes an array. bashRun now accepts
either shape, as the suites' own `run` helpers already do, so the
footgun cannot recur; the test passes an array, and reports the exit code
and stderr when it fails.
A mirror is now stamped once it is complete. A run killed by a signal (a
timeout, a cancelled job) never reaches the exit hook, and a half-built
mirror would otherwise be reused on the next run -- failing, if `bash`
were the entry that never got linked, in a way that has nothing to do
with sqlite3.
test(panels): make the panel life-cycle suites offline and sqlite-isolated
Summary
Three defects found while hardening 1.3.0 against the panels' own source, plus the audit trail for all three, plus a Linux CI workflow that actually runs the release checks. Nothing changes for an existing 3X-UI install; no adapter and no installer file is touched.
1. PasarGuard: the page titled itself "Subscription"
PasarGuard's Jinja2 page context is exactly
{user, links, announce, announce_url, apps}plusnow— the panel-wide subscription settings are not in it. The prelude setsubTitle = '', so every PasarGuard page fell back to the generic word.The page is now named by the admin's configured profile title when there is one, and by the subscriber (
user.username) otherwise — the order the panel itself resolves a title in (SubscriptionOperation._format_profile_titleprefers the admin'sprofile_titleoversub_settings.profile_title).user.usernameis the only deterministic subscriber identifier in the context (emaildoes not exist;id,note,subscription_url,auto_delete_in_daysareexclude=True).user.ipis still never read.{DATA_LIMIT},{EXPIRE_DATE}, …) is refused rather than printed literally — the page cannot resolve those variables.{%- autoescape true -%}block that already wraps the island.2. Rebecca 1.x: the page titled itself "Subscription" too
Same symptom, different cause, and the cause is the interesting part: Rebecca's page context carries no profile title at all.
subscriptionHeaders()writes the panel-widesubscription_profile_titleinto the/infoheader —profile-title: base64:…(internal/app/user/subscription.go:1120). That is the value the adapter reads into itstitlefield.renderSubscriptionHTML()callsrenderSubscriptionPageTemplate(...)(:1131,:1180), andsubscriptionTemplateContext()(:3742) passes only{user, links, links_text, usage_url, support_url, token, current_timestamp, remaining_days}plus the VPN keys.So the adapter's header-derived
titleis correct but invisible to the page, and the page had nothing to read.subTitleis now derived from the only in-context identity,user.username:wordcount > 0, not!= "": pongo2 v6.1.0 has notrimand nostringfilter, andwordcountis0for nil, empty and whitespace-only without ever raising — so a missing, empty or blank name keeps the generic fallback instead of printing an empty title.user.usernameis validated by Rebecca itself against^[a-zA-Z0-9._@-]+$, 3–32 characters (internal/app/user/validation.go:15,308-310). Nothing else is read — noid, no token, no subscription URL, no internal value.{-placeholder guard is needed on this path (unlike PasarGuard): Rebecca never runs a title throughstr.format_map, and{is not in the username charset.{%- autoescape on -%}, so the value is escaped, never markup.subSupportUrl = support_urlwas already correct and is unchanged.PasarGuard is untouched by this fix — no PasarGuard prelude, context or test changed.
3. Country flags: a node name carrying an ISO alpha-2 code found no flag
flagOfonly recognised a regional-indicator pair, soTR | Istanbul,RU-01,GB-LON-1,FI-01,DEreturned''and the row fell to the monogram (T-I,R-U,G-B,F-I) — which reads as the code.This is an input extension only: the return shape is still the pair,
cfg.flagis still'🇩🇪',CODESis still the validity source, the monogram fallback andcleanNameare unchanged. A token is read as a country only when it is uppercase, stands alone between non-letters, and names a real country in the same 258-code registry.LON,USA,A1B,ZZ-01and the lowercase English words that are themselves codes (no,it,us,in) are all refused. An explicit emoji still wins and the label is never rewritten.Country/city names are not resolved — a name table is well over 1 KB against 95 B of headroom, and guessing from an ambiguous name is worse than a monogram. The tests assert the refusal. A node named only by a city (
Istanbul,Frankfurt) therefore still gets a monogram in v1.3.0; that is a documented fallback, not a defect, anddocs/design/FLAG-RENDERER-AUDIT.md§17.5 records the boundary and the three reasons for it. This change does not widen name detection.Design decision worth reviewing
A separate
subscriber_display_nameisland field was designed and rejected on measurement: a fourteenthdata-*attribute plus its model and render paths costs ~157 B, the flag fix costs 179 B, andpulsenovahad 274 B of headroom against a ceiling that may not be raised. The prelude route costs zero artifact bytes, leaves 3X-UI's Go view model untouched (a missing Go map key would print the literal"<no value>"into every 3X-UI page), and keeps the PasarGuard assumption out of shared rendering. Reasoning is recorded in the prelude headers.Artifact impact
All 15 3X-UI artifacts are re-baselined: +179 B each, because
flag.jsis in the shared app script.pulsenovais 204,705 B with 95 B of headroom, inside the 204,800 B ceiling. The ceiling was not raised and no other bytes were reclaimed.The panel shells are not size-locked —
FROZEN_ARTIFACTScovers only the 13 3X-UI designs — so the Rebecca prelude change moves no locked byte.Tests
tests/panels-engines.test.mjs+16 (15 → 31): +9 for PasarGuard (through the shipped prelude on the real Jinja2 — subscriber name; admin profile title over it; a placeholder refused; a missing name keeping the fallback; no admin; hostile HTML; a non-Latin name; a 128-character name; the page context's exact key set) and +7 for Rebecca (through the shipped prelude on the real pongo2 — the page names the subscriber; the/infoheader title does not reach the page; a missing name keeps the generic fallback acrossnull/undefined/''/' '; a Persian name is carried as text; a hostile name is escaped and never becomes markup; a 32-character name is carried whole, not truncated; the page context's exact key set, proving no profile title is in it).tests/flag.test.mjs+7 (13 → 20) — the reported cases; all 676 letter pairs against the 258-code registry; the uppercase/bounded/assigned refusals; emoji-beats-code; a re-assertion that no previous answer moved; the country-name boundary.tests/build.test.mjs— the 4 individual byte-locks and the 13-rowFROZEN_ARTIFACTStable.tools/engines.mjs— the PasarGuard page context can state the admin's own columns (native.admin), because the page context is not the/infopayload.Results — Linux (GitHub Actions)
.github/workflows/release-validation.yml(new) runs the release checks onubuntu-latest— Node 22, Go 1.22,python3-jinja2, ShellCheck — with each of the last three steps guarded byif: ${{ !cancelled() }}, so all four report even when an earlier one fails.Run 36328376214, head
a6c3f51:npm test# tests 784,# pass 781,# fail 3, 70.6 snpm run verifyall checks passednpm run lint:shshellcheck: 12 scripts cleannpm run build:panels51 shell(s) written to dist/panels/anddist/shells/,G1 green — the Go path is losslessBefore this workflow the release checks had never run on Linux;
lint:shhad never run at all.The three failures, isolated
install, verify, rebrand, switch design, roll back and uninstall on a PasarGuard hostinstaller-panel-pasarguard.test.mjs:3787e9d705(this PR's first commit)without sqlite3, activation places the page and says exactly what to setinstaller-panel-rebecca.test.mjs:425ebb10a5(main)install, verify, rebrand, switch design, roll back and uninstall on a Rebecca hostinstaller-panel-rebecca.test.mjs:4407e9d705(this PR's first commit)Measured, not assumed:
main@ebb10a57e9d705(before the Rebecca fix)523441f(with the Rebecca fix)a6c3f51(current head)So the Rebecca fix adds 7 tests and 0 new failures, and #2 is pre-existing on
main. The two commits added after523441f(a CHANGELOG entry and a workflow path) move no test count and change no result.Why #1 and #3 fail on a networked runner
rt_complete_install(installer/lib/row-template.sh:2573) finishes an install that is short of what its own version ships: when the store is missing designs, it downloads the release pinned to the installed version (…/releases/download/v1.3.0) and rebuilds the store from it.Both tests install a deliberately two-design payload (
ids: ['row', 'editorial']), so completion fires, the store ends up holding the releasedrowartifact, andrt_cmd_verifycompares it against$RT_DIST, which came from the locally built payload:On
mainthe two are byte-identical, because v1.3.0 was cut frommain. This PR changesflag.jsandtemplate/index.html, so the built artifact no longer equals the released one, and the comparison fails. It reproduces only where the release is reachable — on a machine that cannot reach GitHub, completion warns and leaves the store alone, so the tests pass.This is a harness assumption — the working tree builds the latest release — rather than a product regression: a real install completes from the release that ships with its own installer. It is worth knowing that the assumption breaks for any artifact-changing PR, and will keep failing on
mainafter this merges until a new release is cut.tests/installer-template-store.test.mjs:395already shows the hermetic form (stubrt_fetch_release); this PR does not touch it, to keep the change to its subject.Docs
docs/design/PGCLOCK-AUDIT.md(new) — read-only audit of the third-party PGClock template: it confirmsuser.usernameas the exposed identity, has no service-name/support-URL/flag logic, and is unlicensed, so nothing was derived andPROVENANCE.mdis unchanged.docs/design/PASARGUARD-ADAPTER-AUDIT.md§8 — the page context is not the/infopayload.docs/design/REBECCA-ADAPTER-AUDIT.md§2 — the same distinction for Rebecca, againstsubscriptionHeadersandsubscriptionTemplateContext: there is no profile title in the page context, so the page's title and the adapter's header-derivedtitlemay legitimately differ.docs/design/PANEL-COMPATIBILITY-AUDIT.md§5 — thesubTitlerow in the mapping table is the/infomapping, not the page's.docs/design/FLAG-RENDERER-AUDIT.md§17 — the alpha-2 path, its cost, and its boundary.