Skip to content

refactor(ui): put every PHP-to-JS config blob behind one emitter and one reader - #301

Merged
HugoFara merged 2 commits into
refactor/shrink-globalsfrom
refactor/config-islands
Aug 30, 2026
Merged

refactor(ui): put every PHP-to-JS config blob behind one emitter and one reader#301
HugoFara merged 2 commits into
refactor/shrink-globalsfrom
refactor/config-islands

Conversation

@HugoFara

Copy link
Copy Markdown
Owner

Stack 4 of 6. Base: #300. Two commits: the config-island unification, and
the CLAUDE.md correction that describes it and the two PRs before it.

Config islands

Server values reach a page as a JSON island because the CSP build of Alpine
cannot evaluate inline expressions. The pattern is sound; the plumbing was not.
35 islands each hand-rolled their own script tag and escaping flags, and
eight pages hand-rolled their own reader, so the two ends of each contract could
drift with nothing to catch it.

Adds ConfigIsland (PHP) and readPageConfig / hasPageConfig (TypeScript),
and routes all of it through them. Three concrete bugs this fixes:

  • Six islands emitted json_encode with no escaping flags at all, so a
    value containing </script> would close the element early and the rest of the
    blob would parse as markup.
  • preferences.php hand-spliced a pre-JSON-encoded value into a literal
    {"currentLanguageCode": …} — valid only as long as the controller remembered
    to encode it. It now passes the plain string and the controller stops
    encoding.
  • starter_vocab.ts parsed without a try/catch, so a malformed blob
    threw during init() and left the user an inert shell. readPageConfig never
    throws: missing element, empty blob and bad JSON all fall back to the caller's
    defaults, merged shallowly with null treated as absent.

JsonScriptBlockEscapingTest now pins a stronger invariant than the old flag
scan. One test feeds the emitter a payload that tries to close its own script
element and checks the rendered output; the other asserts no file outside
ConfigIsland emits a config block, so a new view cannot quietly reintroduce
the hand-rolled form.

Docs

CLAUDE.md was routing work to src/backend/Views, src/backend/Services and
src/backend/Router, none of which exist — what remains under src/backend is
the REST API and the entry point. It also named getSettingWithDefault(),
Globals::table(), global $var, the archivedtexts and textitems2 tables,
and a frontend laid out as pgm.ts / text_events.ts / audio_controller.ts.
All gone: archived texts are a TxArchivedAt column on texts, and
textitems2 is word_occurrences.

Corrects the request flow, directory tree, table list and frontend layout, adds
the missing Activity and Book modules, and documents the config-island pattern
as the only sanctioned way to move values from PHP to a page.

Stack: #298#299#3004#5#6

…one reader

The CSP build of Alpine cannot evaluate inline expressions, so server values
reach a page as a JSON island rather than through x-data. That pattern was
sound; the plumbing around it was not. Thirty-five islands each hand-rolled
their own script tag and escaping flags, and eight pages hand-rolled their own
reader, so the two ends of each contract could drift with nothing to catch it.

Adds ConfigIsland (PHP) and readPageConfig/hasPageConfig (TypeScript), and
routes all of it through them. What that fixes concretely:

- Six islands emitted json_encode with no escaping flags at all, so a value
  containing </script> would close the element early and the rest of the blob
  would parse as markup.
- preferences.php hand-spliced a pre-JSON-encoded value into a literal
  {"currentLanguageCode": ...}, which is only valid as long as the controller
  remembers to encode it. It now passes the plain string and the controller
  stops encoding.
- starter_vocab.ts parsed without a try/catch, so a malformed blob threw
  during init() and left the page an inert shell. readPageConfig never throws:
  missing element, empty blob and bad JSON all fall back to the caller's
  defaults, merged shallowly with null treated as absent.

JsonScriptBlockEscapingTest now pins a stronger invariant than the old flag
scan. One test feeds the emitter a payload that tries to close its own script
element and checks the rendered output; the other asserts no file outside
ConfigIsland emits a config block itself, so a new view cannot quietly
reintroduce the hand-rolled form.
The guide still routed work to src/backend/Views, src/backend/Services and
src/backend/Router, none of which exist — what remains under src/backend is
the REST API and the entry point. It also named getSettingWithDefault(),
Globals::table(), `global $var`, the archivedtexts and textitems2 tables, and
a frontend laid out as pgm.ts / text_events.ts / audio_controller.ts. All of
those are gone; archived texts are a TxArchivedAt column on texts and
textitems2 is word_occurrences.

Corrects the request flow, directory tree, table list and frontend layout to
match, adds the Activity and Book modules, and states the 8.2 docblock floor.

Two additions rather than corrections: the request-context section now says
outright that QueryBuilder reads the user context to scope every query, so a
repository that looks unscoped is filtered and clearing the context is a
security event; and a new Config Islands section documents ConfigIsland and
readPageConfig as the only sanctioned way to move values from PHP to a page.
@HugoFara
HugoFara force-pushed the refactor/config-islands branch from eab56bb to a4240e4 Compare August 30, 2026 22:14
@HugoFara
HugoFara merged commit 27ac193 into develop Aug 30, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant