refactor(ui): put every PHP-to-JS config blob behind one emitter and one reader - #301
Merged
Merged
Conversation
This was referenced Aug 30, 2026
…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
force-pushed
the
refactor/config-islands
branch
from
August 30, 2026 22:14
eab56bb to
a4240e4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack 4 of 6. Base: #300. Two commits: the config-island unification, and
the
CLAUDE.mdcorrection 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) andreadPageConfig/hasPageConfig(TypeScript),and routes all of it through them. Three concrete bugs this fixes:
json_encodewith no escaping flags at all, so avalue containing
</script>would close the element early and the rest of theblob would parse as markup.
preferences.phphand-spliced a pre-JSON-encoded value into a literal{"currentLanguageCode": …}— valid only as long as the controller rememberedto encode it. It now passes the plain string and the controller stops
encoding.
starter_vocab.tsparsed without atry/catch, so a malformed blobthrew during
init()and left the user an inert shell.readPageConfigneverthrows: missing element, empty blob and bad JSON all fall back to the caller's
defaults, merged shallowly with
nulltreated as absent.JsonScriptBlockEscapingTestnow pins a stronger invariant than the old flagscan. 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
ConfigIslandemits a config block, so a new view cannot quietly reintroducethe hand-rolled form.
Docs
CLAUDE.mdwas routing work tosrc/backend/Views,src/backend/Servicesandsrc/backend/Router, none of which exist — what remains undersrc/backendisthe REST API and the entry point. It also named
getSettingWithDefault(),Globals::table(),global $var, thearchivedtextsandtextitems2tables,and a frontend laid out as
pgm.ts/text_events.ts/audio_controller.ts.All gone: archived texts are a
TxArchivedAtcolumn ontexts, andtextitems2isword_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 → #300 → 4 → #5 → #6