Enhancement: Project Front Door#486
Open
baltinerdist wants to merge 106 commits into
Open
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oller (class.Controller.php normalized tabs->PSR-12 spaces per boy-scout policy) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t.tpl Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, events, mosaic, kingdoms, CTA) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…DB clear, responsive, font load) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cache-bust frontdoor.css Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bove) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nderer, Page routing, CMS-backed front door
- db-migrations: all ork_cms_* tables + home-page seed (imports Model_FrontDoor defaults)
- class.CmsPage + model: page/block read+CRUD (Model_FrontDoor block shape)
- render_blocks.tpl: shared block-render loop; _index.tpl now includes it
- controller.Page + Page_view.tpl: published pages at Page/view/{slug}
- Controller::index() renders home from the store, falling back to Model_FrontDoor defaults
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…asAuthority bridge, grant CRUD) Super-admin = canonical HasAuthority(uid, AUTH_ADMIN, 0, AUTH_ADMIN); kingdom/park scopes bridge to officer HasAuthority. Verified 20/20 assertions against live DB. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ditor UI (TinyMCE + media picker, draft/publish/preview) - class.CmsMedia: base64->GD upload + thumbnail + audit; ork_cms_media library - class.CmsSanitizer: strict DOMDocument allowlist (XSS-safe; HTML Purifier not cleanly vendorable in-env) - controller.Cms (page list/edit/preview) + controller.CmsAjax (savepage/publish/unpublish/delete/mediaupload/medialist), all CmsCan-gated; CmsAjax token-skip - Cms_index + Cms_edit (TinyMCE@7.6 CDN, media picker modal, autosave, in-page confirm) + cms-admin.css (dark mode + responsive) - CmsPage gains GetPage/UpdatePage/SetStatus/DeletePage - Fix: Cms/index func_num_args dispatch bug (was rendering empty) - Verified: list, editor, sanitized save (script stripped), media upload+thumb, publish/preview, auth gate Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rms + draft preview - New self-contained block partials (own scoped style/JS, dark-mode): rich_text, heading, divider, spacer, quote, accordion, table, image, gallery (lightbox), video_embed (sanitized id, youtube-nocookie), file_download (url-based), columns (recursive via render_blocks), raw_html (sanitized passthrough) - controller.Cms: page-type starter presets (composed/article/media/resource/blog_index/dynamic) - Cms_edit: friendly forms for gallery/video/file_download/accordion/quote/heading + preset seeding - Cms_preview.tpl: draft preview with Unpublished banner (was a dangling reference) - Verified: all block types render incl. columns recursion; preview works; no fatals Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed block, admin post management
- class.CmsPost: posts + tag upsert/dedup; body via shared block store (owner_type='post')
- controller.Blog: index (paginated, ?tag=), post/{slug} entry, rss (RSS 2.0)
- Blog_index/Blog_post templates + blog_feed dynamic block partial
- Admin: Cms/posts list + Cms/editpost + CmsAjax savepost/publishpost/unpublishpost/deletepost
- DRY: factored the block-body editor out of Cms_edit.tpl into shared cms/_block_editor.tpl
(used by both page and post editors); page editing regression-verified intact
- Verified: post lifecycle, index/entry/RSS/tag-filter render, admin editors, no fatals
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g nav from store - class.CmsNav: menu tree CRUD, reorder, href resolution (page/post/url/dynamic), child cascade on delete - controller.Cms nav() + CmsAjax savenavitem/deletenavitem/reordernav (nav.manage gated) - Cms_nav.tpl manager UI (link picker: page/post/url/route, parent dropdown, reorder) + Navigation tab - marketing_nav.tpl now renders the 'marketing' menu from ork_cms_nav_item, falling back to authored items - seed script imports current nav into the store (idempotent) - Verified: seed (18 items), home renders nav from store, edit->live-render->delete loop, no fatals Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ity / super-admin) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e refactor - Security: sanitizer rejects //, unicode-blob:, javascript:/data:; IsSafeUrl made public; nav-item URLs validated on save; nested body/html block fields recursively sanitized - Data integrity: CreatePage/CreatePost read back by unique tuple (no stale lastInsertId on dup slug); _upsertTag always reads back by slug; ReplaceBlocks wrapped in a transaction - Stability: session-token check no longer logs users out on a transient DB error; is_object() guards before authorization derefs - Perf: CanManageCms single capability probe; _hasAnyCmsCapability/_capFlags resolve grants once; ListPosts batches tag fetch (N+1 -> 1); Nav Reorder transaction; RSS 300s cache - Refactor: new CmsBase extracts _firstRow/_eachRow/_normalizeScopeType (DRY across 5 libs) - Dropped unreliable Size() guard on the home-kingdom lookup; _safeUnlink separator-anchored Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- cms-admin.css: gold/navy accents (buttons/focus/active-tab/selection), MedievalSharp titles, masthead + themed empty-state styles; harden .cms-btn-primary vs <a> link-color inheritance - Fix blank 'New Post' button (was <a>, now <button>); themed empty states with inline CTA - Remove redundant GLOBAL scope column (v2 is global-only) - Nav: drop native title= (house rule -> data-tip), link-type icons instead of URL chip, suppress bare '#' - Consistent 'The Scriptorium' masthead across Pages/Posts/Navigation Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cate/icons - Declarative BLOCK_SCHEMA registry + generic form renderer: friendly forms for all 12 previously-JSON block types (steps/photo_mosaic/table/divider/spacer/raw_html/marketing_nav...) - Dynamic blocks (member_bar/kingdoms_teaser/events_feed/blog_feed) show info cards, not empty JSON - marketing_nav form + 'managed in Navigation tab' note; columns kept as labeled 'advanced' JSON - Drag-and-drop reorder (wired the existing .cms-drag CSS), insert-between, duplicate-block - Block-type icons + searchable/grouped Add-block chooser w/ 'coming soon' tiles; ORK empty-state copy - _blockCatalog gains per-type icon + description; _starter defaults cover all friendly types Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…w overflow - Pages & Posts tables enhanced with DataTables 1.13.8 (reusing the app's recs convention): client sort, pagination (25), built-in search; default sort Updated/Date desc; dark-mode themed - Replaced the redundant Pages search+Filter toolbar with a client-side status filter - Bulk select (checkbox column + select-all) + bulk Publish/Unpublish/Delete looping existing CmsAjax endpoints; bulk delete via in-page confirm modal - Collapsed row actions to Edit + '...' overflow menu (fixed-position to escape table overflow) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…orkflow - CMS shell: persistent gold-accented left rail (Dashboard/Pages/Posts/Media/Navigation + 'View live site') + masthead/breadcrumbs; list pages wired into it (tabs retired) - Dashboard landing (Cms/dashboard): greeting, Quick Create, at-a-glance stats, Continue Editing - Media destination (Cms/media): library grid + upload (reuses CmsAjax/mediaupload + medialist) - Editor: sticky action bar (Status/Save/Publish/Preview), in-context preview pane (iframe + device toggle, refresh on save), breadcrumbs, 'editing the Front Door' banner for system pages - User-menu 'Manage Site Pages' -> Cms/dashboard Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…odern CMS Per feedback: ORK flavor = contextual (terminology/brand), not LARP cosplay. Dropped MedievalSharp font, 'Scriptorium' naming/wordmark, heraldic glyphs (fleur-de-lis/quill), and 'scribe/scroll/tale/paths' copy → plain labels + neutral icons. Kept all structure (shell/dashboard/DataTables/schema forms/in-context preview) and a restrained navy+gold brand accent. Also fixed FA6-only icon names (the app ships FontAwesome 5.8.2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bugs: Media drop-zone collapse (display:block) + stray diamond glyph -> fa-images + library-first layout; duplicate 'Rich Text (legacy)' removed from inserter (addable flag); draft post preview 404 -> new Cms/previewpost route. De-theme leftovers: dashboard greeting (Good morrow/midnight oil) + 'penned/compose/pen' copy -> plain. UX: Posts status filter (parity w/ Pages), Drafts dashboard tile now links, informative masthead subtitles, 'Add cta'->'Add CTA', nav arrow-disable /parent-self-exclude/reorder-debounce, type-key labels, breadcrumb root 'Dashboard', autosave failure keeps 'Unsaved changes'. A11y: aria-labels on toolbar/enable toggle, role=status/aria-live toasts, rail+button :focus-visible, modal focus-trap, scope=col. summarize() precedence fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rd.com content Reproducible seed creating four published CMS pages that showcase the block system — hero, rich_text (real mission copy), card_grid, quote, steps, accordion, gallery, photo_mosaic, cta_band. Idempotent (delete+recreate by slug); host-agnostic relative asset/UIR paths so it works from the CLI bootstrap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EnsureSite now seeds a full starter site on FIRST creation only (guarded behind the idempotency early-return + CreatePage UNIQUE self-guard + nav ListItems count): Home (welcome + intro + kingdom_events, is_system) plus About/History, Our Parks (kingdom_parks), Officers (kingdom_officers + staff_roster), Documents (file_download), a scoped 'marketing' nav menu linking them, and home_page_id → the seeded home. All draft, in the site's own scope, fully editable — a seed, not a cage. Block field shapes verified against each renderer partial. Discovery: Directory prefetches one published-site slug map (PublishedSlugMapByScope — single query, no N+1) and shows a "Visit site" link (data-tip, FA5, dark-mode) on each kingdom/principality with a published site. Review fix: seed a welcome rich_text instead of hero_carousel (that block bakes in a global stats ticker — cut per spec — and emitted an empty-src img). Updated CmsSite unit queue for the post-seed re-read; 40 assertions pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Found by seeding + publishing a real kingdom site and rendering /k/{slug}:
- UpdateSite silently failed when saving slug alongside other fields:
ValidateSlug() calls $DB->Clear() internally, wiping the site_name bind
set earlier, so the UPDATE ran with an unbound :site_name placeholder and
the whole statement failed (name + slug both lost). Now gather all binds
locally and apply them immediately before Execute — no intervening Clear.
- Seed pages were status='draft', so a freshly PUBLISHED site showed
"being built" with dead nav links (public renderer only shows published
pages). Seed pages published (+ published_at); the site-level status is
the real go-live gate, nothing is public until the SITE is published.
- Seeded About page used type='about', which isn't in the ork_cms_page
ENUM → truncated to ''. Use 'article'.
- Org site returned HTTP 500: default.theme's Era Phoenice popover calls
EraPhoenice::format($_epToday), but Controller_Site doesn't set $_epToday.
Gate the popover (global-only chrome) on empty($IsOrgSite).
Verified: published site renders home + nav + tie-back (200); draft →
coming-soon with no content leak; unknown slug → 404; Directory shows the
"Visit site" link. CmsSite unit suite (40 assertions) passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CMS admin built PUBLIC links for the global front door only, so a
kingdom-scoped page linked to Page/view/{slug} (a global lookup) → 404.
Fix the live-URL generation to target the org site:
- Pages list per-row link → Site/view/{siteSlug} (home) or
Site/page/{siteSlug}/{pageSlug} (others), built in the controller
(_pageLiveHref) and passed as each row's live_href.
- "View live site" (dashboard card + rail) → the org site home
(Site/view/{siteSlug}) via SiteLiveUrl, set in _applyScopeData.
Global scope keeps the legacy Page/view route unchanged. Site row is
looked up once per request and memoized.
Verified: kingdom-scoped pages now generate Site/... URLs that all resolve
200; the old global Page/view/{slug} was the source of the 404.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An officer building a site could not see it: Controller_Site 404'd every page on an unpublished (unbuilt/draft) site, even for the owner, so the admin's live links led to "Page not found" and the seeded content looked missing (it wasn't — the pages are published and full of blocks; the SITE just wasn't published yet). Now an authorized officer (HasAuthority AUTH_EDIT over the site's scope, super-admins included) PREVIEWS their unpublished site — the render path runs, draft pages/posts included, with a "Draft preview — not published" banner and no_index. The PUBLIC still gets the gated states (unbuilt → bare 404, draft → coming soon); unauthenticated requests are unchanged. Verified: preview gate opens for an admin over the org; public gate still 404s unauthenticated; starter pages confirmed to carry blocks and render. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Authorized officers (AUTH_EDIT over the org) now get the CMS edit FAB on
their org site — on published pages AND unpublished/draft previews alike —
linking into the SCOPED CMS admin (Cms/edit|editpost|posts + &scope=k:{id}),
so edits land in the org's own content. Home/page → edit FAB; post → edit +
new-post FAB; blog index → manage + new-post FAB. Reuses the same
HasAuthority gate as preview (memoized); the public sees no FAB (verified).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kingdom_parks block: - "Sort order" option — park name (A–Z), city→name, or state→city→name. - "Display heraldry" toggle — park crest on each card (has_heraldry-gated, shield fallback), plus the park's rank/title badge (Barony/Shire/…) for richer, more dynamic cards. New kingdom_parks_map block: - Interactive Google map of the kingdom's active parks with a click-to-open detail sidebar (heraldry, name, city/state, directions, description, profile link) — the Amtgard Atlas map/sidebar pattern, scoped to one kingdom via Map::GetParkLocations. Self-sourcing, instance-safe (unique id + one-time Maps loader/callback queue), fitBounds to the kingdom's parks, Parsedown-safe directions/description, dark-mode + responsive. - Registered in the block catalog, allow-lists (composed/about/dynamic), starter defaults, CmsAjax type allowlist, and editor schema. Starter template: Our Parks page now seeds heading → parks map → parks list (map above list), list defaulting to city sort + heraldry on. Verified in-browser: list renders with crests, rank badges, and city sort in dark mode. The map matches the Atlas exactly; tiles are blank only on localhost (the Google key is referer-restricted to prod — the Atlas is blank locally too) and will render in production. Fix: fall back to the Atlas key when GOOGLE_MAPS_API_KEY is defined-but-empty. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lapsible groups - Add-block chooser now uses a wide modal with responsive 3-column tiles (auto-fill minmax) — far less vertical scrolling. - Each block tile shows its one-line description (already in the catalog) so officers know what a block is without guessing. - Block groups are collapsible: the section header is a button with a rotating caret + a count badge; collapsed state persists across re-renders and is ignored while searching (matches stay visible). - Moved Member Bar from Layout to the Dynamic group (it renders live data). Verified in-browser in the scoped editor: 3 columns, descriptions, collapse, and Member Bar under Dynamic — dark-mode clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New super-admin-only Cms/sites page: a cross-org directory of every started site. Amtgard International (the global front door) is pinned at the top (always live; Manage → global CMS dashboard, Visit → site root), then a Kingdoms section and a Parks section list each org's site with its status badge (Published/Draft/Unbuilt), published/total page + post counts, and last-updated. - Manage → the org's scoped CMS dashboard; Visit → its public /k|/p site. - Publish/Unpublish inline via the existing CSRF-protected CmsAjax endpoints (super-admin passes their scope re-validation; badge + button toggle live). - "New site" provisioning: pick a kingdom (name-sorted; ones with a site flagged) → opens its scoped dashboard, which auto-creates + seeds it. - Hard gate: CmsAuth::IsSuperAdmin (all-zero AUTH_ADMIN) bounces non-supers via _denyRedirect BEFORE any data loads — the cross-org list can't leak to a tenant officer. Rail "All sites" entry shown only when caps.super. New CmsSite::ListAllSites (one query: scope_type-gated kingdom/park name join, collation-safe COALESCE, per-scope correlated page/post counts) + GlobalPageCounts for the front-door card; thin model pass-throughs. Verified in-browser as super-admin: overview renders, publish round-trip toggles Unbuilt→Published live, dark-mode clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Multi-lens review pass over the CMS module. Fixes: - IsSafeUrl: percent-decode before the deny/scheme checks so encoded schemes (%6aavascript:) and encoded controls (java%09script:) can't bypass the URL guard (stored/DOM XSS). Tested against a 17-case battery. - kingdom_parks_map: htmlspecialchars the park name before JSON→innerHTML. - CmsMedia::ListMedia: distinct :search placeholders (a reused named param binds only once under emulated prepares → search matched filename only). - DeletePage: read-back + ROLLBACK before COMMIT (was committing unconditionally → block-less zombie page on a silent DELETE failure). - DeletePost: wrap the three DELETEs in a transaction + read-back rollback. - DeletePage/DeletePost: opt-in scope-ownership guard (mirrors CmsNav::DeleteItem), threaded from the delete controllers — defense in depth. - CmsAuth::RevokeRole: fail-closed authorization (was opt-in/bypassable), matching GrantRole. - kingdom_officers: guard the Heraldry constructor (uncaught throw 500'd page). - render_blocks: try/catch each block include so one bad block can't blank the whole page. - rich_text.tpl: collapse to a thin alias include of richtext.tpl (dedup). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ation) The org-site render fataled inside default.theme's logged-in-only global chrome (the What's New + email-prompt modals, which use base-Controller data $DB/session/release constants that Controller_Site never sets), truncating the page mid-render BEFORE the editor FAB block. So a logged-in officer saw no edit FAB on their site (unauthenticated visitors were unaffected — that chrome is gated on the session token). Gate that global chrome on empty($IsOrgSite) — the same fix already applied to the Era Phoenice popover — so the org-site render reaches the FAB. The global front door / CMS pages are unaffected (IsOrgSite unset → chrome still renders). Verified in-browser: FAB now shows on the org home and opens the scoped editor; the global front door still renders fully. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses the findings from the 11-expert CMS review across five sharded waves (file-disjoint lanes, zero write-collisions). 46 files + 2 migrations. Security & data integrity - Sanitize at the authoritative choke point (CmsPage::ReplaceBlocks), not write-path-only; correct the misleading "sanitized at render" comments. - Image decompression-bomb guard (reject >40MP before GD decode). - InnoDB foreign keys + reference cleanup on delete; soft-delete (deleted_at) with Trash/Restore for pages/posts/media; append-only audit log; revisions with restore; optimistic-concurrency guard (base_version) on block saves; stable block-id upsert; concurrent same-slug create no longer clobbers. Product & content - Real scheduled publishing (published_at gate + lazy promote). - Page hierarchy (parent_id) with nested paths + breadcrumbs. - Reserved page-slug guard + 301 redirect table on slug rename. - /p/ park namespace distinct from /k/ (canonicalizing 301). - Per-page canonical + Open Graph (per-kingdom branding, hero og:image). - Alt-text authoring end-to-end (+ decorative option); PII consent on staff roster; neutral byline fallback; author reassignment on revoke. - 2-level nav; validated tag picker; permission page instead of login bounce. Front-end & a11y - Dark-mode section bands fixed (inline #fff removed); single-h1 outline; keyboard focus rings; carousel pause/play + reduced-motion; gallery focus-trap; surgical block-editor DOM ops (no TinyMCE teardown on reorder); loud/recoverable invalid-JSON errors; runtime theme re-skin. Performance - Theme-CSS cache; kingdom_officers N+1 (heraldry) eliminated; per-block inline-CSS dedupe for heaviest blocks; Google Maps per-env key + lazy-load with graceful fallback. Deploy notes - Run both migrations in filename order (integrity-safety-net, then routing-hierarchy) with the MariaDB client; CMS tables must be InnoDB. - Set a per-env GOOGLE_MAPS_API_KEY or the parks map shows its fallback. Known follow-ups (tracked, non-blocking): full block-fragment/nginx cache, gallery inline-CSS dedupe, vendored TinyMCE self-host bundle, editor-engine JS extraction, posts permanent-purge endpoint, Page/view canonical/OG. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShyA7SCTysgREoQPBoxs1Q
Applies the high-value findings from a 58-reviewer multi-lens polish of the CMS module (implementer→critic verified; block-save guard E2E-tested). Security - Close IDOR on CmsAjax restoremedia/purgemedia/mediaupdate (scope-ownership guards) + CmsSite.logo_media_id cross-scope check + lib-level guards on CmsMedia mutations (defense in depth). - Scope the block-editor persona search (was leaking banned/inactive players cross-org); validate kingdoms_teaser more_href via IsSafeUrl (javascript: URI) and RecordRedirect target (open redirect); add a CLI-only guard to all web-reachable db-migrations/*.php seed scripts. Stability - update_page/update_post now _fail on a false save instead of reporting success; UpdatePage slug-rename does a dup pre-check + write-verify (no bogus 301 on a silently-dropped rename); ReplaceBlocks detects a silently-dropped per-block UPDATE and rolls back; SetTags/SetActive/media-upload read back to verify the write landed. Quality / perf / a11y - Memoize CmsCan's officer scope-bridge; de-duplicate list-row mapping and block-type catalog; GetPageAncestors one round-trip per level; org-header nav ARIA state; steps responsive breakpoint; assorted dead-code and doc fixes. Deferred findings (design decisions / larger refactors) are tracked in docs/TICKETS-cms-polish.md — notably the soft-delete vs slug-uniqueness regression, the CmsPage/CmsPost + editor-template de-duplication, and the public-render N+1 / fragment-cache work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShyA7SCTysgREoQPBoxs1Q
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShyA7SCTysgREoQPBoxs1Q
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShyA7SCTysgREoQPBoxs1Q
…ges) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShyA7SCTysgREoQPBoxs1Q
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShyA7SCTysgREoQPBoxs1Q
…ndering raw URL) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShyA7SCTysgREoQPBoxs1Q
…ewrite (idempotency) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShyA7SCTysgREoQPBoxs1Q
…egenerable) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShyA7SCTysgREoQPBoxs1Q
…ff monograms - Fix dead home hero/CTA '#' links (model.FrontDoor + home-relink migration → Atlas/CMS pages) - Nav: relabel 'AI Programs'→'Programs', add Official Resources dropdown children (nav-polish migration) - Wayfinding: breadcrumbs (Page_view + Controller_Page::view GetPageAncestors) + IsCmsPage brand-font flag - Staff roster: initials monogram avatars instead of gray silhouettes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShyA7SCTysgREoQPBoxs1Q
…nd bands, external links - richtext: constrained body always centered (fixes text pinned to left ~45%) - card_grid: empty/'#'-href cards render non-link 'Coming soon' (no dead links); scrim/contrast - hero_carousel: suppress stat ticker on non-home pages (no 0/0/0) - staff roster CSS: monogram avatars, equal heights, centered solo card - decouple --navy from dark-mode --fd-primary (brand bands stay navy, not periwinkle) - external-link arrow + left-aligned body lists; mobile member-bar hidden - active-nav state; breadcrumb styles; MedievalSharp loads on CMS pages Carries related pre-existing front-door WIP in these files per repo owner direction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShyA7SCTysgREoQPBoxs1Q
Unlinked roster cards (board/team leads/autocrat) open a self-contained, brand-styled dialog showing the person's avatar, name, role, and the FULL (untruncated) bio. Keyboard-accessible (Enter/Space open; ESC/backdrop/x close; focus return + trap), no native dialogs. Single shared modal per page (global guard) populated from the clicked card's data-* attributes. Linked cards (member profiles) keep navigating. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShyA7SCTysgREoQPBoxs1Q
Accumulated uncommitted CMS/front-door implementation across the branch: blog + RSS, media, view analytics (CmsView), multisite/site routing, sanitizer, nav, page/post libs, front-door block templates, CMS admin shell/editor, migrations, docs, and nginx rewrites. Excludes (intentionally NOT committed): system/lib/ork3/class.Authorization.php (carries a local dev auth-bypass hack), scratchpad/ (working files), and tests/scroll/ (unrelated scroll-generator snapshots). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShyA7SCTysgREoQPBoxs1Q
…ings) Applies the problem-kind findings from a multi-lens distributed review of the CMS/front-door platform, verified by an adversarial critic pass per shard. Highlights: - Persistence integrity: block-save verify compares all columns + order-insensitive JSON; zero-row/trashed UpdatePage/UpdatePost now fail; dup-guard distinguishes collision vs dropped write; RestoreRevision hard-fails on corrupt snapshot. - Routing: slug/parent redirects + prefix fallback, post-rename 301s, leaf-only publish gating, cycle/cross-scope parent rejection, transliterated slugs. - Caching/perf: GhettoCache for home page+blocks, site-by-slug, gated _promoteScheduled, in-memory admin path map (no N+1), clamped/cached teaser. - Multi-site scope: fail-closed scope normalization, global grants keyed at 0, RevokeRole capability probe, scoped personlookup, nav resolves only live targets. - Security: sanitizer size/depth budget, columns recursion depth cap, media where-used scan incl. embedded imgs, copyDoc MIME allowlist, CSRF on preview publish. - UX/a11y: carousel slide inerting, dark-mode fixes, keyboard park-map list, consistent staff cards, sticky editbar offset, autosave-live warning. - Migrations: relative UIR seeds, full demo chain, guarded FK adds, idempotent content seeds, hard-required sanitizer, new about-type enum migration. - Model wrappers forward actor/publishedAt/scope; content audit + meta correctness. Deferred (own focused passes): CmsAjax pure/wrapper split, tpl->lib orchestration move, full draft workspace (min-viable autosave guard shipped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gFvamqXW6vPKMx4mQiL5D
Implements 24 selected enhancement findings from the distributed review, verified by an adversarial critic pass per shard + passing test suites. - Front-door perf/a11y: content-visibility on block sections, block surfaces routed through --fd-* theme tokens (zero visual change), dark-mode .fd-path, carousel ARIA region + live announcements, per-block <style> dedup, gallery lightbox scroll-lock + inert background, preview-only block-failure placeholder. - CMS lib: shared CmsPage::HTML_FIELDS/URL_FIELDS constants, content-mutation audit trail, PurgeTrashed + orphan-block sweep + unused-tag prune, block-only saves bump owner updated_at/updated_by, DB-enforced one-active-theme-per-scope. - Controllers: memoized block catalog + batched bulk media delete, centralized page-type enum, deferred per-view analytics write off the render critical path, clearrendercache + runmaintenance actions, capability-named denials. - Editors: shared cms-admin.js (toast/modal/CSRF-post, one source), view counts on Pages/Posts + top-content dashboard panel, window.CMS_CAPS, no raw type slug. - Multi-site: single grant-driven edit-FAB helper (Page/Blog/Site), sitemap.xml. - Tests: CmsSanitizer XSS regression, CmsThemeTokens math, block-field allowlist contract (all passing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gFvamqXW6vPKMx4mQiL5D
Mobile-review and a11y remediation across the front-door blocks, the CMS admin shell, and the shared theme chrome. Mobile layout - CTA rows (cta_band, hero_carousel) wrap via flex/gap instead of margin-left on the nth button, so buttons stack instead of overflowing. - steps grid steps 4-col -> 2-col at 760px -> 1-col at 480px. - Path grid collapses to one column at 520px; heading block font sizes step down at 680px. - Carousel dots + play/pause lift above the 2x2 stat ticker on phones. - Mosaic tiles get a uniform 200px height (!important beats the inline height:100%) with object-fit:cover. - Member bar actually hides on mobile (!important beats inline display:flex). - Long org names ellipsize in the org header instead of shoving the hamburger off-screen. - raw_html / rich-text tables and <pre> scroll inside their own container rather than forcing horizontal page scroll. - CMS nav-manager rows wrap so action buttons keep a 40px touch target; block-editor icon buttons enlarge at 680px. - Recovery-email input goes to 16px to stop iOS zoom-on-focus. Contrast (WCAG AA) - #b8860b (2.9:1 on white) darkened to #8a6608 / #8a5d00 / #7a5c00 across kickers, nav hover/active, officer roles, blog cards and the RSS link. - Muted greys #8a93ab / #8a97ad lifted to #5a648a / #5b6675 in file_download and kingdom_parks_map, with dark-mode overrides. - Dark mode added for the recovery-email modal and the parks-map empty states; --ork-warn defined globally so Cms_media's "In use" label is no longer an undefined token. Accessibility - Icon-only controls get aria-label + aria-hidden on the glyph across Cms_edit / Cms_index / Cms_nav / Cms_media and the theme nav. - Search toggle becomes a real <button>; maintenance banner becomes a button with aria-expanded. - Focus trap + focus restore for CMS admin modals; background inert for the staff-roster contact modal. - Media tiles are keyboard-operable (role=button, tabindex, Enter/Space) with a focus-visible ring. - Kingdom profile tabs get role=tablist/tab/tabpanel + aria-selected; title= swapped for the project data-tip pattern. - Rail and recent-action labels are visually hidden rather than display:none, keeping the accessible name. - Directory section titles promoted from <span> to <h2> (with the global heading-pill reset). - Scrollable tables get tabindex=0 + role=region so keyboard users can reach them; decorative nav carets marked aria-hidden. Other - Carousel supports touch swipe (passive listeners, horizontal-only). - CMS/blog pages suppress the stacked app nav via body.cms-page. - Page_view breadcrumb "Home" uses UIR instead of a hardcoded path. - photo_mosaic surfaces an empty-state hint in the CMS preview. - events_feed date and the render_blocks failure placeholder now use the themed .fd-ev-date / .fd-block-error classes instead of inline styles, so their dark-mode overrides actually apply. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019PrAgxszhNEo5yiR1urCw6
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.
Overview
Introduces a new public "front door" landing page for the ORK, plus a v2 content-management system (CMS) that powers it and a family of content pages and a blog.
Front Door
Directory/index(linked from the nav).Model_FrontDoor→frontdoor/render_blocks.tpl→frontdoor/blocks/{type}.tpl).CMS (v2)
Cms/index): page & post management, drag-orderable block editor with TinyMCE, media library + image picker, draft / publish / preview / autosave.CmsCan()with a bridge to the existingHasAuthorityso kingdom/park officers gain rights when scoped pages arrive. A gated "Manage Site Pages" entry appears in the user drop-down.scope_type/scope_id— global now; kingdom/park drop-in later with no migration).Data / migrations
ork_cms_*tables (db-migrations/2026-06-23-cms-foundation.sql) + idempotent seed scripts (home page, marketing nav).assets/cms-media/dir (runtime uploads gitignored;.gitkeeptracked).Quality
Built and verified phase-by-phase, then put through a multi-lens polish pass (security, data-integrity, stability, performance, conventions) with implementer→critic review:
lastInsertIdguard via read-back;ReplaceBlocks/Reordertransactions).CmsBaserefactor across the CMS libs.Specs:
docs/superpowers/specs/2026-06-22-amtgard-front-door-design.md,docs/superpowers/specs/2026-06-23-amtgard-cms-design.md.Update — Theming & Multi-Site (added since this PR was opened)
CMS Theme Engine
Token-based theming for the public CMS — admins restyle colors, typography, and shape/density from curated design tokens (plus an advanced raw-token panel), no CSS. Values are stored per scope and injected as a
:root { --token: value }block at render; front-door stylesheets referencevar(--token). Dark mode is auto-derived from the single palette (WCAG-contrast-checked). Live editor with preview + light/dark toggle. No free-form CSS (values only flow into named custom properties).CMS Multi-Site — per-kingdom public websites
Cashes in the scope-ready data model: each kingdom gets its own standalone public website at
/k/{slug}, authored with the same block builder.Controller_Site): own header/logo, scoped nav, own theme, own footer with a subtle "Part of the Amtgard ORK ↗" tie-back. Per-org theme injected from the site's own scope.?scope=selector (re-checked server-side viaHasAuthority— never trusted). Officers self-serve edit; monarch/regent (AUTH_ADMIN) publishes. IDOR guard on every mutation. Unpublished sites 404 to the public but preview for authorized officers, who also get the edit/new-post FAB.Cms/sites, ORK super-admins): Amtgard International pinned on top, then every started kingdom/park site — status badge, page/post counts, drill-in to the scoped CMS admin, inline publish/unpublish, and new-site provisioning.Data model is park-ready (
/p/{slug}deferred). Spec:docs/superpowers/specs/2026-07-01-cms-multi-site-kingdom-sites-design.md.Latest CMS-module polish
Multi-lens review (72 sharded reviewers) of the CMS module, verified/fixed:
IsSafeUrlpercent-encode XSS bypass closed (%6aavascript:/java%09script:, tested), park-name escaping in the map block, media-search placeholder fix,DeletePage/DeletePostatomicity (read-back + rollback) + opt-in scope-ownership guards,RevokeRolefail-closed auth, and block-render fault isolation (one bad block can't blank the page).🤖 Generated with Claude Code
Update — Distributed review, then a mobile & accessibility pass
Three further rounds since the last update.
Distributed review — problem findings
Multi-lens review of the whole CMS/front-door platform, each shard verified by an adversarial critic pass.
UpdatePage/UpdatePostnow fail loudly; the dup-slug guard distinguishes a real collision from a dropped write;RestoreRevisionhard-fails on a corrupt snapshot._promoteScheduled, an in-memory admin path map (kills an N+1), clamped and cached teaser.RevokeRolecapability probe, scoped person lookup, nav that resolves only live targets.columns, media where-used scan that includes embedded<img>,copyDocMIME allowlist, CSRF on preview-publish.Distributed review — 24 enhancements
content-visibilityon block sections, block surfaces routed through--fd-*theme tokens (zero visual change), carousel ARIA region + live announcements, per-block<style>dedup, gallery lightbox scroll-lock + inert background, preview-only block-failure placeholder.CmsPage::HTML_FIELDS/URL_FIELDS, content-mutation audit trail,PurgeTrashed+ orphan-block sweep + unused-tag prune, block-only saves bump ownerupdated_at/updated_by, DB-enforced one-active-theme-per-scope.clearrendercache+runmaintenanceactions.cms-admin.js(one source for toast / modal / CSRF-post), view counts on Pages & Posts plus a top-content dashboard panel,window.CMS_CAPS.sitemap.xml.Mobile & accessibility remediation
A mobile-review pass over the front-door blocks, the CMS admin shell, and the shared theme chrome.
margin-lefton the nth button; the steps grid steps 4→2→1 column; the path grid collapses at 520px; carousel dots and the play/pause toggle lift above the 2×2 stat ticker; mosaic tiles take a uniform height withobject-fit:cover; the member bar actually hides on phones; long org names ellipsize instead of shoving the hamburger off-screen; rich-text and raw-HTML tables scroll in their own container rather than forcing horizontal page scroll; CMS nav-manager rows wrap so actions keep a 40px touch target; the recovery-email input moves to 16px to stop iOS zoom-on-focus.#b8860b(2.9:1 on white) darkened across kickers, nav hover/active, officer roles, blog cards and the RSS link; muted greys lifted infile_downloadand the parks map. Dark mode added for the recovery-email modal and the parks-map empty states;--ork-warndefined globally so the media library's "In use" label is no longer an undefined token.aria-labelwitharia-hiddenon the glyph; the search toggle becomes a real<button>and the maintenance banner anaria-expandedbutton; focus trap + focus restore on CMS admin modals and backgroundinerton the staff-roster modal; media tiles are keyboard-operable with a focus ring; kingdom-profile tabs getrole=tablist/tab/tabpanel; icon-rail labels are visually hidden rather thandisplay:noneso the accessible name survives; Directory section titles promoted to<h2>; scrollable tables gettabindex=0+role=region.Page_viewbreadcrumb usesUIRinstead of a hardcoded path;photo_mosaicsurfaces an empty-state hint in the CMS preview.