# Aurora Admin v2 — Project Status _Last updated: 2026-07-05 (latest session — real Posts replacement page, first of the "skinned vs. replaced" pages to actually get replaced)_ ## 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) Not every native WP admin page gets the same treatment, and the user has explicitly said they don't want the "skinned" approach used more broadly without a real replacement behind it — check this list before describing a page as "already handled": - **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, and now **Posts** (added this session — see below). - **Just skinned** — the real native WordPress page still renders; only `admin/assets/native-dark.css` restyles it dark. Same underlying markup, same native limitations, no new functionality: Pages, Media, Users, and every other native screen not listed above. **Posts** (`admin/src/Rest/PostsData.php` + `app/src/pages/posts/Posts.vue`, entry `posts.js`) was the first of these converted from skinned to replaced, built directly off the Comments pattern: - REST: GET `/posts` (status/search/pagination, matching native filters: all/publish/draft/pending/trash), POST `/posts/trash`, POST `/posts/restore`, DELETE `/posts/{id}` (permanent). Capability checks mirror native (`edit_posts` to list, `edit_post`/`delete_post` per-item). - `SubApps.php`: registered as `aurora-admin-posts`, parented under `edit.php` (same pattern as Plugins under `plugins.php`) so it shares the native "Posts" top-level menu's `hook_suffix` slot. - `SubApps::redirect_native_pages()`: visiting `edit.php` directly (typed URL, bookmark — not just the sidebar link) redirects to the Aurora page, gated on **only** the default post type (`post_type=post` or absent) — `edit.php?post_type=page` (Pages) and any other custom post type's list screen is explicitly left alone, since only Posts has a replacement. - `MenuSerializer.php`: the sidebar's own "Posts"/"All Posts" links (both share the literal `edit.php` slug) get rewritten the same way Plugins'/Comments' links already were. - Gated by Settings > Posts' "Modern posts table" toggle — this is the same toggle that got **removed** two commits ago for describing a feature that didn't exist; it's back now that it does, defaulting to on. `modern_plugins`/`modern_comments` also got a `default: true` UI fix at the same time (they already defaulted true server-side, but the Settings UI toggle showed off until explicitly saved — cosmetic mismatch, now consistent). - **Not done**: Pages, Media, Users remain skinned-only. Each would be a comparable-sized build to Posts/Comments (own REST controller, own list/detail UI) — ask the user before starting one, per the pattern established this session (they picked "Posts first, then expand" over doing all four at once). ## 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 `