WP Manifestindependent plugin directory
manifest / multisite / modern-dashboard

Modern Dashboard

A network-first admin dashboard for WordPress Multisite. Aggregates content, users, updates and storage across every site in the network into one control surface.

by Allan Moody · github.com/amoody8/modern-dashboard · website

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/amoody8/modern-dashboard/archive/refs/heads/claude%2Fuipress-multisite-wordpress-rz1062.zip

A network-first admin dashboard for WordPress Multisite.

UiPress-style dashboards are built for a single site. On a network you end up with one dashboard per site and no way to see the whole install at once. This plugin starts from the opposite end: the network is the primary unit, and individual sites are rows in it.

This is an original implementation. It shares goals with UiPress but no code, markup, or assets.

Status

v0.4.0 — all four pillars. Metrics collection, the network overview, the site list with drill-down, network-controlled settings, a drag-and-drop dashboard builder whose templates are assigned by role, a network-defined admin menu editor, and branding for the admin chrome and login screen. What remains is depth rather than new pillars; see Roadmap.

Requirements

WordPress 6.6+ (the react-jsx-runtime script handle this build depends on landed in 6.6)
PHP 8.0+
Install type Multisite, network-activated

The plugin refuses to run outside a network-activated Multisite install and says why in an admin notice rather than failing silently.

What it does

Network overview. Sites, unique users, content, pending updates, uploads size and an attention list, aggregated across every site in the network.

Site list. Every site with its users, content, pending updates, uploads size, last published post and data age. Sortable on any column, filterable by site status (public / archived / spam / deleted) and by state (needs attention, has updates, inactive, stale data), with search across name and address.

Site drill-down. Per-site panel with user counts by role, content and comment breakdown, active theme and version, the specific plugins that are out of date on that site, uploads size, and when the data was last collected. Refresh any single site on demand.

Dashboard builder. Compose the overview from blocks — statistics, a ranked bar chart, the attention list, a site list, network facts, headings and notes — by dragging them into place and setting each one's width. Templates are stored at the network level and assigned per role, with a default for everyone else.

Menu editor. Reorder, rename and hide admin menu items per role, defined once at the network level and applied on every site. Submenus too.

Branding. Admin chrome colours, login screen, and white-label text (footer, greeting, WordPress logo), set network-wide with per-site overrides. Includes a live preview and WCAG contrast warnings on every foreground/background pair.

Network-controlled settings. Collection interval and batch size, storage scanning on/off with a per-site time budget, staleness and inactivity thresholds, excluded sites, and whether site administrators may see their own site's numbers.

Architecture

Collection never happens on a page load

The thing that makes a naive version of this plugin fall over is walking the whole network on every dashboard view. Instead:

  • A cron event (modern_dashboard_refresh_batch) refreshes the N stalest sites per run. Batch size and interval are settings.
  • Reads only ever touch the cache. A 10-site network and a 10,000-site network cost the same per request; the large one just takes more passes to come around.
  • A time-boxed lock stops batches from overlapping, and cannot wedge collection permanently if a batch dies mid-run.
  • New sites are collected immediately on wp_initialize_site rather than waiting for a full cycle.

Storage

Per-site metrics go in site meta (wp_blogmeta), with a network option fallback for networks that have not run the network upgrade that creates that table. A separate lightweight index maps blog ID to collection timestamp, so "which sites are stalest?" is one option read rather than a meta query across the network. The site list primes the whole meta cache in a single query.

Multisite semantics, made explicit

  • Plugin and theme files are network-wide in Multisite, so the update transients are too. What varies per site is which of them are active. The updates collector reports the intersection — updates that actually affect that site — rather than a network-wide number repeated on every row.
  • Summing per-site user counts double-counts anyone who belongs to several sites. The overview reports both: users.unique (distinct headcount) and users.memberships (seats to administer).
  • Collection never triggers an update check. That would mean one outbound request to wordpress.org per site. It reads what WordPress has already cached.

Templates are a flow, not a free grid

A block has a width in twelfths and blocks flow left to right; dragging reorders them. It would have been showier to give every block an x/y position on a canvas, but a freely positioned grid buys its looks with permanent complexity: breakpoint rules, overlap resolution, and a collapse order that has to be authored separately for every layout. Width spans give real control and stay correct on a phone, so the responsive behaviour is a property of the model rather than a pile of special cases. Narrow screens fall back to a two-column grid where wide blocks stay wide and small ones pair up.

The registry is the single authority on what a block is. The palette, the inspector's controls and the server-side validation are all generated from the same definition, so a block type added through the modern_dashboard_blocks filter gets a working editor without a line of JavaScript.

Nothing arriving from the browser is trusted: unknown block types are dropped, widths are clamped to what each block declares it can handle, unknown settings keys are discarded, and every value is coerced to the type its schema declares. The builder's canvas and the live dashboard share one renderer, so what an editor arranges is literally what a viewer gets — there is no second implementation to drift.

Charts

The one chart type is a ranked horizontal bar chart, drawn in plain HTML rather than pulled from a charting library — one form, done properly, costs less than a dependency. It is a single series, so it carries no legend (the title names what is plotted) and no gridlines (every bar is labelled with its value at the tip). A table view is one click away for anyone who wants the numbers directly.

The bar colour was validated rather than eyeballed, which caught a real problem: the plugin's WordPress-admin accent #2271b1 passes contrast on the light surface but measures 2.88:1 on the dark one, under the 3:1 floor. Dark mode therefore steps to #3987e5. Both live in --md-chart-bar.

The menu catalogue is observed, not enumerated

WordPress only knows a site's admin menu while that site is rendering an admin page: $menu is assembled by whichever plugins are active there, so it cannot be read for another site through switch_to_blog(). There is no API that answers "what is in site 47's menu?" from outside site 47.

So the catalogue is built by observation. Every admin page load contributes what that site has, merged into one network-level record keyed by slug — which means it grows with the number of distinct menu items (tens) rather than the number of sites (possibly thousands). Writes only happen when the menu's signature actually changes, so the common case is a read. Merging is additive and converges: a lost concurrent write is repaired by the next page load. Items unseen for 30 days are pruned, so uninstalled plugins fade out on their own.

The consequence worth knowing: a freshly installed network shows an empty catalogue until somebody visits a site's admin. The editor says so rather than looking broken.

Menu rules cannot lock you out

A menu editor can hide the screen you would use to undo your mistake. Three safeguards, in order of how much they matter:

  1. Network admin screens are never modified. This editor and the network dashboard are always reachable, whatever the rules say.
  2. Super administrators are exempt by default. Switchable, but on unless you turn it off.
  3. ?mdash-menu=off restores the untouched menu for anyone who can manage_options — the recovery route for a site administrator given rules that hide too much. An admin notice says when it is active.

Hiding a menu item is cosmetic. It does not revoke a capability, and the page stays reachable by URL for anyone whose role allows it. This is a tidying tool, not an access-control one — use roles and capabilities for that. The editor says this on screen too, because it is the kind of thing people assume the other way round.

Renames are escaped on the way into the menu. WordPress treats menu titles as trusted markup authored by plugin code and prints them without escaping, so a stored label would otherwise be an injection point.

Branding validates colours, and checks they can be read

Every colour ends up interpolated into a stylesheet, so sanitize_hex_color() is the gate: anything that is not hex falls back to its default rather than being passed through. Logos are restricted to http/https — no javascript:, no data:. Colours are re-validated at render time as well as on save, because that is where a string actually becomes CSS and an option can be changed by other means (WP-CLI, a migration, another plugin).

The logo URL gets its own escaper. esc_url() is an HTML escaper — it encodes ampersands, which corrupts any URL with a query string once it is inside a CSS url(). So the raw escaper restricts the protocol, and the characters that could terminate the url() or the surrounding `