Files
aurora-wordpress-admin/PROJECT_STATUS.md
T
2026-07-03 20:00:17 -05:00

42 KiB
Raw Blame History

Aurora Admin v2 — Project Status

Last updated: 2026-07-03 (latest session — free/paid feature gating (licensing))

What this project is

A ground-up rebuild of the "uiXpress" WordPress admin-theme plugin, relaunched as Aurora Admin. The original plugin's look/feel and feature set are the target, but no original code is reused — all Vue components, PHP classes, and build tooling are freshly written. This lives in aurora-admin-v2/ (the earlier in-place rebrand attempt, aurora-admin-working/, is superseded and no longer the active codebase).

Repo

Now on GitLab (self-hosted): ssh://[email protected]:2289/NixReaper/aurora-wordpress-admin.git (HTTPS also available). Repo root = the contents of aurora-admin-v2/ (admin/, app/, aurora-admin.php), not the whole uixpress/ folder — that folder has a bunch of dead-end attempts (aurora-admin-working, aurora-admin-distribution, aurora-admin-rewrite, aurora-admin-GLM, uixpress) that are debugging history, not part of the project.

.gitignore excludes app/node_modules/ and app/dist/ (build output — regenerate, don't version). To clone on a new machine: add an SSH key to the GitLab account, then git clone the URL above, then cd app && npm install.

Claude Code conversation history does not sync across machines — it's local per project-folder-per-machine. Point a fresh session at this file to get back up to speed instead of relying on prior chat history.

Workflow rules established

  • Always verify live in Chrome before declaring a fix done. Use the Chrome MCP tools (tabs_context_mcp, navigate, computer/screenshot, javascript_exec) to inspect real DOM/computed styles rather than guessing from source.
  • Zip packaging: on macOS, build via npm run build in app/, rsync the repo (excluding node_modules, .git, /dist) into a scratch staging dir, then zip -r -X it into dist/aurora-admin-v2.zip — macOS zip produces forward-slash paths natively, no path-separator corruption issue like Windows Compress-Archive/PowerShell had. Always delete the existing zip first (rm -f dist/aurora-admin-v2.zip) — zip -r merges into an existing archive rather than replacing it, so stale entries (old content hashes) silently survive otherwise. If building on Windows again, the old PowerShell System.IO.Compression.ZipFile + .Replace('\', '/') approach from earlier sessions still applies.
  • Fast local iteration: the test site is LocalWP at /Users/kris/Local Sites/aurora/app/public/wp-content/plugins/aurora-admin. Instead of rezipping + manually reinstalling for every small CSS/JS tweak, hot-patch the built files straight into that install (rm -rf app/dist && cp -R <repo>/app/dist app/dist, or copy individual files like admin/assets/native-dark.css) and just reload the page in Chrome — near-instant feedback loop. Rezip properly (see above) once the change is confirmed, so the zip artifact matches what's live-verified.
  • User manually reinstalls the zip from a downloaded/attached copy when actually deploying — the Chrome MCP file_upload tool cannot push arbitrary local files into the browser, confirmed in an earlier session, so that step can't be automated. (Hot-patching, above, sidesteps this for local dev/verification.)
  • Old build caching gotcha: WordPress serves whatever plugin files were last installed — if the user says "didn't fix it," suspect a stale zip still installed before suspecting the code. Verify the live script hash via document.querySelector('script[src*="shell"]') + computed styles when in doubt (done successfully this session to prove a spacing change was live).
  • Git: local repo config only (user.name/user.email set per-repo, not --global). Commit after each user-confirmed-working change, not before.
  • Site under test: http://admin.local/wp-admin/.

Architecture

  • PHP (admin/src/..., namespaced AuroraAdmin\...), bootstrapped from aurora-admin.php (manual require list, dependency order matters), instantiated in admin/src/App/AuroraAdmin.php's constructor.
  • Vue 3 app (app/src/...), Vite multi-entry build (app/vite.config.js rollupOptions.input) → manifest.json, read by admin/src/Utility/Assets.php::enqueue() which also walks the manifest's imports chain to pull in shared-chunk CSS. Data reaches each Vue entry via a data-aurora-admin JSON attribute on a mount div (never wp_localize_script).
  • Single global stylesheet: app/src/shell/shell-frame.css — all --aurora-* CSS variables (incl. .aurora-dark overrides) and the only rules touching WP structural IDs (#wpcontent, #wpbody-content). No element resets/utility classes leak onto native WP pages.
  • Immersive shell (admin/src/Shell/Shell.php + app/src/shell/SiteShell.vue
    • Sidebar.vue + Toolbar.vue + UserMenu.vue): hides native WP chrome, mounts a Vue sidebar/toolbar on every admin page.
    • MenuSerializer.php converts WP's $menu/$submenu globals into JSON for the sidebar's native-menu section — explicitly skips the aurora-admin slug so Aurora's own pages aren't duplicated (they're represented in the sidebar's own branded nav section instead).
    • Sidebar has two sections: the WP-native menu list (icon-chip style, 28×28px rounded icon backgrounds, tight ~2px vertical padding per item) and a bottom "Aurora" section (collapsible, expanded by default, persisted via localStorage['aurora-nav-expanded']) styled like a native WP submenu (plain indented text via .sidebar__sublist/ .sidebar__sublink, no icons) — deliberately matches how e.g. Posts' own submenu (All Posts/Add Post/Categories/Tags) looks, per explicit user request.
    • Sidebar is display:flex; flex-direction:column, split into a .sidebar__scroll (flex:1, scrollable — holds brand/native-list/Aurora section) and a pinned UserMenu footer below it (not part of the scrollable area, so the account card is always visible).
    • UserMenu.vue: floating card that expands upward from a bottom-left trigger (avatar+name+chevron). Card contains: user header (avatar/name/ email), links (Edit Profile → profile.php, Visit site, Site Settings → options-general.php, Sign Out in red). No theme control here anymore — see "Theme system" below; the old light/dark/system 3-way swatch picker and the dark_mode setting it drove are both gone entirely.
    • Old top-right toolbar user-dropdown and sun/moon toggle button are removed — fully replaced by the bottom-left UserMenu.
    • Sidebar is inset, not flush against the browser edge: fixed at top/left/bottom: var(--aurora-sidebar-gap) (16px), fully rounded corners, background: var(--aurora-card-bg) — floats as its own card over the page gradient. --aurora-sidebar-gap is a shared CSS var (shell-frame.css) that the toolbar's left offset and #wpcontent's margin-left both add too, so nothing overlaps when the gap changes.
    • Native-menu accordion: only one WP-native-menu section's submenu is open at a time (Sidebar.vue's openId ref) — expanding one collapses whichever else was open, instead of the list growing indefinitely as more sections get expanded. Was previously a Set of independently toggleable ids (each section remembered its own open/closed state); replaced with a single id so opening any section closes the rest. Starts on whichever section matches the current page (each nav is a full page load in this multi-page admin, so this is computed once at mount, not reactively). The separate "Aurora" branded nav section below it keeps its own independent expand/collapse state (localStorage['aurora-nav-expanded']) — not part of this accordion.

Theme system (rewrite — supersedes the old dark_mode 3-state model)

Went through several rounds of user-directed rework this session, landing on: 10 fixed, named color presets + 2 flat neutrals ("Black & Grey", "White") — no custom color picker, no separate light/dark toggle. Each preset is fully self-contained (its own coordinated backgrounds/cards/borders/text), not a hue applied on top of a shared light/dark base.

  • app/src/pages/settings/themePresets.js: themePresets array (10 hue-based presets like { id: 'indigo', label: 'Indigo', hue: 235 }, plus mono and white with an explicit palette object instead of a hue). buildPalette(preset) returns all --aurora-* tokens — for hue presets, computed via HSL ramps; for the two flat presets, returned as-is. resolvePreset(id) looks up by id.
  • The main app background (--aurora-app-bg) is a 3-stop gradient, not flat: linear-gradient(135deg, vivid1 0%, vivid2 55%, near-black 100%) — two bright, saturated stops of the same hue family carry most of the bar, dipping to a near-black anchor only in the last stretch. This exact ratio went through several corrections against a competing plugin's ("FleekDash") reference screenshots — a 50/50 dark-to-vivid split reads "washed out", two competing saturated hues ~70° apart reads "muddy"; this formula was the one the user confirmed as clean. If revisiting the gradient formula, re-check against those reference screenshots in the conversation history rather than re-deriving from scratch.
  • --aurora-card-bg-deep: a newer token, one step darker than --aurora-card-bg, used only by the Dashboard's AuroraCard.vue. Needed because the Dashboard got its own frame background using --aurora-frame-bg (see "Window chrome" below), which is close in lightness to --aurora-card-bg for most presets — cards would nearly disappear into the new frame without it.
  • applyThemePalette(themePresetId) / applyFontFamily(family) (app/src/pages/settings/applyLiveTheme.js) — shared between the settings page's live preview (Settings.vue's watch()) and the real per-page-load application (SiteShell.vue::onMounted). Sets each token via document.documentElement.style.setProperty.
  • Google Fonts picker: app/src/pages/settings/googleFonts.js (curated ~70 family list, not the full catalog), searchable dropdown in SettingsField.vue's font-picker field type, applied by injecting a Google Fonts CSS2 <link> tag + setting --aurora-font-family.
  • Recurring bug class this session — "independently-painted gradient": several elements (sidebar, toolbar, #wpbody-content on the settings page, #wpbody-content in general) each used to paint their own copy of var(--aurora-app-bg). Since it's a gradient, each element computes it independently over its own bounding box — a narrow tall sidebar and a wide short toolbar get visibly different-looking slices of "the same" gradient, creating seams at their shared edges. Fixed by making html/body the only thing that paints the gradient (background-attachment: fixed so it doesn't scroll away from the fixed sidebar/toolbar), and leaving every other element transparent so that one gradient plane shows through. If a new seam/mismatched-rectangle bug shows up, suspect this pattern first — grep for var(--aurora-app-bg) outside shell-frame.css's html.aurora-ready, html.aurora-ready body rule; it shouldn't be there.
  • Dashboard takeover (admin/src/Pages/DashboardTakeover.php + app/src/pages/dashboard/Dashboard.vue): hijacks native index.php via in_admin_header, hides the native .wrap, renders the Aurora dashboard in its place. 12-col grid; cards are single <section class="aurora-card"> elements with inline grid-column: span N (NOT wrapped — component attribute fallthrough for styles was unreliable). Never use unprefixed class names on Aurora components (.card collided with WP core once).
  • Settings page (admin/src/Pages/SettingsPage.php + app/src/pages/ settings/Settings.vue): owns the top-level "Aurora Admin" WP admin menu directly via add_menu_page (slug aurora-admin). Two-box layout: .settings__nav (category list) + .settings__content (fields), both boxed/bordered on a darker backdrop. Field schema is declarative in app/src/pages/settings/settingsConfig.js, rendered by app/src/pages/settings/components/SettingsField.vue (types: text, toggle, color, image, repeater, multiselect, segmented, remote-sites).
  • 6 sub-apps (admin/src/Pages/SubApps.php + one PHP feature-class pair each + one Vue page + one Vite entry each) — reachable only via the sidebar's Aurora nav section, not the native WP menu:
    • Registered via add_submenu_page('aurora-admin', ...) (or 'plugins.php' for Plugins) and left registered in WP's native $submenu global — no longer hidden via remove_submenu_page(). An earlier version did call it (the standard register-but-hide trick) but that corrupts WP core's own user_can_access_admin_page() capability lookup, which partly depends on the page still being present in $submenu to resolve its required capability — removing it caused "Sorry, you are not allowed to access this page" for every user, including admins, on Menu Creator/ Activity Log/Role Editor/Comments. Root-caused and fixed this session by deleting hide_from_native_submenu() entirely; duplication in the sidebar's native-menu section is instead prevented in MenuSerializer::serialize() (see below), which has no such side effect.
    • All slugs prefixed aurora-admin-* so Shell::is_own_page()'s existing strpos($page, "aurora-admin") === 0 check covers them for free (full custom dark theme, native-dark.css skipped). MenuSerializer::serialize() skips any submenu slug starting aurora-admin- (in addition to already skipping the aurora-admin top-level slug) so none of them are duplicated in the sidebar's native-menu section — needed because most are parented under aurora-admin (already skipped wholesale) but aurora-admin-plugins is parented under the real plugins.php menu.
    • Each page uses the shared two-pane layout app/src/pages/_shared/AppListShell.vue (generalized from Settings.vue's box-pair CSS) + AppListItem.vue for list rows.
    • Menu Creator (admin/src/MenuCreator/): DB table aurora_admin_menus (name/status/roles JSON/items JSON). Runtime admin_menu hook (priority PHP_INT_MAX) applies published configs matching the current user's roles — hides/relabels native menu items by slug. REST: GET/POST /menus, GET/PUT/DELETE /menus/:id, GET /menus/native-items (real WP menu list to build the editor from, via MenuSerializer::serialize()). Reordering is stored (order field) but not yet applied at runtime — CRUD + hide/relabel only so far.
    • Admin Notices (admin/src/Notices/): DB table aurora_admin_notices (title/message/type/roles JSON/pages JSON/dismissible/active). Runtime admin_notices hook renders active, role/page-matching rows via real WP notice markup. REST: GET/POST /notices, PUT/DELETE /notices/:id.
    • Role Editor (admin/src/Roles/): no custom table — wraps core get_editable_roles()/WP_Roles directly. Hard-codes the 5 default roles (administrator, editor, author, contributor, subscriber) as delete-protected (WP has no built-in check for this). REST: GET/POST /roles, PUT/DELETE /roles/:slug.
    • Activity Log (admin/src/ActivityLog/): DB table aurora_admin_activity_log. Hooked to save_post (guards wp_is_post_autosave()/wp_is_post_revision()), before_delete_post, user_register, profile_update, wp_login, activated_plugin, deactivated_plugin, switch_theme, update_option_aurora_admin_settings. REST: GET /activity-log?action_type=&object_type=&search=&page=.
    • Plugins (admin/src/Rest/PluginsData.php + app/src/pages/plugins/Plugins.vue): custom plugin management list (activate/deactivate/delete), also using AppListShell.vue.
    • Comments (admin/src/Rest/CommentsData.php + app/src/pages/comments/): newest sub-app, edit_posts-gated (not manage_options like the others). SubApps::redirect_native_pages() (admin_init) redirects the native edit-comments.php (when hit with no ?page= param, i.e. a direct nav or an old bookmark/menu link) to admin.php?page=aurora-admin-comments. MenuSerializer::resolve_url() also maps the edit-comments.php slug straight to the Aurora URL so the native-menu-derived sidebar entry (for users who can edit_posts but not manage_options, so don't see the Aurora nav section) still lands on the Aurora page. The Dashboard's comments card (DashboardData.php's listUrl) was updated to point at the same Aurora URL instead of the native one.
    • Database Explorer (admin/src/DatabaseExplorer/): no custom table — reads real $wpdb schema. GET /db-tables (SHOW TABLE STATUS), GET /db-tables/:table (columns + paginated rows — table name validated against the real $wpdb table list before use, never interpolated unchecked), POST /db-query (raw query runner, hard-restricted server-side to queries starting SELECT/SHOW/DESCRIBE/EXPLAIN, single-statement only, auto-LIMIT 200 if none given). All 3 endpoints manage_options-gated. Identifier allow-listing + backtick-escaping is the actual safety boundary ($wpdb->prepare() only binds values, not identifiers).
    • All 5 backend classes + SubApps.php are require'd in aurora-admin.php and instantiated in AuroraAdmin.php.
  • Settings persistence: admin/src/Options/Settings.php — partial-payload save via merge_on_save() sanitize callback (merges into existing option, doesn't overwrite). Settings::get_safe() strips license_key and remote site passwords for use on non-admin-gated pages; Settings::get() (full) only used on the manage_options-gated settings page itself.
  • Global search: app/src/shell/components/SearchModal.vue, Ctrl+K, debounced wp/v2/search REST calls, gated on disable_global_search setting.
  • Remote site switcher: toolbar dropdown, gated on canSwitchSites (from remote_site_switcher_capability setting) + non-empty remote_sites.
  • Scheduled content: app/src/pages/dashboard/cards/ScheduledContent.vue calendar cells (squares, date top-left, count centered) are clickable — navigate to edit.php?post_status=future&aurora_day=YYYY-MM-DD if that day has scheduled posts, else show a toast ("no scheduled content"). Server side: admin/src/Posts/ScheduledDayFilter.php filters the edit.php query via pre_get_posts and prints an info notice.
  • Analytics: admin/src/Analytics/Analytics.php (first-party pageview tracker, own DB table wp_aurora_admin_pageviews, bot detection, gated on enable_analytics/track_admins settings) + admin/src/Rest/AnalyticsData.php (aggregation REST endpoint: totals, series, device/referrer/country breakdowns, bounce rate, active users).
  • White label: admin/src/WhiteLabel/WhiteLabel.php — custom favicon, rename plugin in Plugins list, rename top-level menu item, gettext find/replace via text_replacements setting.
  • Native page dark styling: admin/assets/native-dark.css (large static sheet, all rules gated html.aurora-dark.aurora-ready, scoped under #wpbody-content) + admin/assets/native-enhance.js (progressive JS enhancement — currently only enhanceUpdates(), which groups the flat Updates page sections into .aurora-native-card wrapper divs on body.update-core-php). This session also extended coverage to media/ thickbox modals, plugin/theme card grids, and the plugin-search filter bar (all previously untouched, so still native white regardless of theme). Known gap: the plugin "More Details" popup renders inside an <iframe> pointing at a separate WP page — CSS can't reach into it; would need JS injection into the iframe's own document if that's ever worth fixing. This session also added dark-theme rules for the block editor's own chrome (.editor-header/.edit-post-header, .interface-interface-skeleton__sidebar and its nested .components-panel/.editor-post-status/ .interface-complementary-area-header) — found by inspecting the real screen via computer-use and drilling into nested elements' computed styles, since .interface-complementary-area (nested inside the sidebar) had its own explicit white background the outer rule didn't reach, leaving a white patch below the Tags panel until it was targeted directly. The actual post content canvas is deliberately left untouched (WYSIWYG, matches the front-end).

Window chrome (macOS-style title bar + taskbar)

Added earlier this session, user-requested after seeing a competing plugin's demo; extended later the same session to cover the block editor and the Site Editor, and the taskbar was rewritten to support multiple minimized windows. Every page gets a traffic-light title bar; minimizing persists across a full page navigation via a taskbar.

  • app/src/shell/windowChrome.js: initWindowChrome(adminUrl), called unconditionally from SiteShell.vue::onMounted (no longer skipped on the block editor — that special case was removed once the editor got its own handling). Three cases, checked in priority order:
    1. Dashboard (#aurora-admin-dashboard-root present) — wraps the bar + the dashboard root in a new .aurora-dashboard-frame div, giving it the same card-frame treatment (--aurora-frame-bg, border, radius) every other page gets from #wpbody-content itself.
    2. Block editor / Site Editor (isBlockEditorPage(): body has block-editor-page, or the path ends /post.php or /post-new.php — site-editor.php also gets block-editor-page on its body, so this already covers it with no JS change needed) — appends a .aurora-winbar--editor bar (dots only, no bar surface) directly to document.body, since neither editor's real content container is #wpbody-content.
    3. Everything else — native WP pages and all of Aurora's own Vue pages, inserted as the first child of #wpbody-content.
  • Red (close): window.location.href = adminUrl.
  • Green (maximize): toggles html.aurora-maximized — hides .sidebar/ .toolbar, zeroes #wpcontent's margin-left and the editor frame's inset, so content fills the whole browser window. Toggle again (or minimize) to restore. Nothing persisted — resets on next page load.
  • Yellow (minimize): on the Dashboard itself, toggles html.aurora-minimized which just collapses #aurora-admin-dashboard-root down to the bar in place (nowhere further "home" to send it). On every other page, calls taskbar.js's addMinimizedWindow(title, url) then navigates to the dashboard.
  • Taskbar rewritten from a single sessionStorage slot to a list (app/src/shell/taskbar.js): minimizing a second page used to silently overwrite the first one's entry, so only the most recent minimize ever showed up. Now getMinimizedWindows()/addMinimizedWindow() (dedupes by URL)/removeMinimizedWindow(id) operate on an array under sessionStorage['aurora-minimized-windows']; SiteShell.vue v-fors over it, rendering one .aurora-taskbar__item pill per entry inside a transparent, floating .aurora-taskbar bar (inset by --aurora-sidebar-gap on all exposed edges, rounded, its own box-shadow — went through a design iteration from a small centered pill back to full-length per explicit user correction, keeping only the pill shape on the individual items). Each item is independently restorable/closable. currentPageTitle() prefers the page's own on-screen <h1> (#wpbody-content h1, #aurora-admin-dashboard-root h1) over document.title, since WP's title-tag generation came up empty for Aurora's sub-app pages back when they were still remove_submenu_page()'d (see the sub-apps permissions bug above) — document.title is still the fallback for pages where that isn't an issue.
  • Content-behind-taskbar fix: html.aurora-has-taskbar no longer just adds margin-bottom (that only guarantees clearance once scrolled to the very bottom — content taller than the viewport still rendered underneath the fixed taskbar on first paint). Instead it caps #wpbody-content's/ .aurora-dashboard-frame's own height via max-height + overflow-y: auto, same principle as the toolbar's reserved space at the top. This appeared to do nothing at first — #wpbody-content's base rule sets min-height: calc(100vh - toolbar-h - 40px), and per spec min-height always wins over a smaller max-height; fixed by adding min-height: 0 to the taskbar-specific override.
  • Universal content-area height cap (this session): the taskbar-only max-height/overflow-y: auto treatment above is now applied to #wpbody-content unconditionally (not just when html.aurora-has-taskbar is set), capped to calc(100vh - toolbar-h - 40px). Several native and Aurora pages with a lot of content (Add Plugins, Add User, Profile, Appearance > Fonts, the file editors, every Settings page, Activity Log, Database Explorer, Comments) used to grow taller than the viewport and force the whole document to scroll — janky since the sidebar/toolbar are fixed, so scrolling just moved the content away from them with a lot of dead space. Now the content card's own box is capped and scrolls internally, same as the toolbar/taskbar reservations already did.
    • Scroll-anchoring bug this surfaced: making #wpbody-content a real scroll container exposed a second, unrelated bug on Add Plugins — its plugin-card grid renders via JS well after first paint, inserting a lot of content above whatever was already visible in the now-scrollable box. Chrome's scroll anchoring "corrects" for that by silently jumping scrollTop down to compensate, so the page loaded already scrolled ~1700px into the content instead of at the top. Fixed with overflow-anchor: none on #wpbody-content (and the taskbar/dashboard- frame variant) — the standard fix for this class of bug, opting the container out of anchoring entirely.
    • Phantom document-level scrollbar: independently, Add Plugins also left the outer <html>/<body> scrollable with ~1200px of empty dead space below the frame, even though every element inside #wpbody-content measured as properly capped to the viewport — narrowed down to some interaction between the plugin grid's async rendering and the browser's own scrollable-overflow bookkeeping (root cause not fully pinned down; forcibly shrinking or hiding #wpbody-content reduced but didn't eliminate it, and no single oversized/mispositioned descendant could be found). Since the intended design already has the sidebar/toolbar fixed and every page's real content scrolling internally within its own frame, the document itself was never supposed to scroll at all — fixed directly by adding overflow: hidden to html.aurora-ready/body, which removes the dead scroll space outright regardless of what was causing it.
  • Block editor / Site Editor framing: neither editor renders inside #wpcontent's normal box model — each has its own fixed, full-viewport root (.interface-interface-skeleton itself on post.php/post-new.php; #site-editor on site-editor.php, where .interface-interface-skeleton is merely position: relative, nested inside it — a reset rule puts the nested skeleton back to a plain unpositioned child so the two frames don't stack). html.aurora-ready .interface-interface-skeleton, html.aurora-ready #site-editor in shell-frame.css frames whichever is the real fixed root with the same insets/background/radius/shadow treatment #wpbody-content gets elsewhere, plus padding-top: var(--aurora-editor-winbar-h) to reserve a strip for .aurora-winbar--editor's dots and overflow: hidden so the rounded corners actually clip the editor's own white canvas background (border-radius alone doesn't clip children). Known CSS-over-constraint gotcha: WP core's .edit-site { height: 100vh; } class (present on #site-editor) means top, height, and bottom are all specified on that box at once — per spec this is over-constrained and bottom silently gets recomputed from top + height instead of honoring an explicit value, which is why the frame initially ran past the intended bottom inset despite bottom: 24px !important being set. Fixed by adding height: auto !important to the same rule so top/bottom actually determine the box size again. If a future feature needs to reach either editor's own chrome, the real containers are .interface-interface-skeleton (post editor) or #site-editor (Site Editor) — don't assume the normal #wpbody-content page structure there.
  • Shell.php::is_enabled() no longer excludes the Site Editor screen (site-editor used to be in the skip list alongside customize) — it turns out to be built on the same @wordpress/interface package as the block editor (body.block-editor-page is present on both), so the existing block-editor handling covers it once #site-editor is framed.

Licensing (free vs. paid feature gating)

Added this session. The split, as decided with the user: the whole shell/ theming product is free (it's the adoption hook); Role Editor, Activity Log, Database Explorer, Analytics, White Label, and the remote site switcher are paid (the "operational" tools an agency/serious site owner pays for). Menu Creator, Admin Notices, Comments, and the Plugins page stay free.

  • admin/src/Licensing/License.php is the single gate every paid feature checks — PAID_FEATURES maps a feature id to a human label, is_active() and is_feature_available($feature) are the two calls everything else uses. is_active() is a placeholder today: any non-empty license_key setting counts as licensed, no remote validation at all. There's no license server yet — when one exists, only is_active()'s internals need to change (ideally caching the remote check in a transient so it isn't a network call on every admin page load); every gated feature already funnels through this class rather than reading the setting directly, so nothing else needs to change.
  • Sub-apps (SubApps.php): PAGES now has a "feature" key per page (null for the 4 free ones). Locked pages still fully register (add_submenu_page, real capability, real URL — same as before) so nav/ bookmarks/capability checks all keep working; render() and maybe_enqueue() just check License::is_feature_available() and swap in a shared upgrade prompt (below) instead of the real feature's mount div/JS bundle/REST data when unlicensed. This also means a locked page's sensitive data (e.g. Database Explorer's schema access) never gets fetched or serialized to the page in the first place.
  • Shared upgrade prompt: app/src/pages/_shared/UpgradeNotice.vue (lock icon, feature name, description, a CTA linking to admin.php?page=aurora-admin#license) + a dedicated app/src/entries/locked-feature.js Vite entry that mounts it onto [data-aurora-locked-feature]. SettingsField.vue gives the license key field id="license" so that CTA link scrolls straight to it (General is the default-active settings category, so the anchor resolves on a fresh page load too). Reused directly by the Dashboard's Analytics tab (below) rather than building a second locked-state component.
  • Analytics (paid): Analytics.php's constructor returns immediately (no hooks registered, no pageview tracking) when unlicensed. AnalyticsData.php's REST permission_callback also checks License::is_feature_available("analytics"), so the endpoint 403s even if someone hits it directly. The Dashboard's Analytics tab (Dashboard.vue) renders UpgradeNotice in place of the real charts when useAnalyticsData's fetch comes back non-OK (the 403, in practice).
  • White Label (paid): WhiteLabel.php's constructor returns immediately when unlicensed — favicon/plugin-rename/menu-rename/text-replacement filters simply never register. Its settings fields stay visible either way (so an admin can pre-configure before upgrading) — enforcement is entirely server-side, nothing in the UI needs to disable inputs.
  • Remote site switcher (paid): Shell.php's canSwitchSites flag now additionally requires License::is_feature_available("remote-site-switcher"), alongside its existing remote_site_switcher_capability check.
  • Sidebar lock badges: Shell::aurora_nav() adds a "locked" bool per nav item (true for Role Editor/Activity Log/Database Explorer when unlicensed); Sidebar.vue renders a small lock dashicon next to the label when set. The link still navigates normally — SubApps::render() is what actually shows the upgrade prompt there.
  • Settings page: White Label/Analytics category subtitles and the Remote Site Switcher Capability field's description now say "Pro feature — requires a license" (settingsConfig.js) — a plain-text note only, no functional gating in the settings UI itself (the features' own code is the actual boundary).
  • Bug fix surfaced while testing this: RolesData.php's REST callback called get_editable_roles() unqualified, which 500'd every time — that function lives in wp-admin/includes/user.php, which WP only auto-loads for real wp-admin page requests, not REST API requests. Fixed with a function_exists guard + require_once before the call. Unrelated to licensing, just happened to surface while verifying Role Editor's unlock path.

Key bugs fixed (root causes, in case they resurface)

  • .card global class collision: original Dashboard cards used class="card", colliding with WP core's .card { max-width: 520px }. Fixed by renaming to .aurora-card* everywhere. Never use unprefixed class names on Aurora components.
  • Grid-column not applying on wrapped cards: settled on unwrapped <section class="aurora-card" :style="{gridColumn: ...}"> — component boundaries ate the style binding in earlier attempts.
  • Two-box settings layout: took two wrong iterations before matching the user's reference image. Re-confirm layout intent against a screenshot before assuming.
  • Redundant outer frame border on settings page: fixed with a body.toplevel_page_aurora-admin override in shell-frame.css.
  • Dashboard submenu defaulting collapsed: Sidebar.vue's normalize() URL helper didn't map the bare /wp-admin/ root to index.php, so the Dashboard section's active-state check (and thus its default-expanded state) never matched on the dashboard itself. Fixed by normalizing empty/ query-only paths to 'index.php' + path.
  • Aurora top-level menu appearing twice: SettingsPage.php's add_menu_page('aurora-admin', ...) naturally appears in WP's native $menu global. Since it's now also shown in the sidebar's dedicated Aurora nav section, MenuSerializer::serialize() explicitly skips slug aurora-admin to avoid the duplicate.
  • Browser automation can't install plugins: the Chrome MCP file_upload tool only accepts files already "shared with the session" (attachments/ connected folders) — local build output on disk doesn't qualify. Confirmed this session; the user must manually reinstall the zip every time (though see "Fast local iteration" above for the hot-patch workaround used for verification during dev).
  • Independently-painted gradient seams (sidebar/toolbar/settings-page #wpbody-content each showing a visibly different slice of "the same" background gradient): see the theme-system section above — root cause was multiple elements each painting their own copy of a gradient-valued var(--aurora-app-bg) instead of one element painting it and the rest staying transparent.
  • Block editor content rendering underneath the sidebar: Gutenberg's .interface-interface-skeleton is position: fixed; inset: 0, completely bypassing the margin-left that pushes every other page's #wpcontent over — see the "Window chrome" block editor exception above for the full mechanism and the fix.
  • zip -r merging into a stale archive: rebuilding the deployable zip without first deleting the old one left old-hash asset filenames mixed into the new zip (macOS zip -r appends/merges rather than replacing). Always rm -f the zip before rebuilding.
  • remove_submenu_page() corrupting WP's own capability check: root cause of "Sorry, you are not allowed to access this page" on Menu Creator/ Activity Log/Role Editor/Comments — see the sub-apps bullet above. Also broke document.title on those pages, which is why taskbar.js's currentPageTitle() prefers the on-page <h1> instead. If a future custom admin page needs to be kept out of the native menu while staying reachable and capability-checked, filter it out at the menu-serialization layer (like MenuSerializer.php does), never with remove_submenu_page().
  • min-height/max-height conflict silently no-oping a fix: per spec, when min-height is larger than max-height, min-height always wins and max-height is ignored outright — this is why the taskbar-overlap fix first appeared to do nothing (see Window chrome section above).
  • position: fixed over-constraint silently ignoring an explicit bottom: when top, height, and bottom are all specified on a fixed-position box, the spec has bottom get silently recomputed from top + height rather than honored — root cause of the Site Editor frame running past the viewport despite an explicit bottom: 24px !important (see Window chrome section above).

Not-yet-done (deferred, no active request)

  • Full field sets for settings categories beyond General/Analytics/White Label (original plugin had more).
  • Extending native-enhance.js/native-dark.css coverage to more native surfaces — the block editor's own chrome, Widgets screen, Customizer. Media/thickbox modals and plugin/theme card grids were added this session; the plugin "More Details" popup specifically can't be reached (it's an <iframe> — see native page dark styling notes above).
  • Porting the real license-key activation flow (built previously in the old aurora-admin-working project, not yet ported to v2 — currently just a text field).
  • Menu Creator: stored item order field isn't applied at runtime yet (hide/relabel work; reordering doesn't).
  • No automated tests anywhere in the project — all verification has been manual, live-in-Chrome, per the established workflow rule above.

Current state

No task in progress. An earlier session rebuilt the entire theming system (fixed color presets, gradient backgrounds, "independently-painted gradient" seam bugs) and added the macOS-style window chrome + taskbar feature end-to-end (pushed as commits 4a547d2/b0b0a99). Later that same session: root-caused and fixed the Menu Creator/Activity Log/Role Editor "Sorry, you are not allowed to access this page" bug (was remove_submenu_page() corrupting WP's own capability lookup); added a 6th sub-app, Comments; fixed a white-patch bug in the block editor's dark-themed sidebar; rewrote the taskbar from a single sessionStorage slot to a list supporting multiple minimized windows; and extended the window-chrome/taskbar framing to the Site Editor (site-editor.php), including tracking down a CSS over-constraint bug (top+height+bottom all set on a fixed-position box silently ignoring bottom) that was making its frame run past the viewport (pushed as commit 8153d43).

An earlier pass this session capped every native/Aurora page's content area to the viewport with internal scrolling instead of letting tall pages grow past it and scroll the whole document (see "Universal content-area height cap" above), which surfaced and fixed two more bugs along the way — a Chrome scroll-anchoring jump on Add Plugins, and a phantom document-level scrollbar with a large empty gap below the frame. Also made the sidebar's native-menu sections a single-open accordion instead of letting multiple stay expanded at once (pushed as commit 73591e0).

Most recently: added the free/paid feature-gating system end-to-end (see "Licensing" above) — Role Editor, Activity Log, Database Explorer, Analytics, White Label, and the remote site switcher are now paid; Menu Creator, Admin Notices, Comments, Plugins, and the whole shell/theming product stay free. Verified both directions live (entering/clearing a license key immediately locks/unlocks the sidebar badges, the sub-app pages, the Dashboard's Analytics tab, and the underlying REST endpoints), and fixed an unrelated pre-existing 500 in Role Editor's REST endpoint along the way (pushed as commit 6d50260). The distributable zip (dist/aurora-admin-v2.zip) was rebuilt fresh from source and should be re-verified/re-zipped again after this doc update if anything changed since.

All of it was live-verified in Chrome against the LocalWP site (build hot-patched into /Users/kris/Local Sites/aurora/.../aurora-admin/app/dist for iteration — the repo's source files are the actual edits; that copy is just the compiled build artifact, not a separate edit surface).

Not yet built: an actual license-purchase/checkout flow or remote validation server — today entering any non-empty text in the License key field unlocks every paid feature (see "Licensing" above). This is fine for internal testing but must be replaced with real validation before this ships to real users, or the paid tier isn't actually gated.