# Aurora Admin v2 — Project Status _Last updated: 2026-07-05 (latest session — fixed white native form fields on classic edit screens, e.g. Edit Media's title field)_ ## 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://git@git.auroradragon.studio: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 /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 /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 root@auroradragon.studio`). 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//` + 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 `` 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 `
` 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 `