Machine-sync snapshot: the working tree carried the full 3.0.0-3.6.1 arc (licensing/FileManager/DatabaseExplorer removal, all new replacement screens and REST controllers) that had not been committed on this machine. Captured here as a safety baseline before starting the Adminify-parity revamp. Co-Authored-By: Claude Opus 4.8 <[email protected]>
2085 lines
132 KiB
Markdown
2085 lines
132 KiB
Markdown
# Aurora Admin v2 — Project Status
|
||
|
||
_Last updated: 2026-07-11 (latest session — v3.6.0: Site Backup was
|
||
extracted out of Aurora Admin into a new standalone sibling plugin,
|
||
`aurora-site-backup`, at the user's explicit request ("pull out the backup
|
||
as well and lets move that to an add-on") — the same proactive-risk-
|
||
reduction reasoning as Database Explorer's 3.5.0 extraction, since
|
||
restoring a backup means writing arbitrary files back into the site. The
|
||
entire `admin/src/Backup/` tree (job/schedule storage, DB dumper, file
|
||
archiver, state-machine engine, wp_cron safety net, all three storage
|
||
providers) and all four Backup REST controllers moved unmodified into the
|
||
new plugin. Since Backup's storage providers depend on a Settings class
|
||
for stored credentials (S3 keys, Google Drive OAuth tokens), a small
|
||
standalone `Options\Settings` class was added to the new plugin with its
|
||
own `aurora_site_backup_settings` option, replacing the dependency on
|
||
Aurora Admin's own Settings class. `CompanionPluginData.php`'s `PLUGINS`
|
||
array (already generalized in 3.5.0) gained a `site-backup` entry — no new
|
||
PHP route code needed. Verified live via a full `rest_do_request()`
|
||
dispatch that created a real backup job, stepped it through the DB-dump
|
||
phase, and canceled it — not just "loads without fatals" but actual
|
||
end-to-end behavior confirmation. See "Site Backup — REMOVED..." below.
|
||
Same outstanding deployment gap as the other two companion plugins: the
|
||
zip isn't hosted yet.
|
||
|
||
Previous session — v3.5.1: a fresh WordPress.org
|
||
Plugin Check report showed the 3.3.2 compliance pass's `phpcs:ignore`
|
||
annotations weren't actually suppressing anything on several multi-line
|
||
`$wpdb->` calls — WPCS suppresses violations on the exact next physical
|
||
line, but the annotations sat above the *opening* line of the call while
|
||
PHPCS attributed the violation to a line further down inside it (where the
|
||
interpolated table-name token actually appears). Fixed across
|
||
MenuCreatorData.php, NoticesData.php, Notices.php, BackupSchedules.php,
|
||
BackupJobs.php, MediaFolders.php (the largest cluster, mostly fixed by
|
||
collapsing multi-line query calls to one physical line), DbDumper.php,
|
||
AnalyticsData.php, and MediaData.php — verified with an automated scan
|
||
that every dynamic-query `$wpdb->` call is now correctly covered. Also
|
||
found and fixed a real, unrelated bug during verification:
|
||
`MenuCreatorData::create()` built its "fetch the row I just inserted"
|
||
follow-up request with `new WP_REST_Request("GET", "", ["id" => ...])`,
|
||
but that constructor form doesn't populate what `get_param()` reads, so
|
||
creating a menu always returned "Menu not found" — confirmed via a full
|
||
`rest_do_request()` dispatch, fixed with `set_url_params()`. The
|
||
`compressed_files`/`hidden_files`/`unexpected_markdown_file` findings in
|
||
the report are scanner noise from scanning the whole dev repo, not the
|
||
actual submission zip (re-confirmed via `unzip -l`). Submission zip
|
||
rebuilt and re-verified after this pass.
|
||
|
||
Previous session — v3.5.0: Database Explorer was
|
||
extracted out of Aurora Admin into a new standalone sibling plugin,
|
||
`aurora-database-explorer`, following the exact same pattern as File
|
||
Manager's v3.4.0 extraction — direct database-query tooling (browsing
|
||
tables, running free-form SELECT/SHOW/DESCRIBE/EXPLAIN queries) is treated
|
||
by WordPress.org's Plugin Directory review the same way as file-management
|
||
functionality, so it was pulled out proactively rather than waiting for a
|
||
rejection. `CompanionPluginData.php` was generalized from a single
|
||
file-manager-only class into a `PLUGINS` config array keyed by slug, so
|
||
both companion plugins (and any future ones) share the same install/
|
||
activate/status REST routes. The Settings → Add-ons screen now lists both.
|
||
A real production deploy (hosting the `aurora-database-explorer.zip` on
|
||
aurora.auroradragon.studio) is still pending explicit user confirmation,
|
||
not yet done — same outstanding gap File Manager's zip already has.
|
||
|
||
Previous session — v3.4.0: WordPress.org's second review response said the
|
||
whole file-management/code-editing *category* isn't accepted into the
|
||
Plugin Directory, restrictions or not — so File Manager was extracted
|
||
entirely out of Aurora Admin into a new standalone sibling plugin,
|
||
`aurora-file-manager`, installable with one click from the new Settings →
|
||
Add-ons screen (wraps core's own `Plugin_Upgrader`, same class the native
|
||
"Install Now" button uses). See "File Manager — REMOVED from Aurora Admin
|
||
in 3.4.0" below, specifically its final "Extraction into a standalone
|
||
plugin" subsection.
|
||
|
||
Prior versions this doc covers in detail, newest first: v3.3.2 (WP.org
|
||
Plugin Check compliance pass, ~230 justified `phpcs:ignore` annotations
|
||
across every custom-table feature), v3.3.1 (Settings page independent-
|
||
scroll fix), v3.3.0 (Modern Image Formats — WebP/AVIF generation +
|
||
`<picture>` rewriting, reimplemented against documented WP core hooks),
|
||
v3.2.1 (closed the File Manager PHP-editing gap that triggered WP.org's
|
||
*first* rejection), v3.2.0 (Google Drive remote storage for Site Backup),
|
||
v3.1.0 (Site Backup itself — the first background-job/wp_cron
|
||
infrastructure in this codebase), and v3.0.0 (made every previously-paid
|
||
feature free, deleted the entire licensing/tamper-protection subsystem —
|
||
everything earlier in this doc describing license gating is historical
|
||
unless explicitly marked otherwise).)_
|
||
|
||
## What this project is
|
||
|
||
A ground-up rebuild of the "uiXpress" WordPress admin-theme plugin, relaunched
|
||
as **Aurora Admin**. The original plugin's look/feel and feature set are the
|
||
target, but **no original code is reused** — all Vue components, PHP classes,
|
||
and build tooling are freshly written. This lives in `aurora-admin-v2/` (the
|
||
earlier in-place rebrand attempt, `aurora-admin-working/`, is superseded and
|
||
no longer the active codebase).
|
||
|
||
## Repo
|
||
|
||
Now on GitLab (self-hosted): `ssh://[email protected]:2289/NixReaper/aurora-wordpress-admin.git`
|
||
(HTTPS also available). Repo root = the contents of `aurora-admin-v2/`
|
||
(`admin/`, `app/`, `aurora-admin.php`), not the whole `uixpress/` folder — that
|
||
folder has a bunch of dead-end attempts (`aurora-admin-working`,
|
||
`aurora-admin-distribution`, `aurora-admin-rewrite`, `aurora-admin-GLM`,
|
||
`uixpress`) that are debugging history, not part of the project.
|
||
|
||
`.gitignore` excludes `app/node_modules/` and `app/dist/` (build output —
|
||
regenerate, don't version). To clone on a new machine: add an SSH key to the
|
||
GitLab account, then `git clone` the URL above, then `cd app && npm install`.
|
||
|
||
Claude Code conversation history does **not** sync across machines — it's
|
||
local per project-folder-per-machine. Point a fresh session at this file to
|
||
get back up to speed instead of relying on prior chat history.
|
||
|
||
## Workflow rules established
|
||
|
||
- **Always verify live in Chrome** before declaring a fix done. Use the
|
||
Chrome MCP tools (`tabs_context_mcp`, `navigate`, `computer`/screenshot,
|
||
`javascript_exec`) to inspect real DOM/computed styles rather than
|
||
guessing from source.
|
||
- **Zip packaging**: on macOS, build via `npm run build` in `app/`, `rsync`
|
||
the repo (excluding `node_modules`, `.git`, `/dist`) into a scratch staging
|
||
dir, **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.
|
||
- 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.
|
||
|
||
## Architecture
|
||
|
||
- **PHP** (`admin/src/...`, namespaced `AuroraAdmin\...`), bootstrapped from
|
||
`aurora-admin.php` (manual `require` list, dependency order matters),
|
||
instantiated in `admin/src/App/AuroraAdmin.php`'s constructor.
|
||
- **Vue 3** app (`app/src/...`), Vite multi-entry build (`app/vite.config.js`
|
||
`rollupOptions.input`) → `manifest.json`, read by
|
||
`admin/src/Utility/Assets.php::enqueue()` which also walks the manifest's
|
||
`imports` chain to pull in shared-chunk CSS. Data reaches each Vue entry via
|
||
a `data-aurora-admin` JSON attribute on a mount div (never `wp_localize_script`).
|
||
- **Single global stylesheet**: `app/src/shell/shell-frame.css` — all
|
||
`--aurora-*` CSS variables (incl. `.aurora-dark` overrides) and the only
|
||
rules touching WP structural IDs (`#wpcontent`, `#wpbody-content`). No
|
||
element resets/utility classes leak onto native WP pages.
|
||
- **Immersive shell** (`admin/src/Shell/Shell.php` + `app/src/shell/SiteShell.vue`
|
||
+ `Sidebar.vue` + `Toolbar.vue` + `UserMenu.vue`): hides native WP chrome,
|
||
mounts a Vue sidebar/toolbar on every admin page.
|
||
- `MenuSerializer.php` converts WP's `$menu`/`$submenu` globals into JSON
|
||
for the sidebar's native-menu section — explicitly skips the
|
||
`aurora-admin` slug so Aurora's own pages aren't duplicated (they're
|
||
represented in the sidebar's own branded nav section instead).
|
||
- Sidebar has two sections: the WP-native menu list (icon-chip style,
|
||
28×28px rounded icon backgrounds, tight ~2px vertical padding per item)
|
||
and a bottom "Aurora" section (collapsible, expanded by default,
|
||
persisted via `localStorage['aurora-nav-expanded']`) styled like a
|
||
**native WP submenu** (plain indented text via `.sidebar__sublist`/
|
||
`.sidebar__sublink`, no icons) — deliberately matches how e.g. Posts'
|
||
own submenu (All Posts/Add Post/Categories/Tags) looks, per explicit user
|
||
request.
|
||
- Sidebar is `display:flex; flex-direction:column`, split into a
|
||
`.sidebar__scroll` (flex:1, scrollable — holds brand/native-list/Aurora
|
||
section) and a pinned `UserMenu` footer below it (not part of the
|
||
scrollable area, so the account card is always visible).
|
||
- `UserMenu.vue`: floating card that expands **upward** from a bottom-left
|
||
trigger (avatar+name+chevron). Card contains: user header (avatar/name/
|
||
email), links (Edit Profile → `profile.php`, Visit site, Site Settings →
|
||
`options-general.php`, Sign Out in red). No theme control here anymore —
|
||
see "Theme system" below; the old light/dark/system 3-way swatch picker
|
||
and the `dark_mode` setting it drove are both gone entirely.
|
||
- Old top-right toolbar user-dropdown and sun/moon toggle button are
|
||
**removed** — fully replaced by the bottom-left `UserMenu`.
|
||
- Sidebar is **inset**, not flush against the browser edge: fixed at
|
||
`top/left/bottom: var(--aurora-sidebar-gap)` (16px), fully rounded
|
||
corners, `background: var(--aurora-card-bg)` — floats as its own card
|
||
over the page gradient. `--aurora-sidebar-gap` is a shared CSS var
|
||
(`shell-frame.css`) that the toolbar's `left` offset and `#wpcontent`'s
|
||
`margin-left` both add too, so nothing overlaps when the gap changes.
|
||
- **Native-menu accordion**: only one WP-native-menu section's submenu is
|
||
open at a time (`Sidebar.vue`'s `openId` ref) — expanding one collapses
|
||
whichever else was open, instead of the list growing indefinitely as
|
||
more sections get expanded. Was previously a `Set` of independently
|
||
toggleable ids (each section remembered its own open/closed state);
|
||
replaced with a single id so opening any section closes the rest.
|
||
Starts on whichever section matches the current page (each nav is a
|
||
full page load in this multi-page admin, so this is computed once at
|
||
mount, not reactively). The separate "Aurora" branded nav section below
|
||
it keeps its own independent expand/collapse state
|
||
(`localStorage['aurora-nav-expanded']`) — not part of this accordion.
|
||
|
||
## 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.
|
||
- **Dashboard takeover** (`admin/src/Pages/DashboardTakeover.php` +
|
||
`app/src/pages/dashboard/Dashboard.vue`): hijacks native `index.php` via
|
||
`in_admin_header`, hides the native `.wrap`, renders the Aurora dashboard
|
||
in its place. 12-col grid; cards are single `<section class="aurora-card">`
|
||
elements with inline `grid-column: span N` (NOT wrapped — component
|
||
attribute fallthrough for styles was unreliable). Never use unprefixed
|
||
class names on Aurora components (`.card` collided with WP core once).
|
||
- **Settings page** (`admin/src/Pages/SettingsPage.php` + `app/src/pages/
|
||
settings/Settings.vue`): owns the top-level "Aurora Admin" WP admin menu
|
||
directly via `add_menu_page` (slug `aurora-admin`). Two-box layout:
|
||
`.settings__nav` (category list) + `.settings__content` (fields), both
|
||
boxed/bordered on a darker backdrop. Field schema is declarative in
|
||
`app/src/pages/settings/settingsConfig.js`, rendered by
|
||
`app/src/pages/settings/components/SettingsField.vue` (types: text, toggle,
|
||
color, image, repeater, multiselect, segmented). `remote-sites` 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`.
|
||
- **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.
|
||
- **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.
|
||
|
||
## 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.
|
||
|
||
## 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.
|
||
- **`.card` global class collision**: original Dashboard cards used
|
||
`class="card"`, colliding with WP core's `.card { max-width: 520px }`.
|
||
Fixed by renaming to `.aurora-card*` everywhere. Never use unprefixed
|
||
class names on Aurora components.
|
||
- **Grid-column not applying on wrapped cards**: settled on unwrapped
|
||
`<section class="aurora-card" :style="{gridColumn: ...}">` — component
|
||
boundaries ate the style binding in earlier attempts.
|
||
- **Two-box settings layout**: took two wrong iterations before matching the
|
||
user's reference image. Re-confirm layout intent against a screenshot
|
||
before assuming.
|
||
- **Redundant outer frame border on settings page**: fixed with a
|
||
`body.toplevel_page_aurora-admin` override in `shell-frame.css`.
|
||
- **Dashboard submenu defaulting collapsed**: `Sidebar.vue`'s `normalize()`
|
||
URL helper didn't map the bare `/wp-admin/` root to `index.php`, so the
|
||
Dashboard section's active-state check (and thus its default-expanded
|
||
state) never matched on the dashboard itself. Fixed by normalizing empty/
|
||
query-only paths to `'index.php' + path`.
|
||
- **Aurora top-level menu appearing twice**: `SettingsPage.php`'s
|
||
`add_menu_page('aurora-admin', ...)` naturally appears in WP's native
|
||
`$menu` global. Since it's now also shown in the sidebar's dedicated Aurora
|
||
nav section, `MenuSerializer::serialize()` explicitly skips slug
|
||
`aurora-admin` to avoid the duplicate.
|
||
- **Browser automation can't install plugins**: the Chrome MCP `file_upload`
|
||
tool only accepts files already "shared with the session" (attachments/
|
||
connected folders) — local build output on disk doesn't qualify. Confirmed
|
||
this session; the user must manually reinstall the zip every time (though
|
||
see "Fast local iteration" above for the hot-patch workaround used for
|
||
verification during dev).
|
||
- **Independently-painted gradient seams** (sidebar/toolbar/settings-page
|
||
`#wpbody-content` each showing a visibly different slice of "the same"
|
||
background gradient): see the theme-system section above — root cause was
|
||
multiple elements each painting their own copy of a gradient-valued
|
||
`var(--aurora-app-bg)` instead of one element painting it and the rest
|
||
staying transparent.
|
||
- **Block editor content rendering underneath the sidebar**: Gutenberg's
|
||
`.interface-interface-skeleton` is `position: fixed; inset: 0`, completely
|
||
bypassing the `margin-left` that pushes every other page's `#wpcontent`
|
||
over — see the "Window chrome" block editor exception above for the full
|
||
mechanism and the fix.
|
||
- **`zip -r` merging into a stale archive**: rebuilding the deployable zip
|
||
without first deleting the old one left old-hash asset filenames mixed
|
||
into the new zip (macOS `zip -r` appends/merges rather than replacing).
|
||
Always `rm -f` the zip before rebuilding.
|
||
- **`remove_submenu_page()` corrupting WP's own capability check**: root
|
||
cause of "Sorry, you are not allowed to access this page" on Menu Creator/
|
||
Activity Log/Role Editor/Comments — see the sub-apps bullet above. Also
|
||
broke `document.title` on those pages, which is why `taskbar.js`'s
|
||
`currentPageTitle()` prefers the on-page `<h1>` instead. **If a future
|
||
custom admin page needs to be kept out of the native menu while staying
|
||
reachable and capability-checked, filter it out at the menu-serialization
|
||
layer (like `MenuSerializer.php` does), never with `remove_submenu_page()`.**
|
||
- **`min-height`/`max-height` conflict silently no-oping a fix**: per spec,
|
||
when `min-height` is larger than `max-height`, `min-height` always wins and
|
||
`max-height` is ignored outright — this is why the taskbar-overlap fix
|
||
first appeared to do nothing (see Window chrome section above).
|
||
- **`position: fixed` over-constraint silently ignoring an explicit `bottom`**:
|
||
when `top`, `height`, and `bottom` are all specified on a fixed-position
|
||
box, the spec has `bottom` get silently recomputed from `top + height`
|
||
rather than honored — root cause of the Site Editor frame running past the
|
||
viewport despite an explicit `bottom: 24px !important` (see Window chrome
|
||
section above).
|
||
|
||
## Not-yet-done (deferred, no active request)
|
||
|
||
- Full field sets for settings categories beyond General/Analytics/White
|
||
Label (original plugin had more).
|
||
- Extending `native-enhance.js`/`native-dark.css` coverage to more native
|
||
surfaces — the block editor's own chrome, Widgets screen, Customizer.
|
||
Media/thickbox modals and plugin/theme card grids were added this session;
|
||
the plugin "More Details" popup specifically can't be reached (it's an
|
||
`<iframe>` — see native page dark styling notes above).
|
||
- 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.
|