Apps: Posts, Pages, Users, User Edit, Plugins and Comments become App Framework apps - #757
Conversation
… Framework apps The six native list windows are now `.os.php` apps under `apps/`, and the three legacy bundles (`src/posts-window`, `src/plugins-window`, `src/comments-window`), the six `includes/*-window/` modules and the three window stylesheets are gone. Every window keeps its frozen id, gate, opt-in setting, URL remap, query-args filters, REST fields and routes, AJAX handlers and JS hook-bus filters; the styling is verbatim, phone layouts included. 35,359 lines of product code become 26,548. The framework absorbed what each window had hand-rolled: an in-process Core REST proxy (`openstation_app_rest()` / `_page()`) so a `data()` serves the exact rows a browser fetched; the `reopen` lifecycle and per-session params, which replace the three shared-store deep-link hand-offs; `App::config( callable )` for per-viewer config; the list-window furniture (`statusControl()`, `pager()`, `mountMenuCheckboxes()`) and the shared `.os-app-list__*` layout with its phone rules. See docs/migration-list-apps.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
@claude were the framework additions needed? Why were those added and what are their purpose? |
|
Claude finished @AllTerrainDeveloper's task in 48s —— View job TaskAnswering: "were the framework additions needed? Why were those added and what are their purpose?"
AnswerYes — each of the five additions is real shared infrastructure that at least two of the six ported apps needed, not speculative abstraction. Breakdown: 1. 2. 3. 4. Bottom line: all five were needed by ≥2 apps each (three of them by a majority or all six), and the framing in the PR body ("the next list window gets it for free") holds up — |
…, cost
Five reviewers, knip and a CSS-usage scan went over the port; this is
everything they found, applied.
Framework: a window's announce is tagged with its id and its own
`watch()` skips the echo (no second round trip after a mutation);
`ctx.windowId`; `ctx.host.toast( { duration } )`;
`createListTableSync()` for the preserved-table dance every list did by
hand; `App::config( callable )` documented as `function ( App )`;
`openstation_app_rest()` reports a single resource as one, sends its
page defaults, and returns the error `code` with
`openstation_app_rest_page_is_out_of_range()`; the pager is an
`<os-select>`; nonce refresh rewrites both levels of a blob; the i18n
map can attribute one source tree to several handles;
`profile.php` retargets a live User Edit window to the viewer.
Apps: the Plugins multisite status literal (`network-active`); the User
Edit personal-options meta registration (an `init` hook added during
`init` never fired); the Users root no longer doubles as the note→post
drop target; per-row COUNTs in `data()` replaced by grouped queries
(Users 60 → 2 per paint, Comments 20 → 1, one size transient for every
plugin); the Categories mind map and the Tags cloud on one term-canvas
base (~700 duplicated lines gone, one CSS block); the Posts cells split
per column family so the Pages bundle stops shipping the pickers; the
profile surface as one companion bundle both the Users and User Edit
windows load; the Plugins parts split at their seams with shared
reviews / action-button / busy modules, inert wp.org HTML parsing and
no duplicate kit components; the Comments rail no longer collapses
after Load more, its composer clears on a conversation change, its
reply gate matches the route; every dead export, config key, filter
and stylesheet rule the tools found removed; tests backfilled.
Two detectors join the repo: `npm run lint:unused` (knip) and
`npm run lint:css:unused` (`bin/unused-css.sh`).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…take the network persona Three multisite-only failures, two causes. `openstation_plugins_window_caps()` is the Plugins window's screen gate: Core's SITE plugins screen offers no install, upload or delete — those live in the network admin — so the map reports them false on a network however capable the viewer is. The admin-ajax half enforced that in its guard; the `delete` action did not, and went straight to Core's REST controller, which lets a super admin through. The window hid a button the server would still have honoured. `mutate()` now applies the same gate to every row mutation it performs. Editing someone else is `manage_network_users` on a network (`map_meta_cap`, `edit_user`), so the User Edit suite's acting user is a super admin there — the persona the window is built for, and the one `usersApp.php` already takes. Its PUT-through-core and destroy-sessions cases were asserting a single-site rule on a network install. Full suite green both ways: 2,925 tests single-site, 2,925 multisite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HdbgKfaTNgh6W3twXHzXA
Plugin Check refuses a wp.org-hosted plugin that looks like it ships a self-hosted updater, and it decides that by scanning raw PHP for the `update_plugins` transient's own set/delete hook names. The window's per-request snapshot listened on that pair purely to drop itself when the transient changed under it, which is the one thing those hooks are NOT for here — but the scanner cannot tell the difference, and the build failed on it. The snapshot now listens where an intra-request rewrite actually comes from: `upgrader_process_complete` and `deleted_plugin`, the pair that already invalidates the size map beside it. The forced refresh drops the snapshot itself, right after it repopulates the transient, so the Refresh button keeps reading its own fresh answer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HdbgKfaTNgh6W3twXHzXA
…f our own The memo the previous commit kept had to be invalidated whenever anything rewrote the transient, and the obvious listeners for that are the two hook names Plugin Check reads as a self-hosted updater. Losing them left the memo stale: a second read inside one request answered from the first, which the icon-resolution test caught the moment it primed a fixture twice. So the memo goes. `get_site_transient()` is already an in-memory read after the first call of a request — core caches the option behind it — so priming once and reading through is the same cost without a coherence problem to solve. The non-persistent group registration moves beside the one memo left, the local-icon probe, whose answer genuinely cannot change inside a request. Full suite green both ways (2,925 single-site, 2,925 multisite) and `wp plugin check` reports no updater finding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HdbgKfaTNgh6W3twXHzXA
…er prewarm
The six ported windows had no instant-open path: the first paint waited
behind the shell overlay for a whole `mount` request, the bundles
downloaded on the click, and the dock's hover prewarm stood down on
every native tile. `App::prefetch()` was the only way to paint early,
and it runs `data()` on every shell boot — wrong for windows whose
`data()` is queries.
Three layers, each usable on its own:
- A client `placeholder` in `defineApp()`: the app's stand-in data
(an empty list, zero counts) painted from the declared state the
moment the window opens, with the new `ctx.loading` set until the
`mount` answer replaces it — one `mounted()`, no second mount. Posts,
Pages, Users and Plugins paint their frame with the table in its
skeleton; Comments paints bare tabs and six ghost rows. Deep links
still wait for `mount`.
- `<link rel="prefetch">` for every deferred native window's bundle,
companion scripts and companion styles once a sync has settled and
the browser is idle — nothing executes, Save-Data and 2G skip it, one
hint per URL — so a first open's `<script>` comes from cache.
- Hover-intent prewarm for native windows: with "Prewarm windows on
hover" on, the dock's dwell on a system tile, an icon launcher or a
remapped menu URL calls the new `wp.os.prewarmWindow( id )`, which
loads the bundles and has the runtime send the window's first `mount`
now (`wp.os.apps.prewarm`), silently, holding the answer for 30 s. The
open takes it instead of fetching. Iframe tiles keep the speculative
window.
`RuntimeHost.fetch` gains `silent`; `nativeWindows.prewarmById` and
`prewarmWindowById` join the facade. Docs: the client-view section of
app-framework.md ("The first paint"), javascript-reference.md (the new
API, `wp.os.apps.prewarm`, the setting's note), api-index.md,
architecture.md, hooks-reference.md, the migration note, and the
Preferences toggle's description.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Summary
Posts, Pages, Users, User Edit, Plugins and Comments are now App Framework apps under
apps/. The three legacy bundles (src/posts-window,src/plugins-window,src/comments-window), the sixincludes/*-window/modules and the three window stylesheets are deleted. Every window keeps its frozen id, title, size, gate, opt-in setting and URL remap; every query-args filter, REST field, REST route, AJAX handler and JS hook-bus filter keeps its name and signature. Styling is verbatim, phone layouts included.The framework grew what the six windows had each hand-rolled, so the next list window gets it for free:
openstation_app_rest()/openstation_app_rest_page()— in-process Core REST with_fieldsand_embed(+code,openstation_app_rest_page_is_out_of_range()), so adata()serves the exact rows a browser fetched and every third-party REST field /rest_post_queryfilter keeps working.reopenlifecycle action + per-session params — the deep-link contract that replaces the three shared-store hand-offs (user-edit-target,post-filter,tab-target). Remaps passparams;openUserEditWindow()moved tosrc/open-targets/user-edit-window.ts.announceis tagged with its window, and that window'swatch()never answers it with a second round trip.App::config( callable )— per-viewer config resolved when the manifest is built.ctx.windowId,ctx.host.toast( { duration } ), and the list-window furniture in@openstation/app:statusControl(),pager(),mountMenuCheckboxes(),createListTableSync(); the shared.os-app-list__*layout (with its@container/ phone rules) inapp-runtime.css.<os-user-profile>as one companion bundle (openstation-user-profile) both the Users and User Edit windows load, instead of a copy compiled into each.A second review pass (five reviewers +
knip+ a CSS-usage scan) fed a cleanup round: the Plugins multisite status literal (network-active), the User Edit personal-options meta registration (aninithook added duringinitnever fired),profile.phpon a live window, the Users root doubling as a note→post drop target, per-row COUNT queries indata()replaced by grouped ones (Users 60 → 2 per paint, Comments 20 → 1), a shared term-canvas base for the Categories mind map and the Tags cloud (~700 duplicated lines gone), the Posts cells split per column family (Pages no longer ships the tag/category pickers), the Plugins parts split at their seams with one reviews / action-button / busy module, wp.org HTML parsed inert, and every dead export, config key and stylesheet rule the tools found removed. Two detectors are now in the repo:npm run lint:unused(knip) andnpm run lint:css:unused(bin/unused-css.sh).The open is preloaded. The old bundles held the shell overlay until their first REST fetch, and the ported apps inherited a worse version of that beat: bundles downloaded on the click, the frame waited for a whole
mountrequest, and the dock's hover prewarm stood down on native tiles.App::prefetch()was the only early-paint path and it runsdata()on every boot — wrong for windows whosedata()is queries. Three layers now, each usable alone:placeholderindefineApp()— the app's stand-in data, painted from the declared state the moment the window opens, with the newctx.loadingset untilmountanswers (onemounted(), no second mount). Posts, Pages, Users and Plugins paint their frame with the table in its skeleton; Comments paints bare tabs and ghost rows. Deep links still wait.<link rel="prefetch">per deferred native window's bundle, companion scripts and styles (nothing executes; Save-Data and 2G skip it; one hint per URL), so a first open's<script>comes from cache.wp.os.prewarmWindow( id ): bundles into the tab, then the runtime sends the window's firstmountsilently (wp.os.apps.prewarm) and holds the answer for 30 s; the open takes it instead of fetching. Iframe tiles keep the speculative window.Lines: old vs new
Old = the deleted files at
trunk(wc -l). New = the six app directories minus*.test.ts, plus the framework's net growth (git diff --numstatagainsttrunk).apps/<dir>, incl. CSS)Every file is under 700 lines. Bundles (minified): posts 225 KB, pages 163 KB, plugins 138 KB (was 203), users 47 KB (was 128), user-edit 7 KB (was 109), user-profile 111 KB (shared), comments 33 KB.
Behaviour notes
os.plugin.changednow carriessource: 'plugins-app'and the changedids; a Pages trash announcespage(waspost).openstation_comments_window_after_bulkalso fires from the Comments app'smoderateaction (same function as the route). The Comments reply gate isedit_posts, as the route always was.openstation_comments_window_reply_editoris gone (the composer is a textarea; the filter never selected anything)._fieldsdrop the fields the apps now compute in grouped queries (all stay registered).desktop-mode-user-edittakes{ userId }(0= the viewer, which is whatprofile.phpsends),desktop-mode-comments{ post },desktop-mode-plugins{ tab }; a live singleton retargets throughreopen.?openstation_force_refresh=1flag is gone (Refresh is thereloadaction); onedm_pwsz_mapsize transient replaces the per-plugin ones.deletedispatch on a network is refused like the admin-ajax half always was —openstation_plugins_window_caps()is the screen gate, and a super admin holdsdelete_plugins. The update transient is read straight from Core (no memo, no*_site_transient_update_pluginshooks — Plugin Check reads those names as a self-hosted updater).wp.os.prewarmWindow( id )(Experimental),wp.os.apps.prewarm( id ),RuntimeHost.fetch( …, { silent } ),ViewContext.loading,defineApp( { placeholder } ).Full list in
docs/migration-list-apps.md; hook docs, JS reference, API index, examples and the REST route index updated.Verification
npm run build,npm run lint(0 errors),npm run typecheck,npm run lint:php— clean.npm run test:js— 471 files, 5,857 tests passed.npm run test:php— full suite green, single-site and multisite;wp plugin check— 0 findings.npm run lint:css:unused— every selector emitted;npm run lint:unused— no dead exports left underapps/.profile.php,user-edit.php?user_id=N,edit-comments.php?p=N,plugin-install.phpdeep links into open and closed windows; the Users Profile tab and the User Edit window sharing the profile bundle; a multisite install for the network-active plugin rows.🤖 Generated with Claude Code