Files
aurora-wordpress-admin/PROJECT_STATUS.md
T
nixpanelandClaude Sonnet 5 204c4365e2 Add real replacement pages for Pages, Media Library, and Users (v2.6.0)
Completes the "skinned vs. replaced" conversion started with Posts —
every native list-table screen the user asked about now has a genuine
custom Vue page behind it instead of dark-themed CSS over the native
markup. All three follow the same pattern as Posts: own REST controller
in admin/src/Rest/, own Vue page matching the Comments layout, native
screen redirected away (direct visits too, not just the sidebar link),
gated by its own Settings toggle defaulting to on.

- Pages (PagesData.php + Pages.vue): same as Posts, parent page shown
  instead of categories. edit.php?post_type=page redirects; other post
  types untouched.
- Media Library (MediaData.php + Media.vue): thumbnail grid with type
  filters (Images/Audio/Video/Documents/Unattached). Replaces the
  browse/library experience only — media-new.php (the actual upload
  screen) is untouched, same reasoning as leaving the block editor
  alone for Posts/Pages.
- Users (UsersData.php + Users.vue): filterable by role. Deleting a
  user always reassigns their content to whoever performs the deletion
  rather than exposing WP core's reassign-or-delete-everything choice
  as a one-click action — deliberately simplified to avoid a surprising,
  highly destructive default. user-new.php untouched.

Verified live: Media's grid renders real thumbnails/dimensions/file
sizes with working filters and search; Users shows real accounts with
role filters and correctly hides delete for the current user; both
redirect correctly from their native URLs.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-05 06:40:22 -05:00

870 lines
56 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Aurora Admin v2 — Project Status
_Last updated: 2026-07-05 (latest session — Pages/Media/Users replacement pages, completing the "skinned vs. replaced" conversion started with Posts)_
## 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.
All four of this session's pages (`PostsData.php`+`Posts.vue`,
`PagesData.php`+`Pages.vue`, `MediaData.php`+`Media.vue`,
`UsersData.php`+`Users.vue`) follow one shared pattern, established with
Posts and repeated for the other three:
- 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.
- `SubApps.php`: new `PAGES` entry, `parent_slug()`/`capability()` cases,
and a branch in `redirect_native_pages()` — the native screen's file
(`edit.php`, `edit.php?post_type=page`, `upload.php`, `users.php`)
redirects to the Aurora page on **direct visits** (typed URL/bookmark),
not just the sidebar link.
- `MenuSerializer.php`: a matching rewrite so the sidebar's own link
(which shares the same slug as the parent, per WP convention) points at
the Aurora page too.
- A Settings toggle (`modern_posts`/`modern_pages`/`modern_media`/
`modern_users`, all `default: true`) gating the redirect+link-rewrite,
so the native screen can still be reached if turned off. `modern_pages`/
`modern_media`/`modern_users` are new categories/fields this session;
`modern_posts` already existed (see below). All the `modern_*` toggles
now consistently set `default: true` in the Settings UI to match their
actual PHP default (previously some defaulted true server-side but
showed off in the UI until explicitly saved — cosmetic mismatch, fixed
across the board).
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.
## 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).
- **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` and remote
site passwords for use on non-admin-gated pages; `Settings::get()` (full)
only used on the manage_options-gated settings page itself.
- **Global search**: `app/src/shell/components/SearchModal.vue`, Ctrl+K,
debounced `wp/v2/search` REST calls, gated on `disable_global_search` setting.
- **Remote site switcher**: toolbar dropdown, gated on `canSwitchSites` (from
`remote_site_switcher_capability` setting) + non-empty `remote_sites`.
- **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.
## Licensing (free vs. paid feature gating)
Added this session. The split, as decided with the user: the whole shell/
theming product is free (it's the adoption hook); Role Editor, Activity Log,
Database Explorer, Analytics, White Label, and the remote site switcher are
paid (the "operational" tools an agency/serious site owner pays for). Menu
Creator, Admin Notices, Comments, and the Plugins page stay free.
- `admin/src/Licensing/License.php` is the single gate every paid feature
checks — `PAID_FEATURES` maps a feature id to a human label,
`is_active()` and `is_feature_available($feature)` are the two calls
everything else uses.
- **Real license-server integration** (this session, supersedes the earlier
"any non-empty key counts as active" placeholder): validates against the
actual license server — the `aurora-licensing` WordPress plugin running
on `auroraplugins.com` (a cPanel box at `auroradragon.studio`, SSH-
accessible as root via a dedicated key,
`~/.ssh/id_ed25519_auroradragon_root` — the git-remote key is scoped
git-only and can't get a shell). REST namespace
`/wp-json/aurora-licensing/v1/`:
- `POST /validate` — `{license_key, domain, product_id?}` →
`{valid, status, plan, max_sites, active_sites, active_on_domain,
expires_at, product_id}` on success, `{valid:false, error, message}`
on failure (e.g. `invalid_key`).
- `POST /activate` — same request shape → `{activated, plan, max_sites,
expires_at}`, or `{activated:true, message:"Already activated..."}` if
already active on this domain, or a 403 `{valid:false,
error:"limit_reached", message}` if `max_sites` is exceeded.
- `POST /deactivate` — `{license_key, domain}` → `{deactivated, message}`.
- `domain` is normalized server-side (strips protocol/`www.`/trailing
slash) — `License::domain()` just sends the bare host from `home_url()`.
- `GET /my-licenses` (cookie-auth, lists all of a customer's licenses) is
the account-portal-facing endpoint, not used by the plugin itself.
- `License::status()` caches the `/validate` response in the
`aurora_admin_license_status` option for 12 hours
(`STATUS_TTL`) — `is_active()`/`is_feature_available()` read this
cache rather than hitting the network on every admin page load.
- `License::on_settings_updated()` hooks `update_option_aurora_admin_settings`
(same hook `ActivityLog.php` uses) and re-activates/deactivates
immediately whenever `license_key` actually changes, rather than
waiting out the cache TTL — deactivates the old key's slot on this
domain before activating the new one, so switching keys doesn't leak
an activation. Instantiated in `AuroraAdmin.php` (`new License();`)
so this hook actually registers — it wasn't needed when the class was
static-only.
- Network failures/non-JSON responses collapse into a synthetic
`{valid:false, status:"unreachable", ...}` shape (`License::call()`)
so callers only ever branch on response fields, never on `WP_Error`.
- **Settings UI feedback**: `SettingsPage.php` passes `License::status()`
to the Vue settings page as `licenseStatus`; `Settings.vue` forwards it
only to the `license_key` field; `SettingsField.vue` shows a colored
status line under the field (green "License active (plan) — expires
X", amber "unreachable, will retry", red the server's own error
message) — verified live against the real server with a fake test key
and got back the exact `{"valid":false,"error":"invalid_key","message":
"License key not found."}` shape.
- **Not yet done**: no real license key was available to test the
activate/site-limit-reached path end-to-end (only the invalid-key path
was verified against the live server) — worth a follow-up pass once a
real test key exists. No UI yet for the `limit_reached` case beyond
surfacing its message string.
- **Sub-apps** (`SubApps.php`): `PAGES` now has a `"feature"` key per page
(`null` for the 4 free ones). Locked pages still fully register
(`add_submenu_page`, real capability, real URL — same as before) so nav/
bookmarks/capability checks all keep working; `render()` and
`maybe_enqueue()` just check `License::is_feature_available()` and swap in
a shared upgrade prompt (below) instead of the real feature's mount div/JS
bundle/REST data when unlicensed. This also means a locked page's
sensitive data (e.g. Database Explorer's schema access) never gets
fetched or serialized to the page in the first place.
- **Shared upgrade prompt**: `app/src/pages/_shared/UpgradeNotice.vue` (lock
icon, feature name, description, a CTA linking to
`admin.php?page=aurora-admin#license`) + a dedicated
`app/src/entries/locked-feature.js` Vite entry that mounts it onto
`[data-aurora-locked-feature]`. `SettingsField.vue` gives the license key
field `id="license"` so that CTA link scrolls straight to it (General is
the default-active settings category, so the anchor resolves on a fresh
page load too). Reused directly by the Dashboard's Analytics tab (below)
rather than building a second locked-state component.
- **Analytics** (paid): `Analytics.php`'s constructor returns immediately
(no hooks registered, no pageview tracking) when unlicensed.
`AnalyticsData.php`'s REST `permission_callback` also checks
`License::is_feature_available("analytics")`, so the endpoint 403s even
if someone hits it directly. The Dashboard's Analytics tab
(`Dashboard.vue`) renders `UpgradeNotice` in place of the real charts when
`useAnalyticsData`'s fetch comes back non-OK (the 403, in practice).
- **White Label** (paid): `WhiteLabel.php`'s constructor returns immediately
when unlicensed — favicon/plugin-rename/menu-rename/text-replacement
filters simply never register. Its settings fields stay visible either
way (so an admin can pre-configure before upgrading) — enforcement is
entirely server-side, nothing in the UI needs to disable inputs.
- **Remote site switcher** (paid): `Shell.php`'s `canSwitchSites` flag now
additionally requires `License::is_feature_available("remote-site-switcher")`,
alongside its existing `remote_site_switcher_capability` check.
- **Sidebar lock badges**: `Shell::aurora_nav()` adds a `"locked"` bool per
nav item (true for Role Editor/Activity Log/Database Explorer when
unlicensed); `Sidebar.vue` renders a small lock dashicon next to the label
when set. The link still navigates normally — `SubApps::render()` is what
actually shows the upgrade prompt there.
- **Settings page**: White Label/Analytics category subtitles and the
Remote Site Switcher Capability field's description now say "Pro feature
— requires a license" (`settingsConfig.js`) — a plain-text note only, no
functional gating in the settings UI itself (the features' own code is
the actual boundary).
- **Bug fix surfaced while testing this**: `RolesData.php`'s REST callback
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 (file-integrity check on the license gate)
Added this session — the user's concern was specifically someone stripping
out the license check from a distributed copy and giving away a "cracked"
version that shows every paid feature unlocked with no license at all.
**Honest limit, stated up front (and to the user) before building this**:
nothing living in plain, editable PHP can make server-side code truly
tamper-proof — whoever has file access to their own server can always edit
whatever they want, including this integrity check itself. What this
*does* stop is the realistic, low-effort case: someone finds the one
`if (!License::is_feature_available(...))` line and comments it out or
inverts it. It does not stop someone who rewrites a whole file's logic to
never call the gate at all, or who edits `Integrity.php`'s own comparison
logic — that's an unavoidable ceiling for this approach, only closable
with a compiled/encoded extension (its own real costs: hosting
compatibility, trust), which wasn't in scope here.
- `admin/src/Licensing/Integrity.php`: `Integrity::is_tampered()` compares
the sha256 of each "gate" file (the ones that implement or directly
enforce the free/paid split) against
`admin/src/Licensing/integrity-manifest.json`. **No manifest present
(e.g. a git checkout) → never treated as tampered** — this only ever
applies to a distributed zip, never blocks local dev. Result is cached
in a static property per request (file hashing isn't free).
- Protected files (kept in sync between `Integrity.php`'s doc comment and
`bin/build-integrity-manifest.php`'s `$protected` array): `aurora-admin.php`,
`AuroraAdmin.php`, `License.php`, `Integrity.php` itself, `SubApps.php`,
`Analytics.php`, `AnalyticsData.php`, `WhiteLabel.php`, `Shell.php`.
- `License::is_feature_available()` checks `Integrity::is_tampered()`
*before* checking the actual license — a tampered install re-locks every
paid feature regardless of an otherwise-valid, active license. This is
the "re-lock paid features only" behavior the user explicitly chose over
a harsher "block the whole plugin" option, specifically to avoid a false
positive (a host mangling line endings, some other edge case) breaking
the entire admin shell rather than just the paid tier.
- `bin/build-integrity-manifest.php <staged-plugin-root>` generates the
manifest — run against the **staged zip-build copy**, never against the
git checkout (the manifest itself is a build artifact, gitignored, same
"regenerate, don't version" pattern as `app/dist`). This is now a
required step in the zip-build workflow (see "Zip packaging" above) —
forgetting it means the shipped zip has no manifest and therefore no
tamper protection at all.
- **Verified live**, without a real license key, by faking an "active"
license through a temporary mu-plugin that directly set the
`aurora_admin_license_status`/`aurora_admin_settings` options (kept
entirely outside any protected file, so it didn't itself trip the
integrity check) — confirmed: (1) untampered + unlicensed → locked
(baseline unaffected), (2) untampered + simulated-active-license →
unlocked, (3) one protected file edited (a trailing comment appended to
`SubApps.php`) while still "licensed" → re-locked immediately. All test
artifacts (the mu-plugin, the tampered file, the test manifest) were
removed from the LocalWP install afterward.
## Key bugs fixed (root causes, in case they resurface)
- **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).
- Porting the real license-key activation flow (built previously in the old
`aurora-admin-working` project, not yet ported to v2 — currently just a
text field).
- 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.
## 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).
**Not yet done**: the activate/site-limit-reached path hasn't been tested
with a real, valid license key (only the invalid-key path was verified
against the live server) — worth a follow-up pass once a real test key is
available. No purchase/checkout flow lives in this repo (that's entirely
on the `auroraplugins.com`/WooCommerce side, which auto-generates keys on
order completion) — this repo only ever consumes the license API.