42 KiB
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 buildinapp/,rsyncthe repo (excludingnode_modules,.git,/dist) into a scratch staging dir, thenzip -r -Xit intodist/aurora-admin-v2.zip— macOSzipproduces forward-slash paths natively, no path-separator corruption issue like WindowsCompress-Archive/PowerShell had. Always delete the existing zip first (rm -f dist/aurora-admin-v2.zip) —zip -rmerges into an existing archive rather than replacing it, so stale entries (old content hashes) silently survive otherwise. If building on Windows again, the old PowerShellSystem.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 likeadmin/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_uploadtool 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.emailset 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/..., namespacedAuroraAdmin\...), bootstrapped fromaurora-admin.php(manualrequirelist, dependency order matters), instantiated inadmin/src/App/AuroraAdmin.php's constructor. - Vue 3 app (
app/src/...), Vite multi-entry build (app/vite.config.jsrollupOptions.input) →manifest.json, read byadmin/src/Utility/Assets.php::enqueue()which also walks the manifest'simportschain to pull in shared-chunk CSS. Data reaches each Vue entry via adata-aurora-adminJSON attribute on a mount div (neverwp_localize_script). - Single global stylesheet:
app/src/shell/shell-frame.css— all--aurora-*CSS variables (incl..aurora-darkoverrides) 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.vueSidebar.vue+Toolbar.vue+UserMenu.vue): hides native WP chrome, mounts a Vue sidebar/toolbar on every admin page.
MenuSerializer.phpconverts WP's$menu/$submenuglobals into JSON for the sidebar's native-menu section — explicitly skips theaurora-adminslug 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 pinnedUserMenufooter 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 thedark_modesetting 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-gapis a shared CSS var (shell-frame.css) that the toolbar'sleftoffset and#wpcontent'smargin-leftboth 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'sopenIdref) — expanding one collapses whichever else was open, instead of the list growing indefinitely as more sections get expanded. Was previously aSetof 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:themePresetsarray (10 hue-based presets like{ id: 'indigo', label: 'Indigo', hue: 235 }, plusmonoandwhitewith an explicitpaletteobject 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'sAuroraCard.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-bgfor 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'swatch()) and the real per-page-load application (SiteShell.vue::onMounted). Sets each token viadocument.documentElement.style.setProperty.- Google Fonts picker:
app/src/pages/settings/googleFonts.js(curated ~70 family list, not the full catalog), searchable dropdown inSettingsField.vue'sfont-pickerfield 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-contenton the settings page,#wpbody-contentin general) each used to paint their own copy ofvar(--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 makinghtml/bodythe only thing that paints the gradient (background-attachment: fixedso 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 forvar(--aurora-app-bg)outsideshell-frame.css'shtml.aurora-ready, html.aurora-ready bodyrule; it shouldn't be there. - Dashboard takeover (
admin/src/Pages/DashboardTakeover.php+app/src/pages/dashboard/Dashboard.vue): hijacks nativeindex.phpviain_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 inlinegrid-column: span N(NOT wrapped — component attribute fallthrough for styles was unreliable). Never use unprefixed class names on Aurora components (.cardcollided 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 viaadd_menu_page(slugaurora-admin). Two-box layout:.settings__nav(category list) +.settings__content(fields), both boxed/bordered on a darker backdrop. Field schema is declarative inapp/src/pages/settings/settingsConfig.js, rendered byapp/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$submenuglobal — no longer hidden viaremove_submenu_page(). An earlier version did call it (the standard register-but-hide trick) but that corrupts WP core's ownuser_can_access_admin_page()capability lookup, which partly depends on the page still being present in$submenuto 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 deletinghide_from_native_submenu()entirely; duplication in the sidebar's native-menu section is instead prevented inMenuSerializer::serialize()(see below), which has no such side effect. - All slugs prefixed
aurora-admin-*soShell::is_own_page()'s existingstrpos($page, "aurora-admin") === 0check covers them for free (full custom dark theme,native-dark.cssskipped).MenuSerializer::serialize()skips any submenu slug startingaurora-admin-(in addition to already skipping theaurora-admintop-level slug) so none of them are duplicated in the sidebar's native-menu section — needed because most are parented underaurora-admin(already skipped wholesale) butaurora-admin-pluginsis parented under the realplugins.phpmenu. - Each page uses the shared two-pane layout
app/src/pages/_shared/AppListShell.vue(generalized from Settings.vue's box-pair CSS) +AppListItem.vuefor list rows. - Menu Creator (
admin/src/MenuCreator/): DB tableaurora_admin_menus(name/status/roles JSON/items JSON). Runtimeadmin_menuhook (priorityPHP_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, viaMenuSerializer::serialize()). Reordering is stored (orderfield) but not yet applied at runtime — CRUD + hide/relabel only so far. - Admin Notices (
admin/src/Notices/): DB tableaurora_admin_notices(title/message/type/roles JSON/pages JSON/dismissible/active). Runtimeadmin_noticeshook 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 coreget_editable_roles()/WP_Rolesdirectly. 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 tableaurora_admin_activity_log. Hooked tosave_post(guardswp_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 usingAppListShell.vue. - Comments (
admin/src/Rest/CommentsData.php+app/src/pages/comments/): newest sub-app,edit_posts-gated (notmanage_optionslike the others).SubApps::redirect_native_pages()(admin_init) redirects the nativeedit-comments.php(when hit with no?page=param, i.e. a direct nav or an old bookmark/menu link) toadmin.php?page=aurora-admin-comments.MenuSerializer::resolve_url()also maps theedit-comments.phpslug straight to the Aurora URL so the native-menu-derived sidebar entry (for users who canedit_postsbut notmanage_options, so don't see the Aurora nav section) still lands on the Aurora page. The Dashboard's comments card (DashboardData.php'slistUrl) was updated to point at the same Aurora URL instead of the native one. - Database Explorer (
admin/src/DatabaseExplorer/): no custom table — reads real$wpdbschema.GET /db-tables(SHOW TABLE STATUS),GET /db-tables/:table(columns + paginated rows — table name validated against the real$wpdbtable list before use, never interpolated unchecked),POST /db-query(raw query runner, hard-restricted server-side to queries startingSELECT/SHOW/DESCRIBE/EXPLAIN, single-statement only, auto-LIMIT 200if none given). All 3 endpointsmanage_options-gated. Identifier allow-listing + backtick-escaping is the actual safety boundary ($wpdb->prepare()only binds values, not identifiers). - All 5 backend classes +
SubApps.phparerequire'd inaurora-admin.phpand instantiated inAuroraAdmin.php.
- Registered via
- Settings persistence:
admin/src/Options/Settings.php— partial-payload save viamerge_on_save()sanitize callback (merges into existing option, doesn't overwrite).Settings::get_safe()stripslicense_keyand 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, debouncedwp/v2/searchREST calls, gated ondisable_global_searchsetting. - Remote site switcher: toolbar dropdown, gated on
canSwitchSites(fromremote_site_switcher_capabilitysetting) + non-emptyremote_sites. - Scheduled content:
app/src/pages/dashboard/cards/ScheduledContent.vuecalendar cells (squares, date top-left, count centered) are clickable — navigate toedit.php?post_status=future&aurora_day=YYYY-MM-DDif that day has scheduled posts, else show a toast ("no scheduled content"). Server side:admin/src/Posts/ScheduledDayFilter.phpfilters theedit.phpquery viapre_get_postsand prints an info notice. - Analytics:
admin/src/Analytics/Analytics.php(first-party pageview tracker, own DB tablewp_aurora_admin_pageviews, bot detection, gated onenable_analytics/track_adminssettings) +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 viatext_replacementssetting. - Native page dark styling:
admin/assets/native-dark.css(large static sheet, all rules gatedhtml.aurora-dark.aurora-ready, scoped under#wpbody-content) +admin/assets/native-enhance.js(progressive JS enhancement — currently onlyenhanceUpdates(), which groups the flat Updates page sections into.aurora-native-cardwrapper divs onbody.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__sidebarand its nested.components-panel/.editor-post-status/.interface-complementary-area-header) — found by inspecting the real screen viacomputer-useand 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 fromSiteShell.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:- Dashboard (
#aurora-admin-dashboard-rootpresent) — wraps the bar + the dashboard root in a new.aurora-dashboard-framediv, giving it the same card-frame treatment (--aurora-frame-bg, border, radius) every other page gets from#wpbody-contentitself. - Block editor / Site Editor (
isBlockEditorPage(): body hasblock-editor-page, or the path ends/post.phpor/post-new.php—site-editor.phpalso getsblock-editor-pageon its body, so this already covers it with no JS change needed) — appends a.aurora-winbar--editorbar (dots only, no bar surface) directly todocument.body, since neither editor's real content container is#wpbody-content. - Everything else — native WP pages and all of Aurora's own Vue pages,
inserted as the first child of
#wpbody-content.
- Dashboard (
- Red (close):
window.location.href = adminUrl. - Green (maximize): toggles
html.aurora-maximized— hides.sidebar/.toolbar, zeroes#wpcontent'smargin-leftand 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-minimizedwhich just collapses#aurora-admin-dashboard-rootdown to the bar in place (nowhere further "home" to send it). On every other page, callstaskbar.js'saddMinimizedWindow(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. NowgetMinimizedWindows()/addMinimizedWindow()(dedupes by URL)/removeMinimizedWindow(id)operate on an array undersessionStorage['aurora-minimized-windows'];SiteShell.vuev-fors over it, rendering one.aurora-taskbar__itempill per entry inside a transparent, floating.aurora-taskbarbar (inset by--aurora-sidebar-gapon 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) overdocument.title, since WP's title-tag generation came up empty for Aurora's sub-app pages back when they were stillremove_submenu_page()'d (see the sub-apps permissions bug above) —document.titleis still the fallback for pages where that isn't an issue. - Content-behind-taskbar fix:
html.aurora-has-taskbarno longer just addsmargin-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 viamax-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 setsmin-height: calc(100vh - toolbar-h - 40px), and per specmin-heightalways wins over a smallermax-height; fixed by addingmin-height: 0to the taskbar-specific override. - Universal content-area height cap (this session): the taskbar-only
max-height/overflow-y: autotreatment above is now applied to#wpbody-contentunconditionally (not just whenhtml.aurora-has-taskbaris set), capped tocalc(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-contenta 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 jumpingscrollTopdown to compensate, so the page loaded already scrolled ~1700px into the content instead of at the top. Fixed withoverflow-anchor: noneon#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-contentmeasured 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-contentreduced 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 addingoverflow: hiddentohtml.aurora-ready/body, which removes the dead scroll space outright regardless of what was causing it.
- Scroll-anchoring bug this surfaced: making
- Block editor / Site Editor framing: neither editor renders inside
#wpcontent's normal box model — each has its own fixed, full-viewport root (.interface-interface-skeletonitself onpost.php/post-new.php;#site-editoronsite-editor.php, where.interface-interface-skeletonis merelyposition: 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-editorinshell-frame.cssframes whichever is the real fixed root with the same insets/background/radius/shadow treatment#wpbody-contentgets elsewhere, pluspadding-top: var(--aurora-editor-winbar-h)to reserve a strip for.aurora-winbar--editor's dots andoverflow: hiddenso 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) meanstop,height, andbottomare all specified on that box at once — per spec this is over-constrained andbottomsilently gets recomputed fromtop + heightinstead of honoring an explicit value, which is why the frame initially ran past the intended bottom inset despitebottom: 24px !importantbeing set. Fixed by addingheight: auto !importantto the same rule sotop/bottomactually 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-contentpage structure there. Shell.php::is_enabled()no longer excludes the Site Editor screen (site-editorused to be in the skip list alongsidecustomize) — it turns out to be built on the same@wordpress/interfacepackage as the block editor (body.block-editor-pageis present on both), so the existing block-editor handling covers it once#site-editoris 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.phpis the single gate every paid feature checks —PAID_FEATURESmaps a feature id to a human label,is_active()andis_feature_available($feature)are the two calls everything else uses.is_active()is a placeholder today: any non-emptylicense_keysetting counts as licensed, no remote validation at all. There's no license server yet — when one exists, onlyis_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):PAGESnow has a"feature"key per page (nullfor 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()andmaybe_enqueue()just checkLicense::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 toadmin.php?page=aurora-admin#license) + a dedicatedapp/src/entries/locked-feature.jsVite entry that mounts it onto[data-aurora-locked-feature].SettingsField.vuegives the license key fieldid="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 RESTpermission_callbackalso checksLicense::is_feature_available("analytics"), so the endpoint 403s even if someone hits it directly. The Dashboard's Analytics tab (Dashboard.vue) rendersUpgradeNoticein place of the real charts whenuseAnalyticsData'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'scanSwitchSitesflag now additionally requiresLicense::is_feature_available("remote-site-switcher"), alongside its existingremote_site_switcher_capabilitycheck. - Sidebar lock badges:
Shell::aurora_nav()adds a"locked"bool per nav item (true for Role Editor/Activity Log/Database Explorer when unlicensed);Sidebar.vuerenders 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 calledget_editable_roles()unqualified, which 500'd every time — that function lives inwp-admin/includes/user.php, which WP only auto-loads for real wp-admin page requests, not REST API requests. Fixed with afunction_existsguard +require_oncebefore 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)
.cardglobal class collision: original Dashboard cards usedclass="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-adminoverride inshell-frame.css. - Dashboard submenu defaulting collapsed:
Sidebar.vue'snormalize()URL helper didn't map the bare/wp-admin/root toindex.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'sadd_menu_page('aurora-admin', ...)naturally appears in WP's native$menuglobal. Since it's now also shown in the sidebar's dedicated Aurora nav section,MenuSerializer::serialize()explicitly skips slugaurora-adminto avoid the duplicate. - Browser automation can't install plugins: the Chrome MCP
file_uploadtool 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-contenteach 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-valuedvar(--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-skeletonisposition: fixed; inset: 0, completely bypassing themargin-leftthat pushes every other page's#wpcontentover — see the "Window chrome" block editor exception above for the full mechanism and the fix. zip -rmerging into a stale archive: rebuilding the deployable zip without first deleting the old one left old-hash asset filenames mixed into the new zip (macOSzip -rappends/merges rather than replacing). Alwaysrm -fthe 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 brokedocument.titleon those pages, which is whytaskbar.js'scurrentPageTitle()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 (likeMenuSerializer.phpdoes), never withremove_submenu_page().min-height/max-heightconflict silently no-oping a fix: per spec, whenmin-heightis larger thanmax-height,min-heightalways wins andmax-heightis ignored outright — this is why the taskbar-overlap fix first appeared to do nothing (see Window chrome section above).position: fixedover-constraint silently ignoring an explicitbottom: whentop,height, andbottomare all specified on a fixed-position box, the spec hasbottomget silently recomputed fromtop + heightrather than honored — root cause of the Site Editor frame running past the viewport despite an explicitbottom: 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.csscoverage 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-workingproject, not yet ported to v2 — currently just a text field). - Menu Creator: stored item
orderfield 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.