Enhancement: Project Front Door#486
Open
baltinerdist wants to merge 90 commits into
Open
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…oller (class.Controller.php normalized tabs->PSR-12 spaces per boy-scout policy) Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…t.tpl Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…, events, mosaic, kingdoms, CTA) Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…DB clear, responsive, font load) Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…cache-bust frontdoor.css Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…bove) Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…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) <[email protected]>
…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) <[email protected]>
…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 ([email protected] 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) <[email protected]>
…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) <[email protected]>
…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) <[email protected]>
…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) <[email protected]>
…ity / super-admin) Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…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) <[email protected]>
- 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) <[email protected]>
…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) <[email protected]>
…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) <[email protected]>
…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) <[email protected]>
…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) <[email protected]>
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) <[email protected]>
…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) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…CSS tokenization The front-door CSS tokenization aliased --navy→--fd-primary and --ink→--fd-text. The dark baseline overrides those tokens, silently changing .fd-dropdown background and .fd-nav-toggle color in dark mode. Add two html[data-theme="dark"] rules that restore the exact original computed values (#0b1120 and #1a2236) — zero visual change. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Replace exact-match hardcoded hex colors with fd-* CSS custom property tokens so block partials track the active theme. Zero visual change on an unthemed page (token defaults == original literals). 18 replacements across 11 partials: - #fff as contrast text on navy bands → var(--fd-primary-contrast) (cta_band, photo_mosaic, kingdoms_teaser, member_bar, steps) - background:#fff as panel/card bg → var(--fd-bg) (richtext, rich_text, events_feed, blog_feed, accordion, file_download) - color:#1a2236 as body text → var(--fd-text) (blog_feed .bf-card-title, file_download .fdb-file-title) Partials with no exact-match colors (f7f8fb, #667, decorative, etc.) left untouched per conservative tokenization rule. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… override The frontdoor.css [style*="background:#fff"] !important dark rule handles these section wrappers; switching the inline to var(--fd-bg) silently dropped that cover (dark bg shifted #1a2236 -> #0e1626). Revert just the 4 inline backgrounds; the --fd-primary-contrast and CSS-class-rule tokenizations from Task 10 stay. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…/activate/reset - Iframe preview: debounced 150ms fetch to CmsAjax/previewtheme; injects returned CSS at end of iframe <body> (wins frontdoor.css cascade); first preview fires on iframe load event - Duplicate control sync: collectTokens() keys by token name (deduped); handleControlChange mirrors value to all [data-token=same] controls (main ↔ advanced) and all [data-hex-for=same] hex display fields - Light/dark preview toggle: sets data-theme on iframe documentElement so the injected CSS dark block takes effect - Inline contrast warnings: WCAG luminance/contrast formula ported to JS; .te-contrast-warn-inline chip appended near offending text-color control when ratio < 4.5; bar-level summary also wired - Save: POST CmsAjax/savetheme (name=Default, tokens=JSON); stashes theme_id - Apply to site: save then POST CmsAjax/activatetheme; toast "Theme applied to your site." - Reset: tnConfirm modal (never native confirm()); POST CmsAjax/resettheme; reloads page to repopulate controls from factory defaults - Added .te-contrast-warn-inline CSS with dark-mode override Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…, dark placeholder - Remove window.THEME_CATALOG / THEME_FONTS / THEME_VALUES (never read by the IIFE) - Drop unused $idSuffix parameter from $renderControl closure + all 4 call sites - Add html[data-theme="dark"] .te-color-hex::placeholder rule to cms-admin.css Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
6ebe625 to
9545245
Compare
Design for per-kingdom public sites at /k/{slug} on the existing
scope-ready CMS: new ork_cms_site table, Controller_Site public router,
standalone chrome, scope-aware admin, three org-scoped dynamic blocks,
starter template + publish lifecycle. Kingdom-first, park-ready.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
New ork_cms_site table (one publishable, addressable website per org, scope kingdom|park), class.CmsSite.php lifecycle + slug derive/validate (charset, reserved-route blocklist, uniqueness) + idempotent EnsureSite, thin model pass-through, and a standalone unit suite (40 assertions). Migration idempotent + non-destructive; verified applied locally. EnsureSite refuses unresolved (<=0) scope; UpdateSite hyphenates typed slugs via DeriveSlug rather than silently stripping. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Controller_Site renders a kingdom's published pages/posts/blog by slug
through the shared block renderer inside standalone org chrome
(default.theme parameterized with $IsOrgSite — own header/logo/nav/footer,
subtle ORK tie-back, no global nav/member-bar/editor-FAB). Per-org theme
tokens injected from the site's own scope. Raw Site/view|page|blog|post
routes work today; nginx /k/{slug} pretty layer added (belt-and-suspenders).
Scope is always read from the resolved site row; page/post lookups pass
scope + publishedOnly. Lifecycle-safe error states: unknown slug AND
unbuilt sites both render a BARE 404 (no identity leak); draft → branded
coming-soon (no page bodies); missing sub-page → in-chrome 404.
Review fixes: escape <title> sink (site_name stored-XSS); unbuilt 404 no
longer leaks org name/logo/nav; empty "being built" home set no_index;
wired page/post meta_description into the (now escaped) description meta.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Thread a validated per-org scope through the existing global CMS admin
(additive; no-scope = byte-for-byte the old global front-door behavior).
- trait.CmsScope: parses ?scope=k:{id}/p:{id}, RE-VALIDATES server-side via
HasAuthority(AUTH_EDIT) — client value is a selector, never authority;
malformed/unauthorized → deny (no silent downgrade). _rowInScope IDOR
predicate shared by both controllers.
- Cms + CmsAjax: every action/endpoint resolves scope, gates via
CmsCan($uid,$cap,$scope), scope-filters lists, seeds new rows with scope.
IDOR guard (_requireOwned) on every by-id mutation — an officer of one org
cannot edit/publish/delete another org's page/post/media/nav.
- Publish gate: page.publish + site publish require AUTH_ADMIN (monarch/
regent); publishsite/unpublishsite reject global scope. AUTH_EDIT officers
get a clear locked state.
- "Manage Public Site" entry from the kingdom hero (AUTH_EDIT) → EnsureSite +
scoped dashboard; context banner "Editing: {Org} — public site". Scope
rides admin JS fetches; CSRF untouched.
Review fixes: IDOR-guard nav link target page_id/post_id (+ scope-bound nav
JOINs) to close cross-org title/slug disclosure; scope-check hero_media_id
on save; align public org_header nav to the 'marketing' menu the admin
writes (was 'site' → nav rendered empty).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Three new addable dynamic blocks that render live ORK data for the page's org, mirroring the self-sourcing blog_feed pattern: - kingdom_officers — current officers (persona/office) via Kingdom.GetOfficers (public Token='' path suppresses real names); pairs with authored staff_roster for Board of Directors. - kingdom_parks — active parks via Kingdom.GetParks, deep-linked. - kingdom_events — upcoming kingdom events via SearchService. Each derives kingdom_id solely from render-time scope ($SiteNavScope*) set by Controller_Site; non-kingdom/global context early-returns (renders nothing, no kingdom_id=0 junk); graceful empty states; all ORK output htmlspecialchars-escaped; dark-mode + responsive; FA5 icons. Registered in the block catalog, allow-list, starter presets, and editor schema. Review fix: officers avatar shows the shield icon by default and reveals heraldry only onload, eliminating the guaranteed-404 broken-image flash (GetHeraldryUrl has no has-heraldry signal). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
…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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
…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) <[email protected]>
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