794 lines
51 KiB
Markdown
794 lines
51 KiB
Markdown
# Aurora Admin v2 — Project Status
|
||||
|
|
|
|||
_Last updated: 2026-07-04 (latest session — remaining placeholder settings wired up, dashboard scrolling and admin notices fixed)_
|
||||
|
||||
|
|
## 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.
|
|||
|
||||
|
|
## 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.
|