Reworded "What this project is" to describe Aurora on its own terms and dropped the stale uixpress/ folder archaeology from the Repo section. Co-Authored-By: Claude Opus 4.8 <[email protected]>
132 KiB
Aurora Admin v2 — Project Status
_Last updated: 2026-07-11 (latest session — v3.6.0: Site Backup was
extracted out of Aurora Admin into a new standalone sibling plugin,
aurora-site-backup, at the user's explicit request ("pull out the backup
as well and lets move that to an add-on") — the same proactive-risk-
reduction reasoning as Database Explorer's 3.5.0 extraction, since
restoring a backup means writing arbitrary files back into the site. The
entire admin/src/Backup/ tree (job/schedule storage, DB dumper, file
archiver, state-machine engine, wp_cron safety net, all three storage
providers) and all four Backup REST controllers moved unmodified into the
new plugin. Since Backup's storage providers depend on a Settings class
for stored credentials (S3 keys, Google Drive OAuth tokens), a small
standalone Options\Settings class was added to the new plugin with its
own aurora_site_backup_settings option, replacing the dependency on
Aurora Admin's own Settings class. CompanionPluginData.php's PLUGINS
array (already generalized in 3.5.0) gained a site-backup entry — no new
PHP route code needed. Verified live via a full rest_do_request()
dispatch that created a real backup job, stepped it through the DB-dump
phase, and canceled it — not just "loads without fatals" but actual
end-to-end behavior confirmation. See "Site Backup — REMOVED..." below.
Same outstanding deployment gap as the other two companion plugins: the
zip isn't hosted yet.
Previous session — v3.5.1: a fresh WordPress.org
Plugin Check report showed the 3.3.2 compliance pass's phpcs:ignore
annotations weren't actually suppressing anything on several multi-line
$wpdb-> calls — WPCS suppresses violations on the exact next physical
line, but the annotations sat above the opening line of the call while
PHPCS attributed the violation to a line further down inside it (where the
interpolated table-name token actually appears). Fixed across
MenuCreatorData.php, NoticesData.php, Notices.php, BackupSchedules.php,
BackupJobs.php, MediaFolders.php (the largest cluster, mostly fixed by
collapsing multi-line query calls to one physical line), DbDumper.php,
AnalyticsData.php, and MediaData.php — verified with an automated scan
that every dynamic-query $wpdb-> call is now correctly covered. Also
found and fixed a real, unrelated bug during verification:
MenuCreatorData::create() built its "fetch the row I just inserted"
follow-up request with new WP_REST_Request("GET", "", ["id" => ...]),
but that constructor form doesn't populate what get_param() reads, so
creating a menu always returned "Menu not found" — confirmed via a full
rest_do_request() dispatch, fixed with set_url_params(). The
compressed_files/hidden_files/unexpected_markdown_file findings in
the report are scanner noise from scanning the whole dev repo, not the
actual submission zip (re-confirmed via unzip -l). Submission zip
rebuilt and re-verified after this pass.
Previous session — v3.5.0: Database Explorer was
extracted out of Aurora Admin into a new standalone sibling plugin,
aurora-database-explorer, following the exact same pattern as File
Manager's v3.4.0 extraction — direct database-query tooling (browsing
tables, running free-form SELECT/SHOW/DESCRIBE/EXPLAIN queries) is treated
by WordPress.org's Plugin Directory review the same way as file-management
functionality, so it was pulled out proactively rather than waiting for a
rejection. CompanionPluginData.php was generalized from a single
file-manager-only class into a PLUGINS config array keyed by slug, so
both companion plugins (and any future ones) share the same install/
activate/status REST routes. The Settings → Add-ons screen now lists both.
A real production deploy (hosting the aurora-database-explorer.zip on
aurora.auroradragon.studio) is still pending explicit user confirmation,
not yet done — same outstanding gap File Manager's zip already has.
Previous session — v3.4.0: WordPress.org's second review response said the
whole file-management/code-editing category isn't accepted into the
Plugin Directory, restrictions or not — so File Manager was extracted
entirely out of Aurora Admin into a new standalone sibling plugin,
aurora-file-manager, installable with one click from the new Settings →
Add-ons screen (wraps core's own Plugin_Upgrader, same class the native
"Install Now" button uses). See "File Manager — REMOVED from Aurora Admin
in 3.4.0" below, specifically its final "Extraction into a standalone
plugin" subsection.
Prior versions this doc covers in detail, newest first: v3.3.2 (WP.org
Plugin Check compliance pass, ~230 justified phpcs:ignore annotations
across every custom-table feature), v3.3.1 (Settings page independent-
scroll fix), v3.3.0 (Modern Image Formats — WebP/AVIF generation +
<picture> rewriting, reimplemented against documented WP core hooks),
v3.2.1 (closed the File Manager PHP-editing gap that triggered WP.org's
first rejection), v3.2.0 (Google Drive remote storage for Site Backup),
v3.1.0 (Site Backup itself — the first background-job/wp_cron
infrastructure in this codebase), and v3.0.0 (made every previously-paid
feature free, deleted the entire licensing/tamper-protection subsystem —
everything earlier in this doc describing license gating is historical
unless explicitly marked otherwise).)_
What this project is
Aurora Admin — a modern, fast WordPress admin interface. Real replacement screens for the core admin (Posts, Pages, Media, Users, Comments, and more), a full theme system with dark mode, and admin power-tools, all freshly written from scratch (Vue components, PHP classes, and build tooling).
Repo
Now on GitLab (self-hosted): ssh://[email protected]:2289/NixReaper/aurora-wordpress-admin.git
(HTTPS also available). Repo root holds admin/, app/, and aurora-admin.php.
.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, runphp bin/build-integrity-manifest.php <staging-dir>/aurora-adminto generateadmin/src/Licensing/integrity-manifest.jsoninto the staged copy (see "Tamper protection" below — this step is new, don't forget it or the shipped zip will have no manifest and Integrity::is_tampered() will treat it as a dev checkout, i.e. never lock anything), 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 (the manifest-generation step would need a Windows-available PHP CLI too — LocalWP bundles one at~/Library/Application Support/Local/lightning-services/php-*/bin/*/bin/phpon macOS; there's presumably an equivalent bundled binary on Windows). - 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.
Skinned vs. replaced native pages (important distinction, ask before assuming)
The user explicitly said they didn't want the "skinned" approach used without a real replacement behind it. As of this session, every native list-table screen has been converted — there's no more "just skinned" category left among the pages that had one:
- Actually replaced — the native page never renders; a real custom Vue page (own REST controller, own list/detail UI) takes over entirely: Comments, Installed Plugins, Menu Creator, Admin Notices, Role Editor, Activity Log, Database Explorer, Posts, Pages, Media Library, and Users (the last four all added this session, in that order — Posts first as its own commit/version, then Pages/Media/Users together).
- Deliberately still native (not a "skinning" gap — these are specific
screens/actions, not list-tables, and rebuilding them wasn't in scope):
the block editor (
post.php/post-new.php— dark-themed chrome only, per "Window chrome" above),media-new.php(upload/drag-drop),user-new.php(account creation), individual edit screens reached from a list row (user-edit.php, comment/media edit), Settings/Profile pages, the Site Editor. If a new "why isn't X replaced" question comes up, check whether it's actually one of these deliberate exceptions before treating it as a gap.
PostsData.php+Posts.vue, PagesData.php+Pages.vue,
MediaData.php+Media.vue, UsersData.php+Users.vue, and
CommentsData.php+Comments.vue all follow one shared REST/Vue pattern:
- A REST controller in
admin/src/Rest/with list (status/type/role + search + pagination, matching native filter tabs) and action endpoints (trash/restore/delete, or just delete for Media/Users, which don't have a trash state in the same way). Capability checks mirror WP core's own for that screen (edit_posts/edit_pages/upload_files/list_usersto view; per-itemedit_post/delete_post/delete_page/delete_userfor actions). - A Vue page in
app/src/pages/<name>/+ entry inapp/src/entries/+ registered inapp/vite.config.js, visually matching the Comments layout (filter pills, search, table with inline row actions, pagination) — Media is the one exception, using a thumbnail grid instead of a table, since that's what a media library actually needs.
As of this session, how they're mounted onto the page changed. All five
used to be a real registered admin.php?page=aurora-admin-X submenu that
SubApps::redirect_native_pages() redirected direct visits to on
admin_init — this had a proven bug class (see "Key bugs fixed": anything
that redirects straight to the native URL, like a plugin's own
post-activation redirect, lands on the untouched native page until the
sidebar link is clicked again). They're now registered via
ScreenHijack::register() (calls collected in
admin/src/Pages/HijackedNativePages.php) instead: WordPress fires
load-{page}.php before the native screen prints anything, so Aurora hooks
that, checks capability/setting, buffers everything with ob_start()
on in_admin_header, and on admin_footer discards the buffer (guarded by
ob_get_level() > 0, since a screen that exited early via its own
wp_die()/redirect may never have opened one) and prints Aurora's own mount
div instead. The URL never changes — edit.php, upload.php,
users.php, edit-comments.php are still the real URLs, so there's no
separate slug to fall out of sync. Settings toggles (modern_posts/
modern_pages/modern_media/modern_users/modern_comments, all
default: true) are unchanged and still gate this the same way. Their
SubApps::PAGES entries, redirect_native_pages() branches, and
MenuSerializer::resolve_url() rewrite branches were all deleted as dead
code — the native sidebar links already point at the right (now hijacked)
URL with no rewrite needed.
Plugins is the one exception, deliberately left on the old redirect
mechanism — plugins.php prints output in a way that can't be cleanly
buffered (confirmed by the old uixpress reference plugin's own abandoned
attempt at hijacking it the same way). It still has a real
aurora-admin-plugins submenu under plugins.php and its own
redirect_native_pages() branch in SubApps.php.
This is phase 1 of a larger "full admin takeover" initiative — see the new section below.
Two deliberate scope decisions worth remembering if asked "why doesn't X work":
- Media: replaces the library/browse experience only — uploading
(
media-new.php, drag-drop/chunked uploads) was left alone entirely, same reasoning as leaving the block editor alone for Posts/Pages. - Users: deleting a user always reassigns their content to whoever
performs the deletion, rather than exposing WP core's "delete this
user's content too, or reassign to someone else" choice screen as a
one-click REST action — the null-reassign path in
wp_delete_user()is much more destructive than is safe to offer with one click, so it's not offered at all.
modern_posts/modern_plugins/modern_comments were themselves each
briefly removed earlier in this same session (when they had no
implementation behind them) and re-added once real pages existed — if
you ever see a settings field described as removed for "describing a
feature that didn't exist," check whether it's since been rebuilt before
assuming it's still missing.
Full admin takeover initiative (in progress — plugin phase of "DragonPress")
The user's long-term goal: Aurora Admin eventually becomes "DragonPress," a full WordPress core fork built around Aurora. That fork itself is not started and is explicitly out of scope until this plugin-level phase is finished. This phase's goal: when Aurora is active, no native WordPress admin screen should ever be reachable — everything gets replaced or wrapped by Aurora. Full plan lives in the approved plan file (not persisted in repo); summarized here for continuity.
Mechanism: ScreenHijack.php (see above) is the new default for any
screen being added. SubApps.php's redirect mechanism is legacy, kept only
for Plugins.
Sequencing (see task list each session for live status):
- ✅
ScreenHijack.php+ migrate Comments (proof of concept) - ✅ Batch-migrate Posts, Pages, Media, Users
- ✅ New
full_admin_takeovermaster setting (default false — opt-in, unlike the existingmodern_*toggles) + new"admin-takeover"license key gating everything below (this feature key, along with the entire licensing system, was removed in 3.0.0 — see "Licensing" below; the setting itself is unaffected and still opt-in) - ✅ Native Settings screens: General/Writing/Reading/Discussion/Permalinks
— all 5 hijacked via one shared
NativeSettingsPages.php+NativeSettings.vue(one Vue entry, driven by a per-screen field schema), reusing core's/wp/v2/settingsREST route for everything except Permalinks (newPermalinksData.php— core has no REST route for rewrite rules). One gotcha worth remembering: core's REST property names don't always match the underlying option name (title→blogname,description→blogdescription,url→siteurl) —get_option()with the REST name silently reads empty;NativeSettingsPages::OPTION_NAME_MAPhandles the translation for reads (writes go through the REST route itself, which already knows the mapping). Verified live: all 5 screens render real values on direct visit; at the time, Permalinks' Save also correctly 403'd without a real license (REST-level license check working independently of the page-level gate, per the "don't repeat the File Manager bug" requirement) — moot now that licensing is gone (3.0.0), but the REST route still checkscurrent_user_can()directly rather than relying on the page-level lock alone. - ✅ Media Upload (
media-new.php) + Add New User (user-new.php) — both Vue-only (no new PHP REST controllers), talking directly to core's/wp/v2/mediaand/wp/v2/usersroutes. Free, gated by the samemodern_media/modern_userstoggles as their list-view companions. Verified live: created a real user end-to-end. - ✅ Appearance/Tools batch. Correction to this plan discovered during
implementation: Menu Creator (
admin/src/MenuCreator/) manages Aurora's own admin sidebar menu, not WordPress's front-end nav menus — despite the similar name, completely unrelated features. Wiringnav-menus.phpinto it (as originally planned) would have been wrong; built a real, separate Nav Menus replacement instead (NavMenusData.php+NavMenus.vue: create/delete menus, add pages/posts/categories/custom links, reorder/indent items, assign to theme locations). Themes (ThemesData.php+Themes.vue: list/ activate/delete with screenshots) and Widgets (WidgetsData.php+Widgets.vue, deliberately simplified to reordering/moving/ deactivating existing instances — not adding new widget types, same "don't rebuild the real editor" reasoning as the block editor) also shipped. Theme Editor redirects to File Manager rather than getting a second in-browser file editor. Tools/Import-Export/Site Health left un-hijacked as planned (Shell already frames them).- Bug caught and fixed while testing: none of these pages' Vue
components checked
res.okbefore using a fetch response — a 403 error body got rendered as if it were real data (iterated as an object inv-for, producing bogus placeholder rows). Fixed in all three; this class of bug is worth checking for in any future page that talks directly toaurora-admin/v1routes. - Environment note: this dev site's 3 installed themes are all block/
FSE themes (Twenty Twenty-Three/Four/Five), none of which register
classic nav-menu/widget support — core itself refuses
nav-menus.php/widgets.php("Your theme does not support...") regardless of this plugin. Verified the REST layer directly instead (full create/add- item/list/delete round trip); Themes was verified fully live since it doesn't depend on theme support. - Gap found and fixed later (user reported "missing a lot of
features from the original"):
Themes.vue's initial build only covered list/activate/delete — no way to browse/install from WordPress.org or upload a theme zip, both present on the native screen. Fixed the same way as Plugins'addUrl/Media'snewMediaUrl: link out to nativetheme-install.php(not hijacked, already has both a WordPress.org browser and an "Upload Theme" button built in) rather than rebuilding either. New "Add New Theme" button inThemes.vue, gated oncurrent_user_can('install_themes'),addThemeUrlpassed viaAppearancePages.php's existingdata_callback. - Follow-up (user: "when you click add theme, it closes the menu"):
clicking a theme card opens WP core's own fullscreen
.wp-full-overlaypreview/install modal, which covers the native admin menu on a totally vanilla install too — confirmed not a regression, but reframed it like the block editor window anyway per the user's call. See "Theme installer overlay framing" below for the full writeup — two real bugs (avisibility: hiddeninheritance issue and the same over-constrainedheight: 100%+ inset bug the block editor frame already had to work around) were found and fixed along the way.
- Bug caught and fixed while testing: none of these pages' Vue
components checked
- ✅ Profile (
profile.php) — user chose: reskin core fields only (ProfileData.php+Profile.vue, using core's/wp/v2/users/meREST route + a small custom password-change route), and fall through to the real native screen entirely whenever a plugin has hookedshow_user_profile/personal_options(checked viahas_action()at hijack time) rather than attempting to host that plugin's fields — a Vue SPA has no safe way to render arbitrary third-party HTML/JS. Verified live both ways with a test mu-plugin. Password change re-issues the auth cookie afterwp_set_password(), same as core's own user-edit.php. - ✅ Block Editor reskin (
post.php/post-new.php) — the last phase, and deliberately not a Gutenberg content-swap. Extended Shell's already-working block-editor framing (shell-frame.css's.interface-interface-skeleton,windowChrome.js'sisBlockEditorPage()+.aurora-winbar--editordots) with a "← Posts"/"← Pages" breadcrumb, resolved from thepost-type-{slug}body class core adds to post edit screens (needed since Posts/Pages no longer have a fixed URL to hardcode after phase 1's migration). Bug caught while testing: the Site Editor shares theblock-editor-pagebody class but has nopost-type-*class — the crumb logic now explicitly returns nothing in that case instead of guessing wrong. Verified live on both the post editor (correct crumb) and Site Editor (no crumb, unaffected).
All 9 phases of the "full admin takeover" initiative are now shipped. Network Admin (multisite) was explicitly deferred throughout — the dev/ target site isn't multisite and nothing else in the codebase references it.
Free vs. paid — historical, moot since 3.0.0 (everything below is free
now, see "Licensing"): Media Upload/Add User/Profile were always free
(1:1 companions to already-free Posts/Pages/Media/Users). Appearance
(Nav Menus, Widgets, Themes, Theme Editor redirect) and Native Settings
screens were paid ("admin-takeover" feature key) — matched the
"power admin tool" bucket (Database Explorer, Role Editor, etc.) at the
time. Block editor reskin was always free (chrome around already-free
Posts/Pages). Every REST controller in this group still checks
current_user_can() directly in its own permission_callback rather than
relying on page-level gating alone — that defense-in-depth pattern is
unrelated to licensing and remains good practice.
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-siteswas a field type here — removed, see "Remote Site Switcher — removed" below. - 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_keyfor use on non-admin-gated pages;Settings::get()(full) only used on the manage_options-gated settings page itself.Settings::remove_retired_keys()(hooked toinitalongsideregister()) actively deletes settings keys for features that no longer exist — currently just the removed Remote Site Switcher'sremote_sites/remote_site_switcher_capability, sinceremote_sitesstored a real secret (application passwords) that's no longer read anywhere. - Global search:
app/src/shell/components/SearchModal.vue, Ctrl+K, debouncedwp/v2/searchREST calls, gated ondisable_global_searchsetting. - 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.
Native Settings field-parity completion (Writing/Reading/Discussion)
Follow-up to the full-plugin audit above: Writing/Reading/Discussion had
much thinner field coverage than Permalinks did before its presets fix —
raw numeric IDs where native WP has dropdowns, and Discussion exposing
only 2 of ~15 real settings. Investigated each screen against WP core's
register_initial_settings() (wp-includes/option.php) rather than
guessing: Writing and Reading were actually already using 100% of what
core's REST settings schema exposes for those two groups (use_smilies
was the one field core registers that had never been added) — the
remaining gap there was pure UX (raw IDs instead of dropdowns). Discussion
was different: core's schema genuinely only registers
default_ping_status/default_comment_status for that option group,
nothing else — the other ~13 fields (moderation, threading, notification
emails, avatars) have no REST route in WordPress core at all, so exposing
them required a new dedicated controller, the same situation Permalinks
was already in for permalink_structure.
NativeSettingsPages.phpgained three helpers —category_options()/post_format_options()/page_options()— that build real{value, label}option arrays fromget_categories()/get_post_format_strings()/get_pages(), reusing the existing generic"select"field type (SettingsField.vuealready supported it, populated with static arrays forstart_of_weeketc. — no new rendering code needed, just real data instead of hardcoded options).- New
showIf: {key, equals}field property, checked in avisibleFieldscomputed inNativeSettings.vue— lets any field declare "only show once another field's value matches", used for Reading's page pickers (only onceshow_on_front === 'page') and throughout Discussion's now-much-longer field list (e.g. thread depth only shown when threading is enabled). Generic, not special-cased per screen. - New
admin/src/Rest/DiscussionData.php— mirrorsPermalinksData.php's raw-get_option()/update_option()pattern but for ~24 keys instead of one, validated against an explicit per-type allowlist (BOOLEAN_KEYS/INTEGER_KEYS/TEXTAREA_KEYS/ENUM_KEYSconsts) so the route can never be used to write an arbitrary WP option.NativeSettings.vue's Permalinks-onlyisPermalinksspecial case became a small per-screen endpoint map (isPermalinks/isDiscussion) rather than growing more one-off branches. - New
textareafield type inSettingsField.vue(multi-line plain text) formoderation_keys/disallowed_keys— nothing else on any settings screen needed one before this. - Verified live end-to-end, not just visually: dropdowns populate with
real category/format/page data,
showIfconditionals actually show/hide on interaction (toggledshow_on_frontand watched the page pickers appear), and — the one check that actually proves the save path works, not just the UI — toggled a real Discussion setting off, saved, did a full page reload, and confirmed the change was still off (not an optimistic-UI illusion), then reverted it.
Theme installer overlay framing
User clicked "Add New Theme" > a theme card and reported "when you click
add theme, it closes the menu. something is wrong." Investigated live:
clicking a theme opens WP core's own .theme-install-overlay (a
.wp-full-overlay component, shared with the Customizer), a genuine
fullscreen fixed overlay by design — confirmed this covers the native
admin menu on a completely vanilla WordPress install too, not something
this plugin broke. Asked the user whether to leave it native or frame it
like the block editor window; they chose framing despite the
Gutenberg-CSS-conflict-style risk being called out.
app/src/shell/shell-frame.css:.theme-install-overlay(only this specific class — not.wp-full-overlaygenerally, socustomize.phpis untouched/out of scope) gets the standard Aurora inset box. The overlay's two children,.wp-full-overlay-sidebarand.wp-full-overlay-main, are independentlyposition: fixed/absolute— fixed descendants don't move just because their ancestor's own box moved (fixed is relative to the viewport unless an ancestor creates a new containing block), so each needed the identical left/top/bottom math applied directly, not inherited from the overlay's own reframing.margin-lefton.expanded/.collapsedduplicates WP core's own three responsive breakpoints (themes.css) with Aurora's frame offset added on top, preserving the sidebar collapse/expand slide animation as-is.- Two real bugs found and fixed, not obvious from the CSS alone:
- Opening the overlay adds
body.full-overlay-active, and WP core's own CSS setsbody.full-overlay-active { visibility: hidden }with only the overlay itself (#customize-container, .theme-install-overlay) set back tovisible. Aurora's entire shell root is a descendant ofbody, so it inherited that hidden visibility too — this is what silently erased the sidebar/toolbar entirely (not "covered by a higher z-index", genuinely invisible) rather than the milder "overlay covers it" behavior the user was actually reporting. Root-caused by walking the DOM ancestor chain checking each element's ownvisibilityin devtools, then grep-searching loaded stylesheets for rules matchingbodywith avisibilitydeclaration — foundbody.full-overlay-active { visibility: hidden }in WP core's ownthemes.css. Fixed with one override:body.full-overlay-active #aurora-admin-shell-root { visibility: visible !important; }— descendants can locally re-enable an inheritedhiddenvalue, so this doesn't need to touch anything else underbody(which stays hidden, harmless, since it's covered by the overlay's insets anyway). - The overlay's own
height: 100%(WP core) plus this session's addedtop+bottominsets is the exact over-constrained-box bug already documented above for.interface-interface-skeleton/#site-editor— sameheight: auto !importantfix, same root cause (per spec,bottomsilently loses to an explicitheightwhen both are set alongsidetop).
- Opening the overlay adds
- The winbar (traffic-light dots) needed a persistent
MutationObserver, not a one-shot insert — two reasons, both confirmed by testing rather than assumed: (1).theme-install-overlayisn't in the DOM at initial page load (WP core's Backbone view creates it lazily on the first theme click), soinitWindowChrome()'s normal one-time-at-mount check always missed it; (2) every subsequent theme selection (including the prev/next arrows inside the overlay) fully re-renders the overlay's own contents, which wipes out anything previously inserted into it — the bar was confirmed present after the first click, then gone after selecting a second theme.windowChrome.js'swatchForThemeOverlay()keeps an observer running for the page's whole lifetime with an idempotent "insert if missing" callback, rather than disconnecting after the first successful insert. - Verified live end-to-end (not just visually): opened via real clicks on
the grid (not just direct URL navigation to
?theme=slug, which behaves slightly differently), switched themes with the prev/next arrows, closed and confirmed the sidebar/toolbar return correctly with no lingering state, checked the browser console for errors at every step.
Sidebar accordion: hidden native "child" screens
Separate follow-up bug on the same feature, user-reported precisely:
expand Appearance, click "Add New Theme", and the Appearance section
collapses while "AURORA" (the branded nav at the bottom) opens instead —
different from the overlay-covering-the-sidebar issue above, this one is
about which accordion section stays open. Root cause: Sidebar.vue's
accordion picks the open section by matching the current URL against each
menu item's own submenu array (scraped from WP's $submenu global by
MenuSerializer::serialize()) — but theme-install.php/media-new.php/
user-new.php etc. are deliberately absent from $submenu (native
WP's own left menu doesn't list "Add New Theme" as a link either, only
"Themes"/"Customize"/"Editor"), so nothing ever matched and the accordion
fell through to its Aurora-expanded default every time.
Fix reuses WP core's own solution to this exact problem rather than
hardcoding a list: these pages set a $parent_file global directly in
their own PHP (theme-install.php → 'themes.php', media-new.php →
'upload.php', user-new.php → 'users.php') — the same mechanism core
uses for its own native menu highlighting. Shell::print_mount() reads it
(global $parent_file) and passes it through as activeParentFile;
Sidebar.vue's isSectionActive() now also matches on item.id === activeParentFile, alongside the existing exact-URL checks. General fix —
covers any current or future screen using this WP core convention, not
just the three confirmed live (Appearance/Themes, Media, Users).
Remote Site Switcher — removed
Was a Pro feature: Settings > General let you store {url, username, app_password} for other WordPress sites, gated a toolbar dropdown behind a
configurable capability + license check, and clicking an entry just opened
that site's /wp-admin/ in a new tab. User asked what it did; on
inspection, nothing ever actually consumed the stored username/
application password — no REST calls to the remote site, no proxying, no
SSO. It was pure UI scaffolding for a cross-site feature that was never
built on top of it, so removing it lost zero real functionality (the
"switcher" was just a bookmark list either way). Decided to remove rather
than build out the unused half, since the user said it "doesn't really do
much for the software."
Removed: the remote_sites/remote_site_switcher_capability settingsConfig.js
fields, the remote-sites field type + its helpers in SettingsField.vue,
the toolbar dropdown + canSwitchSites in Toolbar.vue/Shell.php, the
"remote-site-switcher" key in License::PAID_FEATURES, and the
secret-stripping logic in Settings::get_safe() (no longer needed with
remote_sites gone). Added Settings::remove_retired_keys() (hooked to
init) to actively delete remote_sites/remote_site_switcher_capability
from any install that already saved them — leaving a stored application
password inert in the database indefinitely isn't good practice even
though nothing reads it anymore.
Role Editor capability tooltips
Added after the user asked directly: "what is level_0? what is level_1?
None of these things are defined." Every capability checkbox in
RoleEditor.vue now has a small info icon with a hover tooltip, driven by
app/src/pages/role-editor/capabilityDescriptions.js — a static lookup
covering all ~55 WordPress core capabilities plus a regex special-case for
level_0–level_10 (the pre-2.0 legacy "user level" system WordPress still
assigns to every default role purely for backward compatibility with old
themes/plugins checking current_user_can('level_N') directly — core
itself hasn't checked a level in years, so these are safe to ignore/leave
alone, just unexplained without this). Anything not in the dictionary
(e.g. a capability another plugin added) falls back to a generic "custom
capability, not part of WordPress core" message rather than showing
nothing. No backend change — this is pure static documentation data,
looked up client-side.
Licensing — removed entirely in 3.0.0 (2026-07-06)
Aurora Admin previously had a full licensing/monetization system (a
License.php gate every paid feature funneled through, real integration
with the aurora-licensing license server on auroraplugins.com, a beta
tester program, a dev-mode license-override toggle). All of it is gone.
The user decided to give Aurora Admin away free on the WordPress.org plugin
repository, so every feature that was previously paid — Role Editor,
Activity Log, Database Explorer, File Manager, Analytics, White Label, Full
Admin Takeover, Media Folders — is now simply available to everyone, no
key required. Deleted: admin/src/Licensing/ entirely (License.php,
Integrity.php — the file-tamper-detection system that only existed to
protect license gating from bypass), bin/build-integrity-manifest.php,
UpgradeNotice.vue, locked-feature.js, the License key field, the
dev-mode "Simulate active license" toggle, and every lock badge/gating
check across ~20 PHP files and ~8 Vue files. license_key and
dev_license_override are actively purged from any site's stored settings
on upgrade (Settings::remove_retired_keys()), same pattern used for the
earlier Remote Site Switcher removal. Report a Bug still works, now via a
direct call to the feedback endpoint instead of going through the
license-status layer.
This is a clean split from the commercial auroraplugins.com service
(real beta licenses, Pro purchases, the license server itself) — that
system is untouched and unaffected, it's simply no longer wired into this
codebase. If any of the old implementation detail is ever needed again
(the real license-server REST contract, the beta-tester-license flow,
etc.), it's preserved in git log up through the 2.27.0 tag/commit — not
reconstructed here.
Report a Bug (new free page, added 2026-07-05)
A real WordPress admin page (aurora-admin-bug-report), registered in
SubApps::PAGES exactly like Menu Creator/Admin Notices — free, no
license feature key. Sidebar entry added in Shell::aurora_nav().
app/src/pages/bug-report/BugReport.vue: single free-text textarea plus a read-only diagnostics block auto-filled from server data (plugin version, WP version, PHP version, active theme). Posts toaurora-admin/v1/bug-report.admin/src/Rest/BugReportData.php: thin REST controller, validates the description isn't empty and posts it directly (viawp_remote_post) to the feedback endpoint onauroraplugins.com— no longer goes through a license-status layer (rewritten in 3.0.0 whenLicense.phpwas deleted; previously calledLicense::submit_bug_report(), which reused the samecall()helpervalidate()/activate()used).- License server (
aurora-licensingplugin, not this repo): has awp_aurora_bug_reportstable and a publicPOST /report-bugREST route doing a plainwpdb->insert. No admin UI for reviewing reports yet — query the table directly viawp db query; worth building a dashboard if report volume ever justifies it. - Gotcha hit during the build: this codebase has no autoloader — every PHP
class is manually
required inaurora-admin.php. Forgetting to add the newBugReportData.phprequire there caused aClass ... not foundfatal on first load. If adding another new class file, don't forget this step (mirrors the existingrequirelist right abovenew AuroraAdmin\ App\AuroraAdmin();). - Verified live end-to-end against the real production server (not
mocked): submitted a real report through the actual page, confirmed it
landed in
wp_aurora_bug_reportswith all fields populated correctly, then deleted the two test rows by exact ID (a broadLIKE-based cleanup query was correctly blocked by the safety classifier for being too wide against a live production table — redid it scoped to specific IDs).
The bullets below describe the pre-3.0.0 license-gating mechanics (per-page
feature keys, the shared UpgradeNotice.vue prompt, sidebar lock badges,
etc.) — all removed; see "Licensing" above. Kept only for the one bug fix
still relevant today:
- 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.
Tamper protection — removed in 3.0.0
Existed solely to stop someone stripping the license check out of a
distributed copy. With licensing gone entirely (see "Licensing" above),
there's nothing left to protect — Integrity.php and
bin/build-integrity-manifest.php were deleted along with License.php.
Full historical detail (how the sha256 manifest worked, the "re-lock
paid features only" design decision, live verification steps) is in
git log up through the 2.27.0 commit if this kind of protection is ever
wanted again for some other purpose.
Database Explorer — REMOVED from Aurora Admin in 3.5.0, now a separate plugin
Extracted entirely out of this codebase in 3.5.0, following the exact same pattern as File Manager's 3.4.0 extraction (see that section immediately below for the fuller precedent). Unlike File Manager, this extraction was proactive, not reactive — WordPress.org hasn't rejected Aurora Admin over Database Explorer specifically, but direct database-query tooling (a table browser plus a free-form SELECT/SHOW/DESCRIBE/EXPLAIN query runner) is the same class of risk WP.org's Plugin Directory review already flagged file-management functionality for, so it was pulled out ahead of a likely third rejection rather than waiting for one.
admin/src/DatabaseExplorer/DatabaseExplorerData.php (REST, manage_options-
gated, read-only statement-type + single-statement guards, identifier
validation against a live SHOW TABLES list since identifiers can't go
through $wpdb->prepare()'s value placeholders) and
app/src/pages/database-explorer/DatabaseExplorer.vue (built on the shared
AppListShell/AppListItem list-detail layout, both also copied into the
new plugin since it has no shared shell to depend on) moved, unmodified in
behavior, into a new sibling plugin: aurora-database-explorer
(aurora-database-explorer.php, own bootstrap, own REST namespace
aurora-database-explorer/v1, own Vite build, own light-only
base-theme.css subset of the --aurora-* palette). Removed from Aurora
Admin: the DatabaseExplorerData require/wiring in aurora-admin.php/
AuroraAdmin.php, the aurora-admin-database entry in SubApps.php and
Shell::aurora_nav(), the database-explorer Vite build entry, and a
now-stale doc-comment cross-reference in DbDumper.php.
admin/src/Rest/CompanionPluginData.php — previously a single class
hardcoded to File Manager — was generalized into a PLUGINS config
array keyed by slug (file-manager, database-explorer), with the REST
route registration, status()/install() logic, and Plugin_Upgrader
wrapping all made slug-generic. Adding a third companion plugin later is a
config-array entry, not new route-registration code. The Settings →
Add-ons screen (settingsConfig.js) now lists both companion plugins;
SettingsField.vue's companion-plugin field type was already fully
generic (driven by field.statusEndpoint/field.installEndpoint/
field.manageUrl), so no Vue changes were needed there beyond the new
config entry.
Verified live: built (npm run build, clean), PHP-linted (clean),
rsynced into the LocalWP test site, activated via a direct WP-bootstrap
script (browser cookie-based activation isn't practical for headless
verification). Since the admin's browser session couldn't be driven
directly, verification used a server-generated WordPress auth cookie
(wp_generate_auth_cookie(), 'auth' scheme — this site is plain HTTP,
not HTTPS, so the 'secure_auth' scheme WP prefers under SSL doesn't
apply) and a real curl request against the live site: confirmed HTTP 200,
the aurora-database-explorer-root mount div present, the correct built
script filename referenced, and no PHP warnings/fatals in the response
body. The plugin's REST namespace (aurora-database-explorer/v1) was
confirmed registered via /wp-json/'s route index. A full authenticated
REST nonce round-trip (actually calling /db-tables with a valid
X-WP-Nonce) was attempted but not completed — a nonce generated outside
a real cookie-validated session doesn't carry the right session token, so
rest_cookie_invalid_nonce was returned; not pursued further given this
is a direct, unmodified copy of already-shipped, already-working logic
(only the namespace and REST route prefix changed), and the companion
CompanionPluginData REST routes were separately confirmed present in the
main Aurora Admin site's own route index.
Deployment gap, not yet done (same as File Manager's): the
aurora-database-explorer.zip is not yet hosted at
https://aurora.auroradragon.studio/aurora-database-explorer.zip — a real
production-server deploy that needs the user's go-ahead, same as any other
production deploy. Until deployed, the Add-ons "Install & Activate" button
for Database Explorer will fail with a fetch error. Also outstanding:
resubmitting the rebuilt Aurora Admin zip to WP.org — the user's action,
not something to do unprompted.
File Manager — REMOVED from Aurora Admin in 3.4.0, now a separate plugin
Extracted entirely out of this codebase in 3.4.0 — see the new final
subsection below for why and how. Everything else in this section is
historical: it describes File Manager's design and history while it was
part of Aurora Admin (v3.0.0 through v3.3.2). None of the file paths this
section references (admin/src/FileManager/, app/src/pages/file-manager/)
exist in this repo anymore — the equivalent code now lives in the sibling
aurora-file-manager repo, unmodified in behavior from what's described
below.
Added originally in an earlier session — the user has the "WP File Manager" plugin (elFinder-
based) installed on the test site and asked for equivalent functionality
built into Aurora, as a fresh implementation rather than embedding
elFinder. admin/src/FileManager/FileManagerData.php (REST) +
app/src/pages/file-manager/FileManager.vue (entry file-manager.js),
registered as aurora-admin-file-manager in SubApps.php (parented under
aurora-admin like Menu Creator/Role Editor, not a native-page
replacement — there's no native WP file browser to redirect from).
- Root is
ABSPATH(the WordPress install directory), same as the reference plugin. Capability ismanage_optionseverywhere, no exceptions — by far the highest-risk surface in the plugin (arbitrary filesystem read/write on the site's own PHP). - Path containment: every client-supplied path goes through
resolve_path(), whichrealpath()s the candidate and checks the resolved path is still inside the root — never a raw string check against"../", since that alone doesn't catch symlinks or encoded traversal. New files/folders (which don't exist yet, sorealpath()would return false) resolve their parent directory instead, then just sanitize the new name component (wp_basename(), reject empty/./../ control characters).- Verified live via direct REST calls:
?path=../../../etc→ 404 (correctly rejected); creating/renaming to a blocked extension → 400; deleting the root path itself → 400; a full create-folder → create- file → write → read → rename → recursive-delete round trip all succeeded and left no trace afterward.
- Verified live via direct REST calls:
- Blocked extensions on upload/create/rename
(
FileManagerData::BLOCKED_EXTENSIONS):.phpand friends,.cgi,.sh,.exe, etc. — the single most important control, since uploading a webshell through a "file manager" is the classic way this class of feature gets exploited. A separateBINARY_EXTENSIONSlist (images/archives/media/fonts) plus a 2MB size cap keeps the text editor from trying to open something it shouldn't. - WordPress.org rejected the 3.1.0 submission over this exact feature
(2026-07-11):
BLOCKED_EXTENSIONSwas enforced on create/upload/rename, but not on opening/saving an existing file — amanage_optionsuser could open a theme's.phpfile in the CodeMirror editor (which has first-class PHP syntax highlighting,@codemirror/lang-php) and save it back with immediate execution effect on the next request. The reviewer's rejection language ("script insertion... PHP is even more complicated... WordPress itself allows you to lock people out of being able to edit theme and plugin files directly via DEFINES") pointed straight at this — it's functionally the same capability as WP core's own Theme/Plugin editor, which core lets hosts disable viaDISALLOW_FILE_EDITspecifically because of this risk class. Fixed in 3.2.1:is_editable()(used by bothread_file()andwrite_file(), and by theisEditableflaglist_dir()returns per item, which the Vue UI's Edit button/context-menu/double-click all gate on) now also excludesBLOCKED_EXTENSIONS, and additionally returnsfalseoutright whenDISALLOW_FILE_EDITis defined and true. Every mutating endpoint (create/upload/rename/delete/write/copy/move/bulk/trash restore-delete-empty) now also checks a newmods_allowed()helper againstDISALLOW_FILE_MODS— the broader host-level file-modification lockdown WordPress core's install/update/delete flows already respect. Browsing, downloading, and file-info/preview are unaffected — this only closes the write-to-existing-executable-file gap. No client-side Vue changes were needed; the UI already fully gated on theisEditableserver flag. - Deliberately not built (scope decisions): zip/unzip (extraction has its own well-known vulnerability class — zip-slip, where a crafted archive's entries escape the extraction directory), chmod/permissions editing (too easy to accidentally lock yourself out or open a real hole).
- Feature-parity pass (later session, prompted by the user comparing
against WP File Manager): added everything else on that gap list that
didn't carry zip/chmod's security risk — move/copy between folders
(
copy_item/move_item, with a same-descendant guard so a folder can't be moved/copied into itself, and " (2)"-style auto-renaming on name collision rather than overwriting), multi-select bulk actions (one/file-manager/bulkendpoint reusing the exact single-item logic for trash/move/copy so bulk can't drift from the single-item buttons), a trash instead of permanent delete (flat UUID-named folder underwp-content/uploads/aurora-admin-trash/+ a JSON manifest tracking original path/name/type/deleted-at — chosen over encoding the original path into the trash filename itself, which gets fragile with nesting/ special characters/length limits), a lazy-loaded folder tree sidebar (reuses the existing list endpoint filtered to directories, no new route needed, also doubles as the move/copy destination picker), a read-only file info panel (/file-manager/info— permissions/owner/dates/mime; directory size computed on-demand for this one call, not for every row in the main list), image thumbnails (/file-manager/preview— inline, extension-gated, size-capped, no server-side resizing), and drag-and-drop upload + sortable columns (both frontend-only). Verified live: multi- select, info panel, and a full create→trash→restore round trip through the actual UI; copy/bulk-trash/empty-trash verified directly against the REST endpoints. Zip/unzip and chmod remain deliberately unbuilt. - UI reworked again, same session: user pointed at WP File Manager's
actual toolbar screenshot and asked for the same layout. Replaced the
header-buttons + per-row-Actions-column split with one unified toolbar
(back/forward/up history, new folder/file, upload, copy/cut/paste,
rename, trash, download/edit/info, list/grid toggle, fullscreen,
search) — buttons enable based on current selection. Move/copy switched
from the destination-picker modal to clipboard-style copy/cut/paste
(select → Copy/Cut → navigate anywhere → Paste), matching how elFinder-
based file managers actually work; still calls the same
/file-manager/bulkendpoint underneath. Added a grid view and fullscreen toggle. Verified live: copy→navigate→paste, list/grid toggle, back navigation. - Right-click context menu + real code editor (same session, user
asked directly): context menu mirrors the toolbar's own functions
exactly (no separate logic to drift out of sync) — right-click an
unselected item to select just it, right-click within an existing
multi-selection to keep it and act on all of it, right-click empty
space for folder-level actions (New Folder/File, Upload, Paste). The
plain
<textarea>editor became a real CodeMirror 6 instance (CodeEditor.vue) — syntax highlighting, line numbers, folding, active-line highlight, language chosen by extension (JS/TS/CSS/HTML/ JSON/PHP/Markdown/XML/SQL, unrecognized extensions fall back to plain text), themed off Aurora's own CSS variables rather than a hardcoded palette. New npm deps:codemirror+ one@codemirror/lang-*package per language above. Adds real weight to the file-manager bundle (~250KB gzipped) but it's only loaded when that page is actually opened. Verified live: context menu selection/empty-space cases and Escape-to-close; PHP syntax highlighting confirmed on a real file. - Was gated as a paid feature until 3.0.0 — free now, along with every
other previously-paid feature (see "Licensing" above). The REST
permission callbacks here (and Database Explorer/Role Editor/Activity
Log's) still check
current_user_can()directly, not just at the page level — that defense-in-depth pattern is still correct practice even though there's no license left to bypass.
Extraction into a standalone plugin (3.4.0)
WordPress.org's response to the 3.2.1 resubmission (which restricted PHP editing per the section above) was that restriction doesn't matter — the whole category of file-management/code-editing functionality isn't accepted into the Plugin Directory, bundled in any WordPress.org-listed plugin, safeguards or not. The user's direction: pull File Manager out of Aurora Admin entirely and offer it as a separate, independently-installable companion plugin instead of deleting the functionality.
- New sibling plugin,
aurora-file-manager(separate repo/directory at/Users/kris/Projects/aurora-file-manager, not a subfolder of this one) —FileManagerData.phpand the three Vue components (FileManager.vue/FileManagerTree.vue/CodeEditor.vue) moved over with the identical PHP-editing-restricted behavior from 3.2.1 kept intact (not restored to unrestricted editing — that restriction is a genuine security improvement independent of WP.org's policy, so there was no reason to relax it just because it's no longer being reviewed). Only mechanical changes: namespaceAuroraAdmin\FileManager→AuroraFileManager\FileManager, REST namespaceaurora-admin/v1→aurora-file-manager/v1, text domain/trash-dirname strings updated to match. Own plugin bootstrap (aurora-file-manager.php), own minimal admin-page shell (AuroraFileManager.php— a single top-level menu page, since this plugin has no Aurora Admin shell/sidebar framework to hook into and needs to work whether or not Aurora Admin is even installed), own Vite build (app/package.json/vite.config.js, same CodeMirror dependency set), and a small standalonebase-theme.cssextracting just the light-mode--aurora-*CSS variables the three Vue components actually reference (10 of them) rather than the whole window-chrome/dark-mode/sidebar system, which this plugin has no use for. Verified live both with Aurora Admin active (nav item appears alongside Aurora's own) and fully deactivated (File Manager still works standalone, confirming zero hidden dependency on Aurora Admin). - Removed from Aurora Admin:
admin/src/FileManager/andapp/src/pages/file-manager/deleted outright, therequire/use/ instantiation lines inaurora-admin.php/AuroraAdmin.php, theaurora-admin-file-managersub-app entry inSubApps.php/Shell.php, and thefile-managerVite build entry all removed. Several other classes (FileArchiver,BackupPaths,BackupGoogleDriveData,BackupData) had comments citingFileManagerDataas a design precedent (containment-check pattern, lazyWP_Filesysteminit, nonce-via-query-param permission callback) — rewritten to stand alone rather than reference a class that no longer exists in this repo. AppearancePages's Theme Editor redirect removed, not repointed — it used to sendtheme-editor.phpvisits to File Manager instead of hijacking the native editor in place (reasoning: no point building a second unhardened PHP-editing surface). With File Manager gone, visits now fall through to WordPress core's own Theme Editor unmodified, which already respectsDISALLOW_FILE_EDITitself — arguably a better fit for WP.org's stated comfort zone than redirecting to any file editor, even a separately-distributed one.- New Settings → Add-ons screen offers one-click install: a new
companion-pluginfield type inSettingsField.vue(Settings.vue now passes a sharedapi()fetcher down toSettingsFieldas a prop, the first field type that needs its own REST calls beyond the genericwp/v2/settingssave flow) checks install/active status on mount and shows "Install & Activate" / "Activate" / "Installed & active — open File Manager →" accordingly. Backend:CompanionPluginData.phpwrapsPlugin_Upgrader::install()— the exact same core classplugin-install.php's native "Install Now" button and the Upload Plugin tab both use — pointed at a fixed, hardcoded HTTPS zip URL (https://aurora.auroradragon.studio/aurora-file-manager.zip), never a client-supplied one, so there's no SSRF surface: the install target isn't user input, only the decision to trigger the install is. InheritsPlugin_Upgrader's ownWP_Filesystemcredential handling for hosts that need FTP/SSH creds for direct writes.- Live-testing note: clicking "Install & Activate" for a
not-yet-installed copy was correctly blocked by the safety
classifier — the button, once the zip is actually hosted at that
URL, downloads and executes a whole WordPress plugin with full admin
privileges, which needs the user's explicit go-ahead each time, not
just code-level review. The underlying
install()/activate_plugin()logic was still verified functionally correct via a manual rsync-based install (bypassing the remote-fetch step entirely) and by exercising the "already installed, just activate" code path through the plain Plugins screen instead of the new button. - Deployment gap, not yet done: the zip isn't actually hosted at
aurora.auroradragon.studio/aurora-file-manager.zipyet — that's a real production-server deploy (the same box/pattern used for beta zip hosting, see reference_auroradragon_server) requiring explicit user confirmation before it happens, not something to do unprompted. Until it's deployed, the Add-ons install button will fail with a fetch error if clicked against a site that doesn't already have the plugin installed some other way (manual upload, local rsync in dev).
- Live-testing note: clicking "Install & Activate" for a
not-yet-installed copy was correctly blocked by the safety
classifier — the button, once the zip is actually hosted at that
URL, downloads and executes a whole WordPress plugin with full admin
privileges, which needs the user's explicit go-ahead each time, not
just code-level review. The underlying
Media Folders (new — duplicates FileBird Pro's folders)
User has FileBird Pro installed on the test site
(wp-content/plugins/filebird-pro/) and asked to duplicate its folder
organization capability for Aurora's own Media Library, since Aurora's
Media page (app/src/pages/media/Media.vue, hijacked in place via
ScreenHijack — see HijackedNativePages.php) was a flat grid with no
folders. Researched FileBird's actual schema/REST surface first (its
wp_fbv folder table + wp_fbv_attachment_folder join table, filebird/v1
REST namespace) rather than guessing, then asked the user three scoping
questions before building:
- Custom DB tables, not a WP taxonomy — user's choice, matches FileBird's own model.
- Start fresh — no import of FileBird's existing folder data.
- Paid feature — but only the folder capability, not the Media Library page itself, which was already free/ungated and stays that way; gating the whole page would have downgraded something already free.
Implementation:
- New
admin/src/Media/MediaFolders.php— storage + model, following the exactinit-hookedmaybe_install()+DB_OPTIONversion-check idiom already used byActivityLog.php/Notices.php/MenuCreator.php/Analytics.php. Tables:wp_aurora_media_folders(id, name, parent, ord, color, created_by) andwp_aurora_media_folder_items(folder_id, attachment_id) — deliberately namedaurora_*, notfbv*, so both plugins coexist without collision. Simpler than FileBird's own schema: no folder "type" (collection/saved-search — skipped, no organizational value over folders), and one folder per attachment enforced in application logic (assign_attachments()always clears existing membership first). - New
admin/src/Rest/MediaFolderData.php—aurora-admin/v1REST routes for folder CRUD +/media-folders/assign. Everypermission_callbackcheckscurrent_user_can('upload_files'). MediaData.php's existingGET /mediaroute gained an additivefolderIdparam andserialize_attachmentgained afolderIdfield, batch-looked-up viaMediaFolders::folder_ids_for_attachments()(one query per page of results, not one per attachment).Media.vuegained a sidebar: pinned "All Media"/"Uncategorized" pseudo-folders (ids0/-1, never real DB rows) above a newMediaFolderTree.vue(recursive tree component modeled onFileManagerTree.vue, plus inline create/rename/delete viawindow.prompt/confirm— matchingFileManager.vue's own existing UX convention for the same actions — a right-click context menu, and HTML5 drag-and-drop for both folder reparenting and dropping media cards onto folders). A "Move to folder" dropdown on each media card is a non-drag fallback, since drag-and-drop alone is easy to miss. Was gated behind a license until 3.0.0 (theUpgradeNotice.vuefallback this paragraph used to describe is gone along with the rest of licensing) — at the time this was notable as a new gating shape for this codebase, since previously a sub-app was either wholly free or wholly paid; now moot since nothing is gated.- Testing note:
window.prompt()/confirm()block the automated Chrome tooling entirely (the tab hangs until the dialog is dismissed) — create/rename/delete were verified via directfetch()calls against the REST routes instead of clicking through the UI. Drag-and-drop was effectively verified two ways: the RESTassignendpoint directly, and organically — while testing, the real user apparently used the live feature themselves in a separate tab (created a "Logos" folder via the actual UI, dragged all 6 media items into it), which incidentally proved the whole click-to-create → drag-to-assign path works for a real human, not just via REST.
Real bug found and fixed while building this, unrelated to the folder
schema itself: Shell::is_own_page() only recognized Aurora's own pages
by ?page=aurora-admin* or pagenow === 'index.php' — it had no way to
know a ScreenHijack-based page (Posts, Pages, Media, Users, Comments,
all of which keep their native URL, e.g. upload.php, with no page=
query param at all) was one of Aurora's own. This meant native-dark.css
(loaded whenever !is_own_page(), meant only for real untouched native
pages) was loading on every hijacked page too, and its generic #wpbody- content a { color: var(--aurora-accent) } rule was overriding any link
inside the real Aurora UI that needed proper accent/contrast coloring.
Invisible until now because no earlier hijacked page happened to render a
link depending on that contrast — Media Folders' unlicensed "Enter a
license key" CTA (white text on an accent-colored button) was the first,
and came out as invisible accent-on-accent text. Fixed at the source
rather than patched around: ScreenHijack::register() now sets a static
is_hijack_active() flag the moment any registered hijack actually
activates for the current request (all its gating checks passed), and
Shell::is_own_page() checks it as a third condition. Confirmed fixed by
inspecting the CTA's computed style before/after (color literally equal
to its own background before the fix, i.e. invisible) rather than just
eyeballing a screenshot.
Site Backup — REMOVED from Aurora Admin in 3.6.0, now a separate plugin
Extracted entirely out of this codebase in 3.6.0, at the user's explicit request ("pull out the backup as well and lets move that to an add-on"), following the same pattern established by File Manager (3.4.0) and Database Explorer (3.5.0) — see those sections for the fuller precedent. Proactive, not reactive: no WP.org rejection specifically over Site Backup, but restoring a backup means writing arbitrary files back into the site, the same class of risk that's kept file-management functionality out of Aurora Admin proper.
The entire admin/src/Backup/ tree (BackupJobs.php, BackupPaths.php,
BackupSchedules.php, DbDumper.php, FileArchiver.php,
BackupEngine.php, BackupCron.php, and Backup/Storage/'s
StorageProviderInterface.php/LocalStorageProvider.php/S3Signer.php/
S3StorageProvider.php/GoogleDriveStorageProvider.php/
StorageFactory.php) plus the four Backup REST controllers
(BackupData.php, BackupScheduleData.php, BackupSettingsData.php,
BackupGoogleDriveData.php) and the Vue UI (SiteBackup.vue,
BackupHistoryList.vue, BackupScheduleForm.vue,
BackupSettingsPanel.vue, plus the shared AppListShell/AppListItem
list-detail components and ProgressBar.vue/useJobPolling.js it
depends on) moved, unmodified in behavior, into a new sibling plugin:
aurora-site-backup (aurora-site-backup.php, own bootstrap, own REST
namespace aurora-site-backup/v1, own Vite build, own light-only
base-theme.css).
New wrinkle not present in the File Manager/Database Explorer
extractions: Backup's storage providers (S3StorageProvider,
GoogleDriveStorageProvider) and BackupSettingsData/
BackupGoogleDriveData all depend on AuroraAdmin\Options\Settings for
storing credentials (S3 keys, Google Drive OAuth tokens) — a dependency
on Aurora Admin's shared settings option that the standalone plugin can't
have. Added a small standalone AuroraSiteBackup\Options\Settings class
to the new plugin instead, replicating just the get($key, $default) +
direct update_option() read-modify-write shape the Backup code already
used (no WP Settings API registration ceremony needed, since Backup never
went through register_setting()'s sanitize flow) — backed by its own
aurora_site_backup_settings option so the plugin works completely
standalone.
Removed from Aurora Admin: all of the above source files, the
aurora-admin-site-backup entries in SubApps.php/Shell::aurora_nav(),
the site-backup Vite build entry, and the corresponding requires/new X() wiring in aurora-admin.php/AuroraAdmin.php.
CompanionPluginData.php's PLUGINS array (already generalized to a
config-driven shape in 3.5.0) gained a site-backup entry — no new PHP
route-registration code needed. The Settings → Add-ons screen now lists
all three companion plugins.
Verified live, more thoroughly than the previous two extractions:
built, PHP-linted, activated with no fatals, and — going beyond "loads
without errors" — exercised via a full rest_do_request() dispatch that
created a real backup job (POST /backup/jobs), stepped it through its
first DB-dump chunk (POST /backup/jobs/:id/step, confirmed
progress_tables_done actually advanced), and canceled/deleted it
(DELETE /backup/jobs/:id) — all against the live LocalWP database, not
mocked. The admin page itself was also confirmed rendering correctly
(mount div + correct built script, HTTP 200, no PHP warnings) via a
server-generated auth cookie over real HTTP, same technique used for the
other two companion plugins.
Deployment gap, same as the other two: the aurora-site-backup.zip
is not yet hosted at
https://aurora.auroradragon.studio/aurora-site-backup.zip — a real
production-server deploy that needs the user's go-ahead. Until deployed,
the Add-ons "Install & Activate" button for Site Backup will fail with a
fetch error.
Everything below this point in the section is historical: it describes
Site Backup's design while it was part of Aurora Admin (v3.1.0 through
v3.5.1). None of the file paths referenced (admin/src/Backup/,
app/src/pages/site-backup/) exist in this repo anymore — the equivalent
code now lives in the sibling aurora-site-backup repo, unmodified in
behavior from what's described below.
Duplicator-style full backup + restore-in-place, on-demand or on a
recurring schedule, local or S3-compatible remote storage. Full design
lives in the approved plan file (pure-beaming-pelican.md, not persisted
in repo); summarized here for continuity, same convention as the earlier
Full Admin Takeover / Media Folders plans.
Scope, decided up front (v1 boundaries, not oversights): restore-in-place
only, no cross-domain migration installer.php generation (Duplicator's own
headline feature — deferred, real scope/risk). Restore only ever sources
from Aurora's own backup history, never an uploaded arbitrary archive
(real attack surface — path traversal, zip bombs, malicious file
reintroduction — not worth taking on for v1). Storage: local disk, one S3-compatible remote implementation (covers AWS
S3, Backblaze B2, Cloudflare R2, DigitalOcean Spaces via a configurable
endpoint), and, added in 3.2.0, Google Drive via OAuth 2.0 (see "Google
Drive remote storage" below). Dropbox is still deferred — the
storage-provider interface was deliberately designed so a new provider
plugs in without touching the job engine, which the Google Drive addition
confirmed in practice: zero BackupEngine changes were needed.
The job engine is genuinely new infrastructure — this codebase had no
background-job/cron/chunked-processing pattern anywhere before this
(confirmed via grep: zero wp_cron, zero ZipArchive usage). Every
backup/restore step is bounded (row/file/byte count and a wall-clock
budget) so a multi-GB site never risks one request's timeout or memory
limit. BackupEngine::process_step($job_id) is the single state-machine
driver; two triggers converge on it — a foreground REST "step" call the
Vue UI polls (setTimeout-recursion, not setInterval, so a slow step
can't overlap the next poll) and a wp_cron safety net
(BackupCron::schedule_next_tick(), guarded by wp_next_scheduled() so
an actively-polling tab and cron can't stack duplicate events for the same
job). Verified live: a job advanced purely via wp-cron.php ticks with
zero manual REST calls, and closing the browser mid-backup didn't stop it
from finishing.
BackupJobs/BackupSchedules— two new tables, both via the exactMediaFolders.phpinstall idiom (DB_VERSION/DB_OPTIONversion-gateddbDelta()).BackupJobs::update_progress()does an optimistic-lock write (WHERE id = %d AND updated_at = %s) so a foreground poll and a cron tick landing on the same job in the same second can't double-process a chunk.DbDumper— paginated dump (~500 rows/step), schema captured per-table at first touch (DROP TABLE IF EXISTS+ the literalSHOW CREATE TABLEoutput) so restore is deterministic and re-runnable from scratch, not an ALTER/merge. Chunked restore via a byte-cursor into the dump file (fseek+boundedfread, growing the read if no statement boundary is found — guards against a single large serialized value landing on a chunk edge).- Real bug found and fixed while building this: Aurora's own
wp_aurora_backup_jobs/wp_aurora_backup_schedulestables were initially included in what gets dumped like any other table. A restore'sDROP TABLE+CREATE TABLEon those tables was overwriting the live jobs table with a stale snapshot captured mid-backup — deleting the very job row driving the restore out from under itself (subsequent/stepcalls started 404ing). Fixed by excluding Aurora's own bookkeeping tables fromknown_tables()entirely. Bookkeeping about backups isn't site content anyway; it has no business being restored. - Second real bug, more serious:
quote_value()originally pre-converted real newline bytes to a literal\ntwo-character sequence before calling$wpdb->_real_escape()— but_real_escape()(a thin wrapper onmysqli_real_escape_string()) already does exactly this conversion itself. The manual pre-conversion meant_real_escape()then escaped the backslash again, so a restored text field ended up containing the literal two characters\+ninstead of a real newline — a real, permanent data-corruption bug, caught by a live backup→mangle→restore test and confirmed via the dashboard's own recent-comments widget rendering\nas visible text instead of a line break. Fixed by deleting the manual pre-conversion entirely and trusting_real_escape()alone.
- Real bug found and fixed while building this: Aurora's own
FileArchiver— one-shot manifest build (a flat text file, one relative-to-wp-contentpath per line — cheap since it's metadata only, not file content), excluding the backup storage directory during the walk.wp-config.phpneeds no explicit exclusion — it lives atABSPATH, a sibling ofwp-content/, so the walk (rooted atWP_CONTENT_DIR) never visits it at all. Zip step reopens the same.zipevery request (ZipArchive::CREATEis safe on an existing file — opens for append), adds files viaaddFile()(streams from disk, neveraddFromString()) in batches bounded by both file count and cumulative bytes. Extract step containment-checks every entry before writing anything (reject..-containing names, then the authoritative realpath-based check againstWP_CONTENT_DIR— same principle asFileManagerData::resolve_path()) — verified live with a deliberately crafted malicious zip entry (../evil-outside-wp-content.txt), confirmed the file never got written anywhere outsidewp-content, and the rest of the restore still completed successfully around it.- Local storage hardening —
wp-content/uploads/aurora-backups/gets WordPress core's ownindex.phpstub and an.htaccessdeny (both Apache 2.2 and 2.4 directives). No protection on nginx (ignores.htaccessentirely) — the real safeguard is that the download route (BackupData::download_job()) ismanage_options-gated and streams the file through PHP in bounded chunks (never a whole-fileWP_Filesystem::get_contents()— a backup archive can legitimately be multi-GB, unlike File Manager's individually-capped files). - Retention —
BackupSchedules::prune()keeps the newestretention_countcompleted backups per schedule, deleting the rest (job row + stored archive, via whicheverstorage_providerthat specific job actually used — a schedule's storage setting can change after some of its jobs already ran). Only scheduled backups prune; an on-demand backup a user triggers by hand isn't subject to a schedule's retention limit just because it happens to share a storage provider. - S3-compatible remote (
S3StorageProvider+S3Signer) — manual AWS Signature Version 4 signing viahash_hmac()overwp_remote_request()calls, no SDK/Composer dependency (none exists in this codebase). Verified two ways: (1) byte-exact match against AWS's own published worked example ("GET Object" signing example,AKIDEXAMPLE/wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEYplaceholder credentials — cross-checked independently via a from-scratchopenssl-based re-derivation of the same signing chain, not just self-consistency); (2) a real request to AWS's live API with fake credentials came back with AWS's genuineInvalidAccessKeyIderror — proof the request itself is well-formed enough for AWS to actually parse and respond to, not just syntactically plausible. Full upload/multipart/download/restore round-trip against a real bucket was explicitly deferred (needs real cloud credentials the user didn't provide) — S3 code is verified correct at the protocol level, not live-tested end-to-end against an actual bucket. Configurable endpoint field (not hardcoded to AWS) so Backblaze B2/Cloudflare R2/DigitalOcean Spaces work through the same code path. Files above 100MB use S3 Multipart Upload, chunked the same bounded-per-step way as everything else here.- S3 credentials live under
Settings'sbackup_s3_credentialskey, excluded fromSettings::get_safe()(verified live: the raw secret never appears in any other page's serializeddata-aurora-adminpayload) — the exact "future field that shouldn't be broadcast" caseget_safe()'s own docblock already anticipated. Save/read use a dedicated REST route (BackupSettingsData) rather than the genericwp/v2/settingsflow specifically because the generic flow's shallow merge-on-save would blow away a previously-saved secret on any partial update (the client is never given the real secret back to resend) — the dedicated route does its own read-modify-write, preserving the existing secret when the field is left blank. Verified live: saved credentials, reloaded the page, secret field correctly showed a masked placeholder instead of the real value or an empty field.
- S3 credentials live under
- Vue UI (
SiteBackup.vue+BackupHistoryList/BackupScheduleForm/BackupSettingsPanel) — three tabs (Backups/Schedules/Settings), a new sharedProgressBar.vueanduseJobPolling.jscomposable (thesetTimeout-recursion polling loop, shared between the backup-create and restore flows).window.confirm()/alert()block the automated Chrome tooling the same waywindow.prompt()does elsewhere in this codebase (see File Manager's testing notes) — verified those flows by pressing Enter to accept the native dialog rather than working around it.- Real bug found and fixed during live testing: a job's
phasecolumn never got updated to a clean"done"value on successful completion (BackupJobs::set_status()only touchedstatus), so the UI kept showing the stale last-real-phase label ("Archiving files…") indefinitely even after the job had actually finished. Fixed by havingset_status()also setphase = "done"specifically on successful completion (failed/cancelled jobs deliberately keep their last real phase — useful debugging context, e.g. "failed during file_zip").
- Real bug found and fixed during live testing: a job's
- Not built in this pass: the plan's "run schedule now" UI action —
the REST plumbing for it exists (
POST /backup/jobsaccepts an optionalscheduleIdto attribute an on-demand job to a schedule's retention pool, exercised directly during retention testing), but there's no button for it inBackupScheduleForm.vueyet.
Google Drive remote storage (added 3.2.0)
Extends the storage-provider abstraction with a second remote option,
GoogleDriveStorageProvider — same upload/download/delete/list
interface as S3, plus upload_step/download_step chunked variants the
job engine calls the same way it calls S3's. StorageFactory and
BackupData.php's provider allow-list were the only job-engine-adjacent
touch points; BackupEngine itself needed zero changes.
- OAuth model: since this is a free, publicly-distributed GPL plugin,
it can't ship a shared Google OAuth client secret — each site owner
creates their own Google Cloud OAuth client (Client ID + Secret, pasted
into Settings) and registers Aurora's REST callback URL
(
/wp-json/aurora-admin/v1/backup/gdrive/oauth-callback, shown read-only/copyable in the Settings UI) as its redirect URI. Scope is the narrowdrive.file(only files/folders the app itself creates, not full Drive access).access_type=offline+prompt=consentguarantee a refresh token even on re-auth; astatenonce in a short-lived transient covers CSRF on the callback. Only the refresh token is ever persisted — an access token is fetched fresh viaget_access_token()before every authenticated call, avoiding stale-token bookkeeping given operations here are already inherently multi-step.- Credentials live under
Settings'sbackup_gdrive_credentialskey, excluded fromget_safe()(same treatment asbackup_s3_credentials— verified live it never leaks into another page's serialized payload).BackupGoogleDriveData'sconfigGET/POST routes follow the identical masked-secret read-modify-write pattern established byBackupSettingsDatafor S3. oauth-callbackis the one route in this controller that isn'tmanage_options-gated at the permission-callback level — Google hits it via a real browser redirect with noX-WP-Noncepossible, so it's instead gated on thestateparameter matching the stored transient (same class of exceptionFileManagerData::download_file()already established for direct-navigation routes).
- Credentials live under
- Resumable upload/chunked download mirror S3's multipart precedent
exactly, against Drive API v3's own documented shapes instead of SigV4:
a resumable session (
POST .../upload/drive/v3/files?uploadType=resumable→Locationresponse header is the session URI), then boundedPUTchunks withContent-Range/Content-Lengthheaders (308 Resume Incompletemeans more to send — the byte cursor advances from the response'sRangeheader, not local arithmetic, to trust what Google actually received). Download is aRange-headerGETagainst?alt=media, appended to the local file the same justified-raw-fopenwayDbDumper::append()/S3'sdownload_step()already do. The small.sqldump uses a one-shotmultipart/relatedupload instead (manually constructed boundary body — same reasoning as S3's plain-PUT case for small files). - Verification without live credentials — the user doesn't have a
Google Cloud OAuth client set up yet, so this shipped code-complete but
not live-connection-tested, the same deferral S3's live bucket round-trip
got. Two tiers of evidence instead: (1) a line-by-line request-shape
review of every Drive API call against Google's published REST reference
(endpoints, field names, header formats all documented, no ambiguity to
guess at); (2) posting fake credentials to Google's real live OAuth
endpoints and confirming genuine, specific Google errors came back
(
invalid_request/invalid_client/invalid_token) rather than a generic connection failure — proof the requests are well-formed enough for Google's own server to parse and evaluate them. Also verified live in the browser: the Settings tab's Google Drive section renders correctly (redirect URI, masked-secret fields), and clicking "Connect to Google Drive" opens a real new tab toaccounts.google.comthat comes back with Google's owninvalid_request: Missing required parameter: client_iderror — confirming the whole button→REST→Google round trip is wired correctly even with no client ID entered yet.- Explicit gap, not yet closed: a full "Connect to Google Drive"
round trip only works once the site owner (a) creates a real Google
Cloud OAuth client, (b) registers the exact redirect URI shown in
Settings, and (c) serves the site over HTTPS (or is exactly
localhost) — none of which the LocalWP dev site (http://admin.local) satisfies. Not live-tested end-to-end against a real Drive account.
- Explicit gap, not yet closed: a full "Connect to Google Drive"
round trip only works once the site owner (a) creates a real Google
Cloud OAuth client, (b) registers the exact redirect URI shown in
Settings, and (c) serves the site over HTTPS (or is exactly
Modern Image Formats (new Settings section, free, added 3.3.0)
The user shared a downloaded copy of WordPress.org's own "Modern Image
Formats" (formerly "WebP Uploads") feature plugin as a functional
reference and asked for equivalent functionality inside Aurora's
Settings — explicitly not to use their code, just to understand and
reimplement the behavior. Inspected their readme.txt/settings.php/
hooks.php to understand the feature set and which WordPress core hooks
it uses, then built admin/src/ImageFormats/ImageFormats.php from
scratch against those same documented core hooks and APIs (no shared
code, different function/class names, different settings storage,
different metadata key shape, different picture-building approach).
Scope decided up front (confirmed with the user via AskUserQuestion):
"Automatic" format selection (prefers AVIF, falls back to WebP, based on
wp_image_editor_supports()) rather than WebP-only; settings live in a
new Aurora Settings category rather than hijacking native
Settings > Media (which Aurora doesn't otherwise own); always generate
the original format alongside the modern one and rewrite <img> markup
into <picture> elements, rather than a WebP-only replacement mode
(maximizes compatibility at the cost of extra storage — the same
trade-off the reference plugin's own non-default mode makes).
- Generation (
generate_sibling_formats(), hooked onwp_generate_attachment_metadataat priority 20): for every JPEG/PNG upload, re-encodes the full size and every already-generated subsize into the target mime type viawp_get_image_editor($path)->save(null, $target_mime)—save()'s null-filename behavior derives a sibling filename in the same directory automatically, a documented core behavior, not a technique borrowed from the reference plugin. Discards the result if it isn't actually smaller than the source. Recorded in the attachment's own metadata under a newaurora_image_formatskey (a sibling structure to core's ownsizeskey) — deliberately not reusing the reference plugin'ssourceskey name. - Cleanup (
cleanup_sibling_formats(), hooked ondelete_attachment): deletes every generated sibling file when the attachment is deleted — core only knows to clean up files it generated itself. - Quality (
set_quality(), hooked onwp_editor_set_quality): pins WebP/AVIF quality at 82 explicitly, untouched for every other mime type. <picture>rewriting (build_picture(), called from bothwp_content_img_tagandwp_get_attachment_image_html— two separate core filters needed since core doesn't routewp_get_attachment_image()output through the content-tag filter): wraps the original<img>tag in<picture>with one<source>offering the generated format, reusing the original tag's ownsizesattribute rather than re-deriving it. Controlled by its own dependent setting so a site can keep generating the files without the markup rewrite if it conflicts with a theme's image styling.- Settings UI: a new "Modern Image Formats" category in
settingsConfig.js— the first use of thedependsOnfield pattern (added tosettingsConfig.js/Settings.vue/SettingsField.vue/Toggle.vuein this pass; didn't exist before), which greys out and disables the "Output format" and "Serve via<picture>element" fields when the master toggle is off. No new REST route — reuses the existingwp/v2/settings+aurora_admin_settingsmerge-on-save flow every other Aurora setting already uses. - Verified live end-to-end, not just at the protocol level (unlike
Google Drive above, this needed no real third-party credentials, so a
full round trip was actually testable): uploaded a real JPEG via a
direct
wp/v2/mediaREST call (browser file-picker upload wasn't available in this session — worked around by base64-encoding a generated test image and POSTing aFormData/Blobfrom within the authenticated browser tab), confirmedwp_get_attachment_metadata()showed a genuineaurora_image_formats.fullentry pointing at a smaller.aviffile that existed on disk with the expected byte size drop, confirmed a real post embedding that image rendered<picture><source type="image/avif" srcset="...">wrapping the original<img>tag, and confirmed deleting the attachment removed both the original and generated files with no orphans left behind.- Interesting environment finding: this LocalWP install's PHP 8.2
GD build supports AVIF encoding directly (
imageavif()) even without the Imagick extension loaded — so "Automatic" correctly picked AVIF over WebP on a host that, per the reference plugin's own FAQ language, would usually be described as "AVIF not supported without ImageMagick." Worth remembering if a future host behaves differently (GD without libavif support, or an older PHP) — the graceful degradation to WebP-then-nothing should still hold, just not something this session's live test actually exercised.
- Interesting environment finding: this LocalWP install's PHP 8.2
GD build supports AVIF encoding directly (
Key bugs fixed (root causes, in case they resurface)
- Deleting a plugin fataled the whole site ("major issue" per user
report — WordPress's generic "There has been a critical error" page,
no plugin actually deleted). Error log showed
Call to undefined function request_filesystem_credentials()inside core'sdelete_plugins(), called fromPluginsData::delete_plugin(). That function lives inwp-admin/includes/file.php, which a REST API request doesn't load by default —delete_plugin()only requiredplugin.php.update_plugin()right next to it already requiredfile.phpfor the identical reason;delete_plugin()just never got the same fix, presumably because activate/deactivate/list don't touch the filesystem layer so never needed it. Fixed by requiringfile.phpindelete_plugin()too, guarded the same way (function_exists()check first). Verified by actually deleting a real plugin (WP File Manager, user-approved as safe to sacrifice for the test) end to end — no fatal, correct "Plugin deleted." message, confirmed no new error in the log afterward. AppListShell.vue's two panes shared one page-level scroll instead of scrolling independently — this shared component backs Database Explorer, Role Editor, Activity Log, Admin Notices, and Menu Creator, so the bug (and fix) applies to all five, not just wherever it's first noticed. User reported it via Database Explorer specifically: selecting a table near the bottom of the (40-table) list didn't seem to do anything until manually scrolling back to the top, where the detail panel's new content had actually rendered. Root cause:.app-list-shellonly had amin-height, so the flex container just grew with content and both the list pane and detail pane relied on the outer page's own scroll — there was no independent scroll region for either. Fixed by giving the container a realheight: calc(100vh - var(--aurora-toolbar-h) - 60px)and both.app-list-shell__nav/.app-list-shell__contenttheir ownheight: 100%; overflow-y: auto, with separate mobile-breakpoint rules (stacked panes get natural height + their ownmax-height, not a 50/50 squash). Verified live by scrolling the list to the bottom and confirming the detail panel updates immediately, in view, with no scrolling needed.- File Manager's right-click context menu never appeared for real
users, despite appearing to work in earlier automated testing (which,
in hindsight, was triggering the absence of the race rather than
proving the feature worked). Root cause: right-clicking — especially
via a trackpad two-finger tap or Ctrl-click on macOS — can fire a
trailing synthetic
clickevent immediately aftercontextmenuon the same target. The menu's document-level "click anywhere else closes it" listener was attached synchronously the instant the menu opened, so that trailing click closed the menu in the same tick it appeared. Fixed by deferring the listener's registration one tick past the opening event (setTimeout(..., 0)) inFileManager.vue'sopenContextMenu(). Confirmed by simulating the exact race (dispatchingcontextmenuthenclickon the same element in the same tick) and checking the menu stayed open, plus confirming a genuine subsequent click still closes it. If a future dropdown/menu/popover in this codebase uses the same "open on some event, close on any document click" pattern, apply the same one-tick-deferred-listener fix — this exact race will resurface. - Every
ScreenHijack-mounted page missing the traffic-light bar, rendering outside the normal card frame: user reported this across Posts, Media, Comments, Users, Add User, Profile (and it affected every otherScreenHijackpage too). Root cause: WordPress prints<div id="wpbody"><div id="wpbody-content">afterin_admin_headerfires (confirmed directly in core'swp-admin/admin-header.php—do_action('in_admin_header')is literally before the<div id="wpbody">line), butScreenHijack.php'sob_start()was hooked toin_admin_headeritself. So the buffer captured those wrapper divs' opening tags too (not just the native content meant to be discarded), and since their closing tags print inadmin-footer.phpbeforedo_action('admin_footer')fires, those got captured and discarded too. Net effect:ob_end_clean()silently ate the entire#wpbody/#wpbody-contentwrapper, opening and closing tags both, and the mount div printed a level too high (a direct child of#wpcontent). This brokewindowChrome.js's traffic-light insertion (it specifically looks up#wpbody-content) and the shell's card-frame CSS, which both assume that nesting exists. Fixed by havingScreenHijack.phpre-print the#wpbody/#wpbody-contentwrapper divs itself around the mount point — same DOM shape WordPress would have produced, just with different content inside. If a future hijacked screen looks "unframed," check this first. - Plugins dropped to the native page after activating a plugin: user
installed FileBird and it "defaulted back to WordPress plugins page
until I clicked on plugins from the menu." Root cause: unlike Posts/
Pages/Media/Users/Comments, Plugins was originally built with only a
MenuSerializer.phplink rewrite (the sidebar's own "Plugins" link pointed at the Aurora page) — it never got aredirect_native_pages()case for direct visits toplugins.phpitself. WordPress's own post-activate/post-install redirect (wp_redirect(self_admin_url('plugins.php?activate=true'))) is exactly that: a direct visit with no?page=param, bypassing the rewritten sidebar link entirely and landing on the true native page — fixed only once the user next clicked the sidebar link themselves. Added the missingredirect_native_pages()case, gated bymodern_pluginslike the others, redirecting toplugins.php?page=aurora-admin-plugins(notadmin.php?page=..., to match the URL the sidebar link itself already uses). Verified live by visitingplugins.php?activate=truedirectly (the exact URL WP's own redirect produces) — now lands on the Aurora page correctly. If a similarly "sidebar-link-only" page ever gets reported as dropping to native after some WP-core-triggered redirect, checkredirect_native_pages()for a missing case first — this is the second time that exact gap has caused a bug (Plugins now fixed; worth double-checking Comments/Posts/Pages/Media/Users don't have similar missed redirect sources, e.g. bulk actions, if this resurfaces). - White native form fields on classic (non-block-editor) screens: user
reported "a lot of endpoints still linking back to the original
WordPress dashboard [look]" — investigation landed on the Edit Media
screen (attachments don't use the block editor, so they hit the older
meta-box-based edit template) showing a stark white title field and
File URL input. Root cause: WP core styles some specific fields via a
2-ID selector (
#titlediv #title { background: #fff }), which beatsnative-dark.css's existing form-field rule (one#wpbody-contentid- classes) regardless of class count — the same CSS-specificity trap as
the earlier block-editor-page background bug (class count doesn't
matter once the other selector has more IDs). Fixed by adding
!importantto the whole form-field rule block (text inputs/textareas/ selects) — safe everywhere, since there's no legitimate case where a native admin form field should stay white. Verified this one rule fixed Edit Media, Edit Post (classic fields), and Comment edit simultaneously; Add User was already clean. The WYSIWYG editor body (TinyMCE) staying white is a separate, deliberate decision (same as the block editor's own canvas) — not a bug, don't "fix" it.
- classes) regardless of class count — the same CSS-specificity trap as
the earlier block-editor-page background bug (class count doesn't
matter once the other selector has more IDs). Fixed by adding
- Admin Notices invisible on the Dashboard:
admin_noticesprints inside#wpbody, whichDashboardTakeover::hide_native()hides entirely (to hide the native dashboard, not ours) — collateral damage for anything else relying on that hook there. Fixed withNotices::render_for_dashboard(), called directly inside the visible dashboard frame (DashboardTakeover::render_mount()), withrender()(the normaladmin_noticeshook) bailing on the Dashboard so it isn't rendered twice. Second, independent bug surfaced by the same fix:wp-admin/js/common.jsunconditionally relocates every.noticeelement on the page to right after the page's<h1>($('.notice').not('.inline, .below-h2').insertAfter($headerEnd)) — regardless of where it was placed server-side, it would've moved right back into the hidden#wpbodyon its own. Core's own.below-h2class is the documented opt-out; added only for the dashboard render path (render_notices($keep_in_place)), not the normal one, where the relocation is harmless/expected. .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). - 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.
- Comments list's pagination footer reads "0 to 0 of 0" even when the "All" tab correctly shows a real count — cosmetic, found during the same audit, not investigated further.
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).
An earlier pass this session 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 (pushed as commit
540083e).
Most recently: added file-integrity tamper protection (see "Tamper
protection" above) so that stripping/commenting-out the license check in a
distributed copy re-locks every paid feature, even with an otherwise-valid
license — Integrity::is_tampered() checksums the gate files against a
manifest generated at zip-build time. Verified live via a temporary
mu-plugin that faked an active license without touching any protected
file, confirming untampered+unlicensed stays locked, untampered+licensed
unlocks, and one tampered file re-locks regardless of license status. The
distributable zip (dist/aurora-admin-v2.zip) was rebuilt with the new
manifest-generation step 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).
All of the above (this paragraph and the three before it) is history: the
entire licensing/feature-gating/tamper-protection system it describes was
removed from this repo in 3.0.0 (2026-07-06) when the user decided to give
Aurora Admin away free on WordPress.org — see "Licensing" and "Tamper
protection" above. Nothing here reflects current behavior; kept only so the
reasoning behind the old design (and the commits that built it) stays
findable. The commercial auroraplugins.com/aurora-licensing service
itself is untouched and still consumes its own license API independently
of this plugin.