- License.php now calls the aurora-licensing plugin's REST API (/wp-json/aurora-licensing/v1/validate|activate|deactivate) instead of treating any non-empty license_key as active. Result is cached in the aurora_admin_license_status option for 12 hours; is_active() and is_feature_available() read the cache instead of hitting the network on every admin page load. - Hooks update_option_aurora_admin_settings to activate/deactivate immediately when the license_key setting actually changes, rather than waiting out the cache. Instantiated in AuroraAdmin.php so that hook registers. - Settings page now surfaces the real status (active/invalid/unreachable, with the server's own message) as a status line under the License key field, instead of a field that silently does nothing. - Verified live against the real server: a fake test key correctly came back locked with the server's own "License key not found." message. Co-Authored-By: Claude Sonnet 5 <[email protected]>
46 KiB
Aurora Admin v2 — Project Status
Last updated: 2026-07-04 (latest session — real license-server integration)
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/. - License server access:
auroraplugins.comruns on the same cPanel box as the git remote,auroradragon.studio(hostnamewyvern.auroradragon.studio), but the git remote's SSH key (~/.ssh/id_ed25519) is scoped to git-only — no shell. Root shell access is a separate dedicated key,~/.ssh/id_ed25519_auroradragon_root(ssh -i ~/.ssh/id_ed25519_auroradragon_root [email protected]). The license server itself is theaurora-licensingWordPress plugin at/home/aurora/public_html/wp-content/plugins/aurora-licensing/aurora-licensing.php(also present under/home/plugins/public_html/...— a second copy on the same box). If SSH to port 22 suddenly times out, something on the box (fail2ban or similar) appears to re-trigger a short block on rapid repeated connection attempts, including plainnc/probe connections — it clears on its own after roughly 1-2 minutes; wait it out with one careful attempt rather than retrying in a tight loop.
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.- Real license-server integration (this session, supersedes the earlier
"any non-empty key counts as active" placeholder): validates against the
actual license server — the
aurora-licensingWordPress plugin running onauroraplugins.com(a cPanel box atauroradragon.studio, SSH- accessible as root via a dedicated key,~/.ssh/id_ed25519_auroradragon_root— the git-remote key is scoped git-only and can't get a shell). REST namespace/wp-json/aurora-licensing/v1/:POST /validate—{license_key, domain, product_id?}→{valid, status, plan, max_sites, active_sites, active_on_domain, expires_at, product_id}on success,{valid:false, error, message}on failure (e.g.invalid_key).POST /activate— same request shape →{activated, plan, max_sites, expires_at}, or{activated:true, message:"Already activated..."}if already active on this domain, or a 403{valid:false, error:"limit_reached", message}ifmax_sitesis exceeded.POST /deactivate—{license_key, domain}→{deactivated, message}.domainis normalized server-side (strips protocol/www./trailing slash) —License::domain()just sends the bare host fromhome_url().GET /my-licenses(cookie-auth, lists all of a customer's licenses) is the account-portal-facing endpoint, not used by the plugin itself.License::status()caches the/validateresponse in theaurora_admin_license_statusoption for 12 hours (STATUS_TTL) —is_active()/is_feature_available()read this cache rather than hitting the network on every admin page load.License::on_settings_updated()hooksupdate_option_aurora_admin_settings(same hookActivityLog.phpuses) and re-activates/deactivates immediately wheneverlicense_keyactually changes, rather than waiting out the cache TTL — deactivates the old key's slot on this domain before activating the new one, so switching keys doesn't leak an activation. Instantiated inAuroraAdmin.php(new License();) so this hook actually registers — it wasn't needed when the class was static-only.- Network failures/non-JSON responses collapse into a synthetic
{valid:false, status:"unreachable", ...}shape (License::call()) so callers only ever branch on response fields, never onWP_Error. - Settings UI feedback:
SettingsPage.phppassesLicense::status()to the Vue settings page aslicenseStatus;Settings.vueforwards it only to thelicense_keyfield;SettingsField.vueshows a colored status line under the field (green "License active (plan) — expires X", amber "unreachable, will retry", red the server's own error message) — verified live against the real server with a fake test key and got back the exact{"valid":false,"error":"invalid_key","message": "License key not found."}shape. - Not yet done: no real license key was available to test the
activate/site-limit-reached path end-to-end (only the invalid-key path
was verified against the live server) — worth a follow-up pass once a
real test key exists. No UI yet for the
limit_reachedcase beyond surfacing its message string.
- 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).
An earlier pass this session 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. That pass used a placeholder
is_active() (any non-empty key counted as licensed) and fixed an
unrelated pre-existing 500 in Role Editor's REST endpoint along the way
(pushed as commit 6d50260).
Most recently: replaced that placeholder with real validation against the
license server at auroraplugins.com (see "Real license-server
integration" above) — /validate//activate//deactivate calls, 12-hour
cached status, immediate re-check on a license-key change, and a status
line in the Settings UI. Verified live end-to-end against the real server
with a fake test key, which correctly came back locked with the server's
own "License key not found." message. 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 done: the activate/site-limit-reached path hasn't been tested
with a real, valid license key (only the invalid-key path was verified
against the live server) — worth a follow-up pass once a real test key is
available. No purchase/checkout flow lives in this repo (that's entirely
on the auroraplugins.com/WooCommerce side, which auto-generates keys on
order completion) — this repo only ever consumes the license API.