@@ -110,9 +110,26 @@ Track the mobile-critical flows, not the file count.
110110rendered by Alpine.js." So Phase 1 is ** cutting the server-shell umbilical, not
111111converting pages** .
112112
113- ** Status (re-audited 2026-08-07): complete.** Every mobile-critical surface is
114- shell-free, and the legacy-fragment cleanup that was the last open item
115- ([ #266 ] ( https://github.com/HugoFara/lwt/issues/266 ) ) has shipped.
113+ ** Status (re-audited 2026-08-07): complete * as scoped* .** Every mobile-critical
114+ surface is shell-free, and the legacy-fragment cleanup — the last open item in
115+ this phase — has shipped.
116+
117+ ** This does not close [ #266 ] ( https://github.com/HugoFara/lwt/issues/266 ) .** That
118+ issue asks for PHP to emit "only data for hydration", which is a wider bar than
119+ Phase 1's. The * data* half is met (config blobs carry boot parameters —
120+ ` ['textId' => …, 'langId' => …] ` — and everything else is fetched from
121+ ` /api/v1 ` ), but PHP still owns the markup half (audited 2026-08-08):
122+
123+ | PHP still owns | Where | Scale |
124+ | --- | --- | --- |
125+ | DOM scaffold | ` x-data ` in views | 53 views |
126+ | Which JS modules load | ` PageLayoutHelper::getRequiredModules() ` → ` <meta name="lwt-modules"> ` | every page |
127+ | i18n injection | ` PageLayoutHelper::buildI18nScript() ` | every page |
128+ | Icons | ` IconHelper::render() ` → server-rendered ` <i data-lucide> ` | throughout |
129+
130+ The bundled client works around all four with a build-time transpiler
131+ (` build/php-view-prerender.mjs ` ) rather than removing them — see Phase 2. #266
132+ stays open against that markup half; it is not a Phase 1 deliverable.
116133
117134- [x] ** (Phase 0 gate) Injectable API base URL.** Done in Phase 0 — same seam.
118135 ` @shared/api/client ` resolves an injectable ** absolute** server root and
@@ -203,10 +220,51 @@ shell-free, and the legacy-fragment cleanup that was the last open item
203220 keys were decorative, and the bare ` books ` entry allowed only GET and
204221 POST. ` EndpointMethodReachabilityTest ` now asserts real request shapes
205222 resolve, so the next drift fails a test instead of a feature.
223+ - [x] ** Form POSTs — the write half, for the surfaces above**
224+ ([ #262 ] ( https://github.com/HugoFara/lwt/issues/262 ) ). Those surfaces
225+ * read* from ` /api/v1 ` ; several still * wrote* by posting a form to the page
226+ origin, which a client pointed at a different server cannot do.
227+ Converted: tags, user profile/password/preferences,
228+ standalone term creation, the ** text editor**
229+ (` POST /api/v1/texts ` , ` PUT /api/v1/texts/{id} ` — neither endpoint
230+ existed; ` TextsApi.create() ` had been calling a 404 since it was written),
231+ and the ** feed forms** (` POST ` /` PUT /api/v1/feeds ` ).
232+
233+ Two mass-assignment holes closed on the way: `texts.TxLgID` and
234+ `news_feeds.NfLgID` are client-supplied references into `languages`, and
235+ a foreign key proves the row exists, not that the caller owns it. Both
236+ form handlers passed the submitted value straight to the facade; the API
237+ endpoints check ownership, and the form-POST routes that skipped it are
238+ retired. Multi-user installs only.
239+
240+ Fourteen views still post, down from sixteen: admin (4), the imports
241+ (`import_epub_form`, `upload_form`, `bulk_translate_form`), the feed
242+ wizard's navigation steps (3), the archived-text and check forms, the
243+ Microsoft link confirmation, and `edit_form` for its Check button alone.
244+ Every one of those is a surface this phase deliberately leaves
245+ server-rendered (below), so what remains of #262 is scoped by that list
246+ rather than by view count.
206247
207248** Out of Phase 1** (leave server-rendered, fine in a WebView online): imports
208249(file/web/youtube/whisper), admin/settings, language config, feeds.
209250
251+ Two exceptions inside that list, both because the work was a fix rather than a
252+ conversion:
253+
254+ - ** The feed create/edit forms** went to ` /api/v1 ` even though feeds are out of
255+ scope, because tracing them turned up that ** finishing the RSS wizard had
256+ saved nothing since 2026-08-08** (` 1e2216bb6 ` ). Step 4 posted to
257+ ` /feeds/edit ` , and that commit made the route a 302 to the manager SPA when
258+ the duplicated feeds list was retired; a redirect discards the body. Present
259+ in 3.4.0 and 3.4.1. Verified both ways against a live BBC RSS feed: on the
260+ parent commit the walk ends with ` news_feeds ` empty, on the fix it writes the
261+ row. The wizard's * navigation* steps (2 and 3) still post to ` /feeds/wizard `
262+ — they drive a server-side session state machine that renders HTML previews
263+ of the fetched page, and moving that to the client is its own project.
264+ - ** The text editor's "Check" button** still posts. It asks for a
265+ server-rendered parsing report rather than saving, so it names its own target
266+ with ` formaction ` while the form itself carries no action.
267+
210268** Definition of done per surface:** renders entirely from ` /api/v1 ` JSON, no
211269server-rendered partial carrying data, works against a configurable API base URL.
212270
0 commit comments