# An admin panel with a shape

2026-08-10. The front end was designed. The admin grew.

Ernest's report, and it took three messages to get to the useful sentence:
*"I am not impressed with Admin… I don't know how to describe it"*, then
*"it just looks plain and thrown together instead of planned out"*, then
*"it is cosmetic too"*.

The last one matters. It is two problems, not one.

---

## What was actually wrong

Counted rather than guessed, from `layouts/admin.cvr`:

| | |
|---|---|
| **31** | links in one flat list, all the same size and weight |
| **21** | of them under a single heading called "System" |
| **3** | headings in total: Community, Moderation, System |
| **1** | hook for extensions, appending below everything, under no heading |

"System" held Blogs, Gallery, Downloads and Clubs — which are content. Themes
and Menus — which are appearance. Realtime, OpenSearch, Webhooks — which are
integrations. None of that was a mistake anybody made; it was the order
features shipped in, preserved.

That is the whole diagnosis. **The panel had never been decided.** It had
accumulated, one defensible addition at a time, and what a person reads off
that is "thrown together" without being able to say why.

🚨 And it could not have survived the ports. Forty more applications, each
appending one link to the bottom of a thirty-one item list.

## The two moves

**One: two levels.** Eight areas on a rail — Home, Community, Content,
Moderate, People, Look, System, Modules — and a panel showing only what is
inside the open one. Nothing to read until you pick an area.

Ernest, on seeing it: *"Makes it easier to navigate if you are used to how
Invision does it, except this looks nicer. It feels more functional."* That is
the intent — it is the mental model people arrive with.

**Two: one page shell.** Breadcrumb, title, primary action, tabs, in that
order, on every screen without exception. Thirty screens each solving their own
header is most of what read as unplanned: some had a heading, some had a
button, none had a breadcrumb.

## Modules, not "Add-ons"

Ernest's correction, and the truthful one. In Convoro an extension **is** a
module — same base class, same lifecycle, different directory. So the built-in
modules moved out of System and into that same area. Splitting one concept
across two areas is the mistake the old sidebar made; doing it deliberately
would have been worse.

## The navigation builds itself

`AdminNav` is a registry. A module says where it belongs:

```php
$this->adminNav()
    ->area('content')
    ->item('downloads', '/admin/downloads', 'download.nav_downloads')
    ->badge(fn () => $this->pendingFiles());
```

Decisions inside it worth keeping:

- **The eight areas are a closed list.** An extension cannot mint a ninth. The
  failure being fixed is a navigation that grew a category whenever somebody
  needed one; a setter for that would reproduce it one port at a time. A port
  that fits nowhere is a conversation about the taxonomy.
- **An unknown area throws.** Defaulting would put a screen somewhere plausible
  and silently wrong.
- **A badge is a closure**, resolved while rendering. A count taken at
  registration is the count from before the request began, and on the one
  screen where it matters — the report queue — that is the wrong number.
- **A badge that throws is dropped.** It is decoration on a navigation item; a
  broken query in one module must not blank the panel for every other screen.
- **The label is a translation key**, in the module's own namespace. A module
  owns the words for its own screen, which is what makes a port self-contained.
- **An area with nothing in it is not drawn.** With Forums, Clubs, Chat and
  Realtime all off, "Community" would otherwise lead to an empty panel.

## The Convoro update moved to the dashboard

Ernest, separately: *"it's hard to find under Modules."*

It was worse than hard. The core update was one chip in a strip alongside
reports and queued mail — "3 updates available" — linking to `/admin/modules`,
**which did not render the core update at all.** The one thing an operator must
not miss had less presence than a queued email.

It now has the width of the dashboard, the primary button, and its changelog
inline in a `<details>` — the notes are what decide whether somebody updates
today or next month, and hiding them behind a click is how "there is an update"
becomes background noise. Module updates stay in the strip and stay pointed at
Modules: they are a list of things; this is one decision.

## The cosmetic half

Structure was most of it, but "it is cosmetic too" was fair. The rules applied:

- **Use the whole type scale.** Nearly everything sat at `--t-body`. A figure
  belongs at `--t-h2`, a column label at `--t-micro`, uppercase and tracked. The
  tokens already existed; hierarchy was free.
- **State gets a shape, not only a number.** A severity stripe, a chip, a red
  pip on the rail — so a queue is visible from any screen rather than something
  you find by opening Moderation.
- **Tabular numerals** wherever digits column up.
- 🚨 **One real bug found on the way:** the selected nav item was
  `rgba(79,70,229,0.15)` — a hardcoded indigo matching nothing else on the site
  and ignoring the accent an operator picks in Appearance.

Nothing was repainted. Every colour is a token the front end already used, which
is the point: Ernest's read was *"it goes well with the front end."*

## What was added to the engine

`@hasSection('name')`. The alternative —
`@notempty($__engine->yieldSection('page_tabs'))` — works, and would have put
the engine's own variable into thirty templates where it would be copied
forever. **We own the compiler.** One line there keeps the templates saying what
they mean.

## The trap this hit

The old `admin.nav` hook is still emitted, for extensions written before any of
this existed. It was briefly emitted in *every* area, which put "Disposable
Email Blocker" in the Dashboard panel, and in Content, and in People. It is
scoped to Modules now, under a heading that says what those are: an extension
that has not declared an area has told us exactly one thing about itself.

## The bar

Ernest, on the result: *"I want this to look like we have spent decades building
with a team of developers… It looks like it took a board decision before it was
ever built."*

That is the test for the next screen, and it is a question about the work rather
than about the taste: **does this look decided?**
