Files
aurora-wordpress-admin/PROJECT_STATUS.md
T

2085 lines
132 KiB
Markdown
Raw Normal View History

2026-07-03 02:53:29 -05:00
# 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).)_
2026-07-03 02:53:29 -05:00
## What this project is
A ground-up rebuild of the "uiXpress" WordPress admin-theme plugin, relaunched
as **Aurora Admin**. The original plugin's look/feel and feature set are the
target, but **no original code is reused** — all Vue components, PHP classes,
and build tooling are freshly written. This lives in `aurora-admin-v2/` (the
earlier in-place rebrand attempt, `aurora-admin-working/`, is superseded and
no longer the active codebase).
## Repo
Now on GitLab (self-hosted): `ssh://[email protected]:2289/NixReaper/aurora-wordpress-admin.git`
(HTTPS also available). Repo root = the contents of `aurora-admin-v2/`
(`admin/`, `app/`, `aurora-admin.php`), not the whole `uixpress/` folder — that
folder has a bunch of dead-end attempts (`aurora-admin-working`,
`aurora-admin-distribution`, `aurora-admin-rewrite`, `aurora-admin-GLM`,
`uixpress`) that are debugging history, not part of the project.
`.gitignore` excludes `app/node_modules/` and `app/dist/` (build output —
regenerate, don't version). To clone on a new machine: add an SSH key to the
GitLab account, then `git clone` the URL above, then `cd app && npm install`.
Claude Code conversation history does **not** sync across machines — it's
local per project-folder-per-machine. Point a fresh session at this file to
get back up to speed instead of relying on prior chat history.
2026-07-03 02:53:29 -05:00
## Workflow rules established
- **Always verify live in Chrome** before declaring a fix done. Use the
Chrome MCP tools (`tabs_context_mcp`, `navigate`, `computer`/screenshot,
`javascript_exec`) to inspect real DOM/computed styles rather than
guessing from source.
- **Zip packaging**: on macOS, build via `npm run build` in `app/`, `rsync`
the repo (excluding `node_modules`, `.git`, `/dist`) into a scratch staging
dir, **run `php bin/build-integrity-manifest.php <staging-dir>/aurora-admin`
to generate `admin/src/Licensing/integrity-manifest.json` into 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), then `zip -r -X`
it into `dist/aurora-admin-v2.zip` — macOS `zip` produces forward-slash
paths natively, no path-separator corruption issue like Windows
`Compress-Archive`/PowerShell had. **Always delete the existing zip
first** (`rm -f dist/aurora-admin-v2.zip`) — `zip -r` merges into an
existing archive rather than replacing it, so stale entries (old content
hashes) silently survive otherwise. If building on Windows again, the old
PowerShell `System.IO.Compression.ZipFile` + `.Replace('\', '/')` approach
from earlier sessions still applies (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/php`
on 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 like `admin/assets/native-dark.css`) and just reload the page in
Chrome — near-instant feedback loop. Rezip properly (see above) once the
change is confirmed, so the zip artifact matches what's live-verified.
- User manually reinstalls the zip from a downloaded/attached copy when
actually deploying — **the Chrome MCP `file_upload` tool cannot push
arbitrary local files into the browser**, confirmed in an earlier session,
so that step can't be automated. (Hot-patching, above, sidesteps this for
local dev/verification.)
- **Old build caching gotcha**: WordPress serves whatever plugin files were
last installed — if the user says "didn't fix it," suspect a stale zip
still installed before suspecting the code. Verify the live script hash via
`document.querySelector('script[src*="shell"]')` + computed styles when in
doubt (done successfully this session to prove a spacing change was live).
- **Git**: local repo config only (`user.name`/`user.email` set per-repo, not
`--global`). Commit after each user-confirmed-working change, not before.
2026-07-03 02:53:29 -05:00
- Site under test: `http://admin.local/wp-admin/`.
- **License server access**: `auroraplugins.com` runs on the same cPanel
box as the git remote, `auroradragon.studio` (hostname
`wyvern.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 the `aurora-licensing` WordPress 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 plain `nc`/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.
2026-07-03 02:53:29 -05:00
## Architecture
- **PHP** (`admin/src/...`, namespaced `AuroraAdmin\...`), bootstrapped from
`aurora-admin.php` (manual `require` list, dependency order matters),
instantiated in `admin/src/App/AuroraAdmin.php`'s constructor.
- **Vue 3** app (`app/src/...`), Vite multi-entry build (`app/vite.config.js`
`rollupOptions.input`) → `manifest.json`, read by
`admin/src/Utility/Assets.php::enqueue()` which also walks the manifest's
`imports` chain to pull in shared-chunk CSS. Data reaches each Vue entry via
a `data-aurora-admin` JSON attribute on a mount div (never `wp_localize_script`).
- **Single global stylesheet**: `app/src/shell/shell-frame.css` — all
2026-07-03 02:53:29 -05:00
`--aurora-*` CSS variables (incl. `.aurora-dark` overrides) and the only
rules touching WP structural IDs (`#wpcontent`, `#wpbody-content`). No
element resets/utility classes leak onto native WP pages.
2026-07-03 02:53:29 -05:00
- **Immersive shell** (`admin/src/Shell/Shell.php` + `app/src/shell/SiteShell.vue`
+ `Sidebar.vue` + `Toolbar.vue` + `UserMenu.vue`): hides native WP chrome,
mounts a Vue sidebar/toolbar on every admin page.
- `MenuSerializer.php` converts WP's `$menu`/`$submenu` globals into JSON
for the sidebar's native-menu section — explicitly skips the
`aurora-admin` slug so Aurora's own pages aren't duplicated (they're
represented in the sidebar's own branded nav section instead).
- Sidebar has two sections: the WP-native menu list (icon-chip style,
28×28px rounded icon backgrounds, tight ~2px vertical padding per item)
and a bottom "Aurora" section (collapsible, expanded by default,
persisted via `localStorage['aurora-nav-expanded']`) styled like a
**native WP submenu** (plain indented text via `.sidebar__sublist`/
`.sidebar__sublink`, no icons) — deliberately matches how e.g. Posts'
own submenu (All Posts/Add Post/Categories/Tags) looks, per explicit user
request.
- Sidebar is `display:flex; flex-direction:column`, split into a
`.sidebar__scroll` (flex:1, scrollable — holds brand/native-list/Aurora
section) and a pinned `UserMenu` footer below it (not part of the
scrollable area, so the account card is always visible).
- `UserMenu.vue`: floating card that expands **upward** from a bottom-left
trigger (avatar+name+chevron). Card contains: user header (avatar/name/
email), links (Edit Profile → `profile.php`, Visit site, Site Settings →
`options-general.php`, Sign Out in red). No theme control here anymore —
see "Theme system" below; the old light/dark/system 3-way swatch picker
and the `dark_mode` setting it drove are both gone entirely.
- Old top-right toolbar user-dropdown and sun/moon toggle button are
**removed** — fully replaced by the bottom-left `UserMenu`.
- Sidebar is **inset**, not flush against the browser edge: fixed at
`top/left/bottom: var(--aurora-sidebar-gap)` (16px), fully rounded
corners, `background: var(--aurora-card-bg)` — floats as its own card
over the page gradient. `--aurora-sidebar-gap` is a shared CSS var
(`shell-frame.css`) that the toolbar's `left` offset and `#wpcontent`'s
`margin-left` both add too, so nothing overlaps when the gap changes.
- **Native-menu accordion**: only one WP-native-menu section's submenu is
open at a time (`Sidebar.vue`'s `openId` ref) — expanding one collapses
whichever else was open, instead of the list growing indefinitely as
more sections get expanded. Was previously a `Set` of independently
toggleable ids (each section remembered its own open/closed state);
replaced with a single id so opening any section closes the rest.
Starts on whichever section matches the current page (each nav is a
full page load in this multi-page admin, so this is computed once at
mount, not reactively). The separate "Aurora" branded nav section below
it keeps its own independent expand/collapse state
(`localStorage['aurora-nav-expanded']`) — not part of this accordion.
## 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_users`
to view; per-item `edit_post`/`delete_post`/`delete_page`/`delete_user`
for actions).
- A Vue page in `app/src/pages/<name>/` + entry in `app/src/entries/` +
registered in `app/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):
1. ✅ `ScreenHijack.php` + migrate Comments (proof of concept)
2. ✅ Batch-migrate Posts, Pages, Media, Users
3. ✅ New `full_admin_takeover` master setting (default **false** — opt-in,
unlike the existing `modern_*` 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)
4. ✅ 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/settings` REST route for everything
except Permalinks (new `PermalinksData.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_MAP`
handles 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 checks `current_user_can()` directly rather than
relying on the page-level lock alone.
5. ✅ 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/media` and `/wp/v2/users` routes. Free, gated by the same
`modern_media`/`modern_users` toggles as their list-view companions.
Verified live: created a real user end-to-end.
6. ✅ 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. Wiring
`nav-menus.php` into 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.ok` before using a fetch response — a 403
error body got rendered as if it were real data (iterated as an
object in `v-for`, producing bogus placeholder rows). Fixed in all
three; this class of bug is worth checking for in any future page
that talks directly to `aurora-admin/v1` routes.
- 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's
`newMediaUrl`: link out to native `theme-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 in `Themes.vue`, gated on `current_user_can('install_themes')`,
`addThemeUrl` passed via `AppearancePages.php`'s existing
`data_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-overlay`
preview/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
(a `visibility: hidden` inheritance issue and the same over-constrained
`height: 100%` + inset bug the block editor frame already had to
work around) were found and fixed along the way.
7. ✅ Profile (`profile.php`) — user chose: reskin core fields only
(`ProfileData.php` + `Profile.vue`, using core's `/wp/v2/users/me` REST
route + a small custom password-change route), and fall through to the
real native screen entirely whenever a plugin has hooked
`show_user_profile`/`personal_options` (checked via `has_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 after `wp_set_password()`, same as core's own user-edit.php.
8. ✅ 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`'s
`isBlockEditorPage()` + `.aurora-winbar--editor` dots) with a
"← Posts"/"← Pages" breadcrumb, resolved from the `post-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 the `block-editor-page`
body class but has no `post-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`: `themePresets` array (10 hue-based
presets like `{ id: 'indigo', label: 'Indigo', hue: 235 }`, plus `mono` and
`white` with an explicit `palette` object instead of a hue). `buildPalette(preset)`
returns all `--aurora-*` tokens — for hue presets, computed via HSL ramps;
for the two flat presets, returned as-is. `resolvePreset(id)` looks up by id.
- The main app background (`--aurora-app-bg`) is a **3-stop gradient**, not
flat: `linear-gradient(135deg, vivid1 0%, vivid2 55%, near-black 100%)` —
two bright, saturated stops of the same hue family carry most of the bar,
dipping to a near-black anchor only in the last stretch. This exact ratio
went through several corrections against a competing plugin's ("FleekDash")
reference screenshots — a 50/50 dark-to-vivid split reads "washed out", two
competing saturated hues ~70° apart reads "muddy"; this formula was the one
the user confirmed as clean. If revisiting the gradient formula, re-check
against those reference screenshots in the conversation history rather than
re-deriving from scratch.
- `--aurora-card-bg-deep`: a newer token, one step darker than
`--aurora-card-bg`, used only by the Dashboard's `AuroraCard.vue`. Needed
because the Dashboard got its own frame background using `--aurora-frame-bg`
(see "Window chrome" below), which is close in lightness to `--aurora-card-bg`
for most presets — cards would nearly disappear into the new frame without it.
- `applyThemePalette(themePresetId)` / `applyFontFamily(family)`
(`app/src/pages/settings/applyLiveTheme.js`) — shared between the settings
page's live preview (`Settings.vue`'s `watch()`) and the real per-page-load
application (`SiteShell.vue::onMounted`). Sets each token via
`document.documentElement.style.setProperty`.
- Google Fonts picker: `app/src/pages/settings/googleFonts.js` (curated ~70
family list, not the full catalog), searchable dropdown in
`SettingsField.vue`'s `font-picker` field type, applied by injecting a
Google Fonts CSS2 `<link>` tag + setting `--aurora-font-family`.
- **Recurring bug class this session — "independently-painted gradient"**:
several elements (sidebar, toolbar, `#wpbody-content` on the settings page,
`#wpbody-content` in general) each used to paint their *own* copy of
`var(--aurora-app-bg)`. Since it's a gradient, each element computes it
independently over *its own* bounding box — a narrow tall sidebar and a
wide short toolbar get visibly different-looking slices of "the same"
gradient, creating seams at their shared edges. Fixed by making `html`/`body`
the *only* thing that paints the gradient (`background-attachment: fixed`
so it doesn't scroll away from the fixed sidebar/toolbar), and leaving
every other element transparent so that one gradient plane shows through.
**If a new seam/mismatched-rectangle bug shows up, suspect this pattern
first** — grep for `var(--aurora-app-bg)` outside `shell-frame.css`'s
`html.aurora-ready, html.aurora-ready body` rule; it shouldn't be there.
2026-07-03 02:53:29 -05:00
- **Dashboard takeover** (`admin/src/Pages/DashboardTakeover.php` +
`app/src/pages/dashboard/Dashboard.vue`): hijacks native `index.php` via
`in_admin_header`, hides the native `.wrap`, renders the Aurora dashboard
in its place. 12-col grid; cards are single `<section class="aurora-card">`
elements with inline `grid-column: span N` (NOT wrapped — component
attribute fallthrough for styles was unreliable). Never use unprefixed
class names on Aurora components (`.card` collided with WP core once).
2026-07-03 02:53:29 -05:00
- **Settings page** (`admin/src/Pages/SettingsPage.php` + `app/src/pages/
settings/Settings.vue`): owns the top-level "Aurora Admin" WP admin menu
directly via `add_menu_page` (slug `aurora-admin`). Two-box layout:
`.settings__nav` (category list) + `.settings__content` (fields), both
boxed/bordered on a darker backdrop. Field schema is declarative in
`app/src/pages/settings/settingsConfig.js`, rendered by
`app/src/pages/settings/components/SettingsField.vue` (types: text, toggle,
color, image, repeater, multiselect, segmented). `remote-sites` was 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 `$submenu` global —
**no longer hidden via `remove_submenu_page()`**. An earlier version did
call it (the standard register-but-hide trick) but that corrupts WP
core's own `user_can_access_admin_page()` capability lookup, which partly
depends on the page still being present in `$submenu` to resolve its
required capability — removing it caused "Sorry, you are not allowed to
access this page" for every user, including admins, on Menu Creator/
Activity Log/Role Editor/Comments. Root-caused and fixed this session by
deleting `hide_from_native_submenu()` entirely; duplication in the
sidebar's native-menu section is instead prevented in
`MenuSerializer::serialize()` (see below), which has no such side effect.
- All slugs prefixed `aurora-admin-*` so `Shell::is_own_page()`'s existing
`strpos($page, "aurora-admin") === 0` check covers them for free (full
custom dark theme, `native-dark.css` skipped). `MenuSerializer::serialize()`
skips any submenu slug starting `aurora-admin-` (in addition to already
skipping the `aurora-admin` top-level slug) so none of them are
duplicated in the sidebar's native-menu section — needed because most are
parented under `aurora-admin` (already skipped wholesale) but
`aurora-admin-plugins` is parented under the real `plugins.php` menu.
- Each page uses the shared two-pane layout `app/src/pages/_shared/AppListShell.vue`
(generalized from Settings.vue's box-pair CSS) + `AppListItem.vue` for
list rows.
- **Menu Creator** (`admin/src/MenuCreator/`): DB table
`aurora_admin_menus` (name/status/roles JSON/items JSON). Runtime
`admin_menu` hook (priority `PHP_INT_MAX`) applies published configs
matching the current user's roles — hides/relabels native menu items by
slug. REST: `GET/POST /menus`, `GET/PUT/DELETE /menus/:id`,
`GET /menus/native-items` (real WP menu list to build the editor from,
via `MenuSerializer::serialize()`). Reordering is stored (`order` field)
but not yet applied at runtime — CRUD + hide/relabel only so far.
- **Admin Notices** (`admin/src/Notices/`): DB table `aurora_admin_notices`
(title/message/type/roles JSON/pages JSON/dismissible/active). Runtime
`admin_notices` hook renders active, role/page-matching rows via real WP
notice markup. REST: `GET/POST /notices`, `PUT/DELETE /notices/:id`.
- **Role Editor** (`admin/src/Roles/`): no custom table — wraps core
`get_editable_roles()`/`WP_Roles` directly. Hard-codes the 5 default
roles (`administrator, editor, author, contributor, subscriber`) as
delete-protected (WP has no built-in check for this). REST:
`GET/POST /roles`, `PUT/DELETE /roles/:slug`.
- **Activity Log** (`admin/src/ActivityLog/`): DB table
`aurora_admin_activity_log`. Hooked to `save_post` (guards
`wp_is_post_autosave()`/`wp_is_post_revision()`), `before_delete_post`,
`user_register`, `profile_update`, `wp_login`, `activated_plugin`,
`deactivated_plugin`, `switch_theme`,
`update_option_aurora_admin_settings`. REST:
`GET /activity-log?action_type=&object_type=&search=&page=`.
- **Plugins** (`admin/src/Rest/PluginsData.php` + `app/src/pages/plugins/Plugins.vue`):
custom plugin management list (activate/deactivate/delete), also using
`AppListShell.vue`.
- **Comments** (`admin/src/Rest/CommentsData.php` + `app/src/pages/comments/`):
newest sub-app, `edit_posts`-gated (not `manage_options` like the others).
`SubApps::redirect_native_pages()` (`admin_init`) redirects the native
`edit-comments.php` (when hit with no `?page=` param, i.e. a direct nav
or an old bookmark/menu link) to `admin.php?page=aurora-admin-comments`.
`MenuSerializer::resolve_url()` also maps the `edit-comments.php` slug
straight to the Aurora URL so the native-menu-derived sidebar entry (for
users who can `edit_posts` but not `manage_options`, so don't see the
Aurora nav section) still lands on the Aurora page. The Dashboard's
comments card (`DashboardData.php`'s `listUrl`) was updated to point at
the same Aurora URL instead of the native one.
- **Database Explorer** (`admin/src/DatabaseExplorer/`): no custom table —
reads real `$wpdb` schema. `GET /db-tables` (SHOW TABLE STATUS),
`GET /db-tables/:table` (columns + paginated rows — table name validated
against the real `$wpdb` table list before use, never interpolated
unchecked), `POST /db-query` (raw query runner, hard-restricted
server-side to queries starting `SELECT`/`SHOW`/`DESCRIBE`/`EXPLAIN`,
single-statement only, auto-`LIMIT 200` if none given). All 3 endpoints
`manage_options`-gated. Identifier allow-listing + backtick-escaping is
the actual safety boundary (`$wpdb->prepare()` only binds values, not
identifiers).
- All 5 backend classes + `SubApps.php` are `require`'d in
`aurora-admin.php` and instantiated in `AuroraAdmin.php`.
2026-07-03 02:53:29 -05:00
- **Settings persistence**: `admin/src/Options/Settings.php` — partial-payload
save via `merge_on_save()` sanitize callback (merges into existing option,
doesn't overwrite). `Settings::get_safe()` strips `license_key` for use on
non-admin-gated pages; `Settings::get()` (full) only used on the
manage_options-gated settings page itself. `Settings::remove_retired_keys()`
(hooked to `init` alongside `register()`) actively deletes settings keys
for features that no longer exist — currently just the removed Remote
Site Switcher's `remote_sites`/`remote_site_switcher_capability`, since
`remote_sites` stored a real secret (application passwords) that's no
longer read anywhere.
2026-07-03 02:53:29 -05:00
- **Global search**: `app/src/shell/components/SearchModal.vue`, Ctrl+K,
debounced `wp/v2/search` REST calls, gated on `disable_global_search` setting.
- **Scheduled content**: `app/src/pages/dashboard/cards/ScheduledContent.vue`
calendar cells (squares, date top-left, count centered) are clickable —
navigate to `edit.php?post_status=future&aurora_day=YYYY-MM-DD` if that day
has scheduled posts, else show a toast ("no scheduled content"). Server side:
`admin/src/Posts/ScheduledDayFilter.php` filters the `edit.php` query via
`pre_get_posts` and prints an info notice.
- **Analytics**: `admin/src/Analytics/Analytics.php` (first-party pageview
tracker, own DB table `wp_aurora_admin_pageviews`, bot detection, gated on
`enable_analytics`/`track_admins` settings) + `admin/src/Rest/AnalyticsData.php`
(aggregation REST endpoint: totals, series, device/referrer/country
breakdowns, bounce rate, active users).
- **White label**: `admin/src/WhiteLabel/WhiteLabel.php` — custom favicon,
rename plugin in Plugins list, rename top-level menu item, gettext
find/replace via `text_replacements` setting.
- **Native page dark styling**: `admin/assets/native-dark.css` (large static
sheet, all rules gated `html.aurora-dark.aurora-ready`, scoped under
`#wpbody-content`) + `admin/assets/native-enhance.js` (progressive JS
enhancement — currently only `enhanceUpdates()`, which groups the flat
Updates page sections into `.aurora-native-card` wrapper divs on
`body.update-core-php`). This session also extended coverage to media/
thickbox modals, plugin/theme card grids, and the plugin-search filter bar
(all previously untouched, so still native white regardless of theme).
Known gap: the plugin "More Details" popup renders inside an `<iframe>`
pointing at a separate WP page — CSS can't reach into it; would need JS
injection into the iframe's own document if that's ever worth fixing.
This session also added dark-theme rules for the block editor's own chrome
(`.editor-header`/`.edit-post-header`, `.interface-interface-skeleton__sidebar`
and its nested `.components-panel`/`.editor-post-status`/
`.interface-complementary-area-header`) — found by inspecting the real
screen via `computer-use` and drilling into nested elements' computed
styles, since `.interface-complementary-area` (nested inside the sidebar)
had its own explicit white background the outer rule didn't reach, leaving
a white patch below the Tags panel until it was targeted directly. The
actual post content canvas is deliberately left untouched (WYSIWYG, matches
the front-end).
## Window chrome (macOS-style title bar + taskbar)
Added earlier this session, user-requested after seeing a competing plugin's
demo; extended later the same session to cover the block editor and the Site
Editor, and the taskbar was rewritten to support multiple minimized windows.
Every page gets a traffic-light title bar; minimizing persists across a full
page navigation via a taskbar.
- `app/src/shell/windowChrome.js`: `initWindowChrome(adminUrl)`, called
unconditionally from `SiteShell.vue::onMounted` (no longer skipped on the
block editor — that special case was removed once the editor got its own
handling). Three cases, checked in priority order:
1. **Dashboard** (`#aurora-admin-dashboard-root` present) — wraps the bar +
the dashboard root in a new `.aurora-dashboard-frame` div, giving it the
same card-frame treatment (`--aurora-frame-bg`, border, radius) every
other page gets from `#wpbody-content` itself.
2. **Block editor / Site Editor** (`isBlockEditorPage()`: body has
`block-editor-page`, or the path ends `/post.php` or `/post-new.php` —
`site-editor.php` also gets `block-editor-page` on its body, so this
already covers it with no JS change needed) — appends a
`.aurora-winbar--editor` bar (dots only, no bar surface) directly to
`document.body`, since neither editor's real content container is
`#wpbody-content`.
3. **Everything else** — native WP pages and all of Aurora's own Vue pages,
inserted as the first child of `#wpbody-content`.
- Red (close): `window.location.href = adminUrl`.
- Green (maximize): toggles `html.aurora-maximized` — hides `.sidebar`/
`.toolbar`, zeroes `#wpcontent`'s `margin-left` and the editor frame's
inset, so content fills the whole browser window. Toggle again (or
minimize) to restore. Nothing persisted — resets on next page load.
- Yellow (minimize): **on the Dashboard itself**, toggles `html.aurora-minimized`
which just collapses `#aurora-admin-dashboard-root` down to the bar in
place (nowhere further "home" to send it). **On every other page**, calls
`taskbar.js`'s `addMinimizedWindow(title, url)` then navigates to the
dashboard.
- **Taskbar rewritten from a single sessionStorage slot to a list**
(`app/src/shell/taskbar.js`): minimizing a second page used to silently
overwrite the first one's entry, so only the most recent minimize ever
showed up. Now `getMinimizedWindows()`/`addMinimizedWindow()` (dedupes by
URL)/`removeMinimizedWindow(id)` operate on an array under
`sessionStorage['aurora-minimized-windows']`; `SiteShell.vue` `v-for`s over
it, rendering one `.aurora-taskbar__item` pill per entry inside a
transparent, floating `.aurora-taskbar` bar (inset by `--aurora-sidebar-gap`
on all exposed edges, rounded, its own box-shadow — went through a design
iteration from a small centered pill back to full-length per explicit user
correction, keeping only the pill shape on the individual items). Each item
is independently restorable/closable. `currentPageTitle()` prefers the
page's own on-screen `<h1>` (`#wpbody-content h1, #aurora-admin-dashboard-root h1`)
over `document.title`, since WP's title-tag generation came up empty for
Aurora's sub-app pages back when they were still `remove_submenu_page()`'d
(see the sub-apps permissions bug above) — `document.title` is still the
fallback for pages where that isn't an issue.
- **Content-behind-taskbar fix**: `html.aurora-has-taskbar` no longer just
adds `margin-bottom` (that only guarantees clearance once scrolled to the
very bottom — content taller than the viewport still rendered underneath
the fixed taskbar on first paint). Instead it caps `#wpbody-content`'s/
`.aurora-dashboard-frame`'s own height via `max-height` + `overflow-y: auto`,
same principle as the toolbar's reserved space at the top. This appeared to
do nothing at first — `#wpbody-content`'s base rule sets `min-height:
calc(100vh - toolbar-h - 40px)`, and per spec `min-height` always wins over
a smaller `max-height`; fixed by adding `min-height: 0` to the
taskbar-specific override.
- **Universal content-area height cap** (this session): the taskbar-only
`max-height`/`overflow-y: auto` treatment above is now applied to
`#wpbody-content` unconditionally (not just when `html.aurora-has-taskbar`
is set), capped to `calc(100vh - toolbar-h - 40px)`. Several native and
Aurora pages with a lot of content (Add Plugins, Add User, Profile,
Appearance > Fonts, the file editors, every Settings page, Activity Log,
Database Explorer, Comments) used to grow taller than the viewport and
force the *whole document* to scroll — janky since the sidebar/toolbar
are fixed, so scrolling just moved the content away from them with a lot
of dead space. Now the content card's own box is capped and scrolls
internally, same as the toolbar/taskbar reservations already did.
- **Scroll-anchoring bug this surfaced**: making `#wpbody-content` a real
scroll container exposed a second, unrelated bug on Add Plugins — its
plugin-card grid renders via JS well after first paint, inserting a lot
of content *above* whatever was already visible in the now-scrollable
box. Chrome's scroll anchoring "corrects" for that by silently jumping
`scrollTop` down to compensate, so the page loaded already scrolled
~1700px into the content instead of at the top. Fixed with
`overflow-anchor: none` on `#wpbody-content` (and the taskbar/dashboard-
frame variant) — the standard fix for this class of bug, opting the
container out of anchoring entirely.
- **Phantom document-level scrollbar**: independently, Add Plugins also
left the outer `<html>`/`<body>` scrollable with ~1200px of empty dead
space below the frame, even though every element inside `#wpbody-content`
measured as properly capped to the viewport — narrowed down to some
interaction between the plugin grid's async rendering and the browser's
own scrollable-overflow bookkeeping (root cause not fully pinned down;
forcibly shrinking or hiding `#wpbody-content` reduced but didn't
eliminate it, and no single oversized/mispositioned descendant could be
found). Since the intended design already has the sidebar/toolbar fixed
and every page's real content scrolling *internally* within its own
frame, the document itself was never supposed to scroll at all — fixed
directly by adding `overflow: hidden` to `html.aurora-ready`/`body`,
which removes the dead scroll space outright regardless of what was
causing it.
- **Block editor / Site Editor framing**: neither editor renders inside
`#wpcontent`'s normal box model — each has its own fixed, full-viewport
root (`.interface-interface-skeleton` itself on `post.php`/`post-new.php`;
`#site-editor` on `site-editor.php`, where `.interface-interface-skeleton`
is merely `position: relative`, nested inside it — a reset rule puts the
nested skeleton back to a plain unpositioned child so the two frames don't
stack). `html.aurora-ready .interface-interface-skeleton, html.aurora-ready
#site-editor` in `shell-frame.css` frames whichever is the real fixed root
with the same insets/background/radius/shadow treatment `#wpbody-content`
gets elsewhere, plus `padding-top: var(--aurora-editor-winbar-h)` to
reserve a strip for `.aurora-winbar--editor`'s dots and `overflow: hidden`
so the rounded corners actually clip the editor's own white canvas
background (border-radius alone doesn't clip children). **Known
CSS-over-constraint gotcha**: WP core's `.edit-site { height: 100vh; }`
class (present on `#site-editor`) means `top`, `height`, and `bottom` are
all specified on that box at once — per spec this is over-constrained and
`bottom` silently gets recomputed from `top + height` instead of honoring
an explicit value, which is why the frame initially ran past the intended
bottom inset despite `bottom: 24px !important` being set. Fixed by adding
`height: auto !important` to the same rule so `top`/`bottom` actually
determine the box size again. **If a future feature needs to reach either
editor's own chrome**, the real containers are
`.interface-interface-skeleton` (post editor) or `#site-editor` (Site
Editor) — don't assume the normal `#wpbody-content` page structure there.
- `Shell.php::is_enabled()` no longer excludes the Site Editor screen
(`site-editor` used to be in the skip list alongside `customize`) — it
turns out to be built on the same `@wordpress/interface` package as the
block editor (`body.block-editor-page` is present on both), so the
existing block-editor handling covers it once `#site-editor` is framed.
2026-07-03 02:53:29 -05:00
## 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.php` gained three helpers —
`category_options()`/`post_format_options()`/`page_options()` — that
build real `{value, label}` option arrays from `get_categories()`/
`get_post_format_strings()`/`get_pages()`, reusing the existing generic
`"select"` field type (`SettingsField.vue` already supported it,
populated with static arrays for `start_of_week` etc. — no new
rendering code needed, just real data instead of hardcoded options).
- New `showIf: {key, equals}` field property, checked in a
`visibleFields` computed in `NativeSettings.vue` — lets any field
declare "only show once another field's value matches", used for
Reading's page pickers (only once `show_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` — mirrors `PermalinksData.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_KEYS` consts) so the route can never
be used to write an arbitrary WP option. `NativeSettings.vue`'s
Permalinks-only `isPermalinks` special case became a small per-screen
endpoint map (`isPermalinks`/`isDiscussion`) rather than growing more
one-off branches.
- New `textarea` field type in `SettingsField.vue` (multi-line plain
text) for `moderation_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, `showIf` conditionals actually
show/hide on interaction (toggled `show_on_front` and 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-overlay` generally, so `customize.php`
is untouched/out of scope) gets the standard Aurora inset box. The
overlay's two children, `.wp-full-overlay-sidebar` and
`.wp-full-overlay-main`, are independently `position: 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-left` on `.expanded`/`.collapsed` duplicates 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**:
1. Opening the overlay adds `body.full-overlay-active`, and WP core's
own CSS sets `body.full-overlay-active { visibility: hidden }` with
only the overlay itself (`#customize-container, .theme-install-overlay`)
set back to `visible`. Aurora's entire shell root is a descendant of
`body`, 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 own
`visibility` in devtools, then grep-searching loaded stylesheets for
rules matching `body` with a `visibility` declaration — found
`body.full-overlay-active { visibility: hidden }` in WP core's own
`themes.css`. Fixed with one override:
`body.full-overlay-active #aurora-admin-shell-root { visibility: visible !important; }`
— descendants can locally re-enable an inherited `hidden` value, so
this doesn't need to touch anything else under `body` (which stays
hidden, harmless, since it's covered by the overlay's insets anyway).
2. The overlay's own `height: 100%` (WP core) plus this session's added
`top`+`bottom` insets is the *exact* over-constrained-box bug already
documented above for `.interface-interface-skeleton`/`#site-editor`
— same `height: auto !important` fix, same root cause (per spec,
`bottom` silently loses to an explicit `height` when both are set
alongside `top`).
- **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-overlay` isn't in the DOM at initial
page load (WP core's Backbone view creates it lazily on the first theme
click), so `initWindowChrome()`'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`'s `watchForThemeOverlay()` 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 to
`aurora-admin/v1/bug-report`.
- `admin/src/Rest/BugReportData.php`: thin REST controller, validates the
description isn't empty and posts it directly (via `wp_remote_post`) to
the feedback endpoint on `auroraplugins.com` — no longer goes through a
license-status layer (rewritten in 3.0.0 when `License.php` was deleted;
previously called `License::submit_bug_report()`, which reused the same
`call()` helper `validate()`/`activate()` used).
- **License server** (`aurora-licensing` plugin, not this repo): has a
`wp_aurora_bug_reports` table and a public `POST /report-bug` REST route
doing a plain `wpdb->insert`. No admin UI for reviewing reports yet —
query the table directly via `wp 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 `require`d in `aurora-admin.php`. Forgetting to add the
new `BugReportData.php` require there caused a `Class ... not found`
fatal on first load. If adding another new class file, don't forget this
step (mirrors the existing `require` list right above `new 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_reports` with all fields populated correctly,
then deleted the two test rows by exact ID (a broad `LIKE`-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
called `get_editable_roles()` unqualified, which 500'd every time —
that function lives in `wp-admin/includes/user.php`, which WP only
auto-loads for real wp-admin page requests, not REST API requests. Fixed
with a `function_exists` guard + `require_once` before the call. Unrelated
to licensing, just happened to surface while verifying Role Editor's
unlock path.
## 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 is `manage_options` everywhere, 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()`, which `realpath()`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, so `realpath()`
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.
- **Blocked extensions on upload/create/rename**
(`FileManagerData::BLOCKED_EXTENSIONS`): `.php` and 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 separate `BINARY_EXTENSIONS` list
(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_EXTENSIONS` was enforced on create/upload/rename,
but *not* on opening/saving an *existing* file — a `manage_options` user
could open a theme's `.php` file 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 via `DISALLOW_FILE_EDIT`
specifically because of this risk class. Fixed in 3.2.1: `is_editable()`
(used by both `read_file()` and `write_file()`, and by the `isEditable`
flag `list_dir()` returns per item, which the Vue UI's Edit
button/context-menu/double-click all gate on) now also excludes
`BLOCKED_EXTENSIONS`, and additionally returns `false` outright when
`DISALLOW_FILE_EDIT` is defined and true. Every mutating endpoint
(create/upload/rename/delete/write/copy/move/bulk/trash
restore-delete-empty) now also checks a new `mods_allowed()` helper
against `DISALLOW_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 the `isEditable`
server 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/bulk` endpoint 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 under
`wp-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/bulk` endpoint 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.php` and 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: namespace `AuroraAdmin\FileManager` →
`AuroraFileManager\FileManager`, REST namespace `aurora-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 standalone `base-theme.css`
extracting 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/` and
`app/src/pages/file-manager/` deleted outright, the `require`/`use`/
instantiation lines in `aurora-admin.php`/`AuroraAdmin.php`, the
`aurora-admin-file-manager` sub-app entry in `SubApps.php`/`Shell.php`,
and the `file-manager` Vite build entry all removed. Several other
classes (`FileArchiver`, `BackupPaths`, `BackupGoogleDriveData`,
`BackupData`) had comments citing `FileManagerData` as a design
precedent (containment-check pattern, lazy `WP_Filesystem` init,
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 send `theme-editor.php` visits 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 respects `DISALLOW_FILE_EDIT` itself — 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-plugin` field type in `SettingsField.vue` (Settings.vue now
passes a shared `api()` fetcher down to `SettingsField` as a prop,
the first field type that needs its own REST calls beyond the generic
`wp/v2/settings` save flow) checks install/active status on mount and
shows "Install & Activate" / "Activate" / "Installed & active — open
File Manager →" accordingly. Backend: `CompanionPluginData.php` wraps
`Plugin_Upgrader::install()` — the exact same core class
`plugin-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.
Inherits `Plugin_Upgrader`'s own `WP_Filesystem` credential 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.zip` yet — 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).
## 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
exact `init`-hooked `maybe_install()` + `DB_OPTION` version-check idiom
already used by `ActivityLog.php`/`Notices.php`/`MenuCreator.php`/
`Analytics.php`. Tables: `wp_aurora_media_folders` (id, name, parent,
ord, color, created_by) and `wp_aurora_media_folder_items` (folder_id,
attachment_id) — deliberately named `aurora_*`, not `fbv*`, 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/v1` REST routes
for folder CRUD + `/media-folders/assign`. Every `permission_callback`
checks `current_user_can('upload_files')`.
- `MediaData.php`'s existing `GET /media` route gained an additive
`folderId` param and `serialize_attachment` gained a `folderId` field,
batch-looked-up via `MediaFolders::folder_ids_for_attachments()` (one
query per page of results, not one per attachment).
- `Media.vue` gained a sidebar: pinned "All Media"/"Uncategorized"
pseudo-folders (ids `0`/`-1`, never real DB rows) above a new
`MediaFolderTree.vue` (recursive tree component modeled on
`FileManagerTree.vue`, plus inline create/rename/delete via
`window.prompt`/`confirm` — matching `FileManager.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** (the `UpgradeNotice.vue` fallback
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 direct `fetch()` calls against
the REST routes instead of clicking through the UI. Drag-and-drop was
effectively verified two ways: the REST `assign` endpoint 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 exact
`MediaFolders.php` install idiom (`DB_VERSION`/`DB_OPTION` version-gated
`dbDelta()`). `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 literal
`SHOW CREATE TABLE` output) so restore is deterministic and re-runnable
from scratch, not an ALTER/merge. Chunked restore via a byte-cursor into
the dump file (`fseek`+bounded `fread`, 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_schedules` tables were
initially included in what gets dumped like any other table. A
restore's `DROP TABLE`+`CREATE TABLE` on 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 `/step` calls started 404ing). Fixed by excluding Aurora's
own bookkeeping tables from `known_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 `\n` two-character
sequence *before* calling `$wpdb->_real_escape()` — but
`_real_escape()` (a thin wrapper on `mysqli_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
`\`+`n` instead 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 `\n` as visible text
instead of a line break. Fixed by deleting the manual pre-conversion
entirely and trusting `_real_escape()` alone.
- **`FileArchiver`** — one-shot manifest build (a flat text file, one
relative-to-`wp-content` path per line — cheap since it's metadata only,
not file content), excluding the backup storage directory during the
walk. `wp-config.php` needs no explicit exclusion — it lives at
`ABSPATH`, a sibling of `wp-content/`, so the walk (rooted at
`WP_CONTENT_DIR`) never visits it at all. Zip step reopens the same
`.zip` every request (`ZipArchive::CREATE` is safe on an existing file —
opens for append), adds files via `addFile()` (streams from disk, never
`addFromString()`) 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 against `WP_CONTENT_DIR` — same principle as
`FileManagerData::resolve_path()`) — verified live with a deliberately
crafted malicious zip entry (`../evil-outside-wp-content.txt`), confirmed
the file never got written anywhere outside `wp-content`, and the rest of
the restore still completed successfully around it.
- **Local storage hardening** — `wp-content/uploads/aurora-backups/` gets
WordPress core's own `index.php` stub and an `.htaccess` deny (both
Apache 2.2 and 2.4 directives). No protection on nginx (ignores
`.htaccess` entirely) — the real safeguard is that the download route
(`BackupData::download_job()`) is `manage_options`-gated and streams the
file through PHP in bounded chunks (never a whole-file
`WP_Filesystem::get_contents()` — a backup archive can legitimately be
multi-GB, unlike File Manager's individually-capped files).
- **Retention** — `BackupSchedules::prune()` keeps the newest
`retention_count` completed backups per schedule, deleting the rest
(job row + stored archive, via whichever `storage_provider` that
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 via `hash_hmac()` over `wp_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/bPxRfiCYEXAMPLEKEY` placeholder credentials —
cross-checked independently via a from-scratch `openssl`-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 genuine `InvalidAccessKeyId` error — 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`'s `backup_s3_credentials` key,
excluded from `Settings::get_safe()` (verified live: the raw secret
never appears in any other page's serialized `data-aurora-admin`
payload) — the exact "future field that shouldn't be broadcast" case
`get_safe()`'s own docblock already anticipated. Save/read use a
dedicated REST route (`BackupSettingsData`) rather than the generic
`wp/v2/settings` flow 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.
- **Vue UI** (`SiteBackup.vue` + `BackupHistoryList`/`BackupScheduleForm`/
`BackupSettingsPanel`) — three tabs (Backups/Schedules/Settings), a new
shared `ProgressBar.vue` and `useJobPolling.js` composable (the
`setTimeout`-recursion polling loop, shared between the backup-create
and restore flows). `window.confirm()`/`alert()` block the automated
Chrome tooling the same way `window.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 `phase`
column never got updated to a clean `"done"` value on successful
completion (`BackupJobs::set_status()` only touched `status`), so the
UI kept showing the stale last-real-phase label ("Archiving files…")
indefinitely even after the job had actually finished. Fixed by having
`set_status()` also set `phase = "done"` specifically on successful
completion (failed/cancelled jobs deliberately keep their last real
phase — useful debugging context, e.g. "failed during file_zip").
- **Not built in this pass**: the plan's "run schedule now" UI action —
the REST plumbing for it exists (`POST /backup/jobs` accepts an optional
`scheduleId` to attribute an on-demand job to a schedule's retention
pool, exercised directly during retention testing), but there's no
button for it in `BackupScheduleForm.vue` yet.
### 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
narrow `drive.file` (only files/folders the app itself creates, not full
Drive access). `access_type=offline` + `prompt=consent` guarantee a
refresh token even on re-auth; a `state` nonce in a short-lived
transient covers CSRF on the callback. Only the refresh token is ever
persisted — an access token is fetched fresh via `get_access_token()`
before every authenticated call, avoiding stale-token bookkeeping given
operations here are already inherently multi-step.
- Credentials live under `Settings`'s `backup_gdrive_credentials` key,
excluded from `get_safe()` (same treatment as `backup_s3_credentials`
— verified live it never leaks into another page's serialized
payload). `BackupGoogleDriveData`'s `config` GET/POST routes follow
the identical masked-secret read-modify-write pattern established by
`BackupSettingsData` for S3.
- `oauth-callback` is the one route in this controller that isn't
`manage_options`-gated at the permission-callback level — Google hits
it via a real browser redirect with no `X-WP-Nonce` possible, so it's
instead gated on the `state` parameter matching the stored transient
(same class of exception `FileManagerData::download_file()` already
established for direct-navigation routes).
- **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`
→ `Location` response header is the session URI), then bounded `PUT`
chunks with `Content-Range`/`Content-Length` headers (`308 Resume
Incomplete` means more to send — the byte cursor advances from the
response's `Range` header, not local arithmetic, to trust what Google
actually received). Download is a `Range`-header `GET` against
`?alt=media`, appended to the local file the same justified-raw-`fopen`
way `DbDumper::append()`/S3's `download_step()` already do. The small
`.sql` dump uses a one-shot `multipart/related` upload 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 to `accounts.google.com` that comes
back with Google's own `invalid_request: Missing required parameter:
client_id` error — 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.
## 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 on
`wp_generate_attachment_metadata` at priority 20): for every JPEG/PNG
upload, re-encodes the full size and every already-generated subsize
into the target mime type via `wp_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 new `aurora_image_formats` key
(a sibling structure to core's own `sizes` key) — deliberately not
reusing the reference plugin's `sources` key name.
- **Cleanup** (`cleanup_sibling_formats()`, hooked on `delete_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 on `wp_editor_set_quality`): pins
WebP/AVIF quality at 82 explicitly, untouched for every other mime
type.
- **`<picture>` rewriting** (`build_picture()`, called from both
`wp_content_img_tag` and `wp_get_attachment_image_html` — two separate
core filters needed since core doesn't route
`wp_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 own `sizes`
attribute 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 the `dependsOn` field pattern
(added to `settingsConfig.js`/`Settings.vue`/`SettingsField.vue`/
`Toggle.vue` in 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
existing `wp/v2/settings` + `aurora_admin_settings` merge-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/media` REST call (browser file-picker upload wasn't
available in this session — worked around by base64-encoding a
generated test image and POSTing a `FormData`/`Blob` from within the
authenticated browser tab), confirmed `wp_get_attachment_metadata()`
showed a genuine `aurora_image_formats.full` entry pointing at a
smaller `.avif` file 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.
2026-07-03 02:53:29 -05:00
## 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's `delete_plugins()`, called from `PluginsData::delete_plugin()`.
That function lives in `wp-admin/includes/file.php`, which a REST API
request doesn't load by default — `delete_plugin()` only required
`plugin.php`. `update_plugin()` right next to it already required
`file.php` for 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 requiring `file.php`
in `delete_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-shell`
only had a `min-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 real `height: calc(100vh - var(--aurora-toolbar-h) - 60px)`
and both `.app-list-shell__nav`/`.app-list-shell__content` their own
`height: 100%; overflow-y: auto`, with separate mobile-breakpoint rules
(stacked panes get natural height + their own `max-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 `click` event immediately after `contextmenu` on 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)`) in `FileManager.vue`'s `openContextMenu()`.
Confirmed by simulating the exact race (dispatching `contextmenu` then
`click` on 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
other `ScreenHijack` page too). Root cause: WordPress prints
`<div id="wpbody"><div id="wpbody-content">` *after* `in_admin_header`
fires (confirmed directly in core's `wp-admin/admin-header.php` —
`do_action('in_admin_header')` is literally before the `<div id="wpbody">`
line), but `ScreenHijack.php`'s `ob_start()` was hooked to
`in_admin_header` itself. 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 in `admin-footer.php` *before*
`do_action('admin_footer')` fires, those got captured and discarded too.
Net effect: `ob_end_clean()` silently ate the entire `#wpbody`/
`#wpbody-content` wrapper, opening and closing tags both, and the mount
div printed a level too high (a direct child of `#wpcontent`). This broke
`windowChrome.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 having `ScreenHijack.php` re-print the
`#wpbody`/`#wpbody-content` wrapper 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.php` link rewrite (the sidebar's own "Plugins" link
pointed at the Aurora page) — it never got a `redirect_native_pages()`
case for direct visits to `plugins.php` itself. 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
missing `redirect_native_pages()` case, gated by `modern_plugins` like
the others, redirecting to `plugins.php?page=aurora-admin-plugins` (not
`admin.php?page=...`, to match the URL the sidebar link itself already
uses). Verified live by visiting `plugins.php?activate=true` directly
(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,
check `redirect_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 beats
`native-dark.css`'s existing form-field rule (one `#wpbody-content` id
+ 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
`!important` to 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.
- **Admin Notices invisible on the Dashboard**: `admin_notices` prints
inside `#wpbody`, which `DashboardTakeover::hide_native()` hides
entirely (to hide the *native* dashboard, not ours) — collateral damage
for anything else relying on that hook there. Fixed with
`Notices::render_for_dashboard()`, called directly inside the visible
dashboard frame (`DashboardTakeover::render_mount()`), with `render()`
(the normal `admin_notices` hook) bailing on the Dashboard so it isn't
rendered twice. **Second, independent bug surfaced by the same fix**:
`wp-admin/js/common.js` unconditionally relocates every `.notice`
element 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 `#wpbody` on its own. Core's own `.below-h2` class
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.
2026-07-03 02:53:29 -05:00
- **`.card` global class collision**: original Dashboard cards used
`class="card"`, colliding with WP core's `.card { max-width: 520px }`.
Fixed by renaming to `.aurora-card*` everywhere. Never use unprefixed
class names on Aurora components.
2026-07-03 02:53:29 -05:00
- **Grid-column not applying on wrapped cards**: settled on unwrapped
`<section class="aurora-card" :style="{gridColumn: ...}">` — component
boundaries ate the style binding in earlier attempts.
- **Two-box settings layout**: took two wrong iterations before matching the
user's reference image. Re-confirm layout intent against a screenshot
before assuming.
- **Redundant outer frame border on settings page**: fixed with a
`body.toplevel_page_aurora-admin` override in `shell-frame.css`.
- **Dashboard submenu defaulting collapsed**: `Sidebar.vue`'s `normalize()`
URL helper didn't map the bare `/wp-admin/` root to `index.php`, so the
Dashboard section's active-state check (and thus its default-expanded
state) never matched on the dashboard itself. Fixed by normalizing empty/
query-only paths to `'index.php' + path`.
- **Aurora top-level menu appearing twice**: `SettingsPage.php`'s
`add_menu_page('aurora-admin', ...)` naturally appears in WP's native
`$menu` global. Since it's now also shown in the sidebar's dedicated Aurora
nav section, `MenuSerializer::serialize()` explicitly skips slug
`aurora-admin` to avoid the duplicate.
- **Browser automation can't install plugins**: the Chrome MCP `file_upload`
tool only accepts files already "shared with the session" (attachments/
connected folders) — local build output on disk doesn't qualify. Confirmed
this session; the user must manually reinstall the zip every time (though
see "Fast local iteration" above for the hot-patch workaround used for
verification during dev).
- **Independently-painted gradient seams** (sidebar/toolbar/settings-page
`#wpbody-content` each showing a visibly different slice of "the same"
background gradient): see the theme-system section above — root cause was
multiple elements each painting their own copy of a gradient-valued
`var(--aurora-app-bg)` instead of one element painting it and the rest
staying transparent.
- **Block editor content rendering underneath the sidebar**: Gutenberg's
`.interface-interface-skeleton` is `position: fixed; inset: 0`, completely
bypassing the `margin-left` that pushes every other page's `#wpcontent`
over — see the "Window chrome" block editor exception above for the full
mechanism and the fix.
- **`zip -r` merging into a stale archive**: rebuilding the deployable zip
without first deleting the old one left old-hash asset filenames mixed
into the new zip (macOS `zip -r` appends/merges rather than replacing).
Always `rm -f` the zip before rebuilding.
- **`remove_submenu_page()` corrupting WP's own capability check**: root
cause of "Sorry, you are not allowed to access this page" on Menu Creator/
Activity Log/Role Editor/Comments — see the sub-apps bullet above. Also
broke `document.title` on those pages, which is why `taskbar.js`'s
`currentPageTitle()` prefers the on-page `<h1>` instead. **If a future
custom admin page needs to be kept out of the native menu while staying
reachable and capability-checked, filter it out at the menu-serialization
layer (like `MenuSerializer.php` does), never with `remove_submenu_page()`.**
- **`min-height`/`max-height` conflict silently no-oping a fix**: per spec,
when `min-height` is larger than `max-height`, `min-height` always wins and
`max-height` is ignored outright — this is why the taskbar-overlap fix
first appeared to do nothing (see Window chrome section above).
- **`position: fixed` over-constraint silently ignoring an explicit `bottom`**:
when `top`, `height`, and `bottom` are all specified on a fixed-position
box, the spec has `bottom` get silently recomputed from `top + height`
rather than honored — root cause of the Site Editor frame running past the
viewport despite an explicit `bottom: 24px !important` (see Window chrome
section above).
2026-07-03 02:53:29 -05:00
## Not-yet-done (deferred, no active request)
- Full field sets for settings categories beyond General/Analytics/White
Label (original plugin had more).
- Extending `native-enhance.js`/`native-dark.css` coverage to more native
surfaces — the block editor's own chrome, Widgets screen, Customizer.
Media/thickbox modals and plugin/theme card grids were added this session;
the plugin "More Details" popup specifically can't be reached (it's an
`<iframe>` — see native page dark styling notes above).
- Menu Creator: stored item `order` field isn't applied at runtime yet
(hide/relabel work; reordering doesn't).
- No automated tests anywhere in the project — all verification has been
manual, live-in-Chrome, per the established workflow rule above.
- 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.