Download the PHP package milpa/admin without Composer
On this page you can find all versions of the php package milpa/admin. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Vendor milpa
Package admin
Short Description The administration panel of the Milpa PHP framework: an event-driven dashboard composed of Milpa Components, where every installed plugin declares its own section, routes and services — and the panel discovers them without knowing any plugin by name.
License Apache-2.0
Package admin
Short Description The administration panel of the Milpa PHP framework: an event-driven dashboard composed of Milpa Components, where every installed plugin declares its own section, routes and services — and the panel discovers them without knowing any plugin by name.
License Apache-2.0
Please rate this library. Is it a good library?
Informations about the package admin
# Milpa Admin
> The **administration panel** of the Milpa PHP framework — the surface where a human leaves the house ready for the agent. Composed of Milpa Components, event-driven, and extended by declaration: every installed plugin adds its own section without the panel knowing its name.
[](https://github.com/getmilpa/admin/actions/workflows/ci.yml)
[](https://packagist.org/packages/milpa/admin)
[](LICENSE)
## What it is
Milpa is a framework a human administers and an **agent operates**. Everything the agent can do is a governed
operation; everything the human sees is a projection of the same operations. The admin panel is the human's
projection: install capabilities, read the routes and middleware every plugin declared, stand up the services the
app needs, adjust configuration, read the ledgers of what the agent did.
Add one plugin to `config/plugins.php` and `/milpa/admin` exists:
The panel opens on **The house**, followed by **Plugins** (what the app boots, and the capabilities it can grow),
**Routes** (every route the booted plugins declared, with handler and per-route middleware),
**Identity** (the current request's authenticated identity and declared scopes), **Settings** (what the
app declared about the panel itself, key by key, with its source), **Stack** (every backing service the booted
plugins declared they need — image, ports, environment with secrets masked, the declaring plugin — and whether its
port answers on loopback) and **Dev tools** (the ledgers the agent writes, read-only) — and one more per plugin
that declares one.
## Identity
`/milpa/admin/s/identity` shows the actor authenticated for the current request and its declared
scopes. Missing scopes are reported as unavailable; an empty list is shown as empty. Each operation
still makes its own authorization decision. The section reads no private keys or enrollment files
and offers no permission-changing actions.
Passkey registration and sign-in links appear only when the house serves those routes, with a
return to Identity. The ceremonies remain outside the panel's gate so a visitor can sign in.
The terminal reports no authenticated HTTP identity. English is the default; `?lang=es` selects Spanish.
## The same panel, in a terminal
A section is declared once. `/milpa/admin` paints it as a page, and `coa panel` paints the same declaration in
the terminal — the sections along the bottom, one state table per section, `tab` and the digits to move:
Nothing is declared twice and no plugin says a word about a terminal. The panel implements
`Milpa\Console\State\SectionStateSource` **once, for everyone**: it already discovers every section every
request, so it hands that same catalogue — its own sections and every guest's — to the dashboard
`milpa/console` carries. A component's state IS the picture; `milpa/live-tui` turns any state array into a
table, so no section writes a second renderer to gain a second surface.
A section that cannot be mounted reports that in its own state and the rest of the panel stands, exactly as a
component that throws paints its failure inside its own region on the page.
## The one idea: a section is a component a plugin declares
The panel discovers implementers by `instanceof` over the booted plugins, **at request time** (boot order does not
matter), lists each section in the sidebar under its group, routes it at `/milpa/admin/s/`, and renders it
inside the shell under a header that says who declared it. A duplicate id is a loud 500 naming both plugins — never
a silent "last one wins". One prop name is **reserved**: `query` — the shell hands every active section the request's
query params under `props['query']`, so a section can read its own (`?session=`, a filter) without the shell
interpreting it; an `AdminSection` that declares `props['query']` itself is refused at construction
(`InvalidArgumentException`) rather than silently overwritten on every request. The component **names** the panel
registers itself are reserved the same way — the dashboard primitives, `admin-sidebar`, `admin-section-header`: a
section may *name* one (`component: 'metric-card'`), but a section that brings its own `definition` under one is
refused (`ReservedComponentException`, a 500 that names the section and the name it tried to take) rather than
silently repainting every section that names it, the host's header included.
Two lifecycle pairs let another plugin extend a section or the shell without touching either:
`admin.section.before_render` / `after_render` (props, then HTML — both mutable) and
`admin.shell.before_render` / `after_render` (the composition and sidebar items, then HTML). A sidebar item a
subscriber adds names its `group` like a section does (`app` when it names none).
The panel **declares** those four to the dispatcher it boots with (greenhouse `decisions/0228`): when the app's
dispatcher implements `Milpa\Interfaces\Event\DeclaredEvents` (`milpa/core` ≥ 0.11), `AdminPlugin::boot()` hands it
`AdminEvents::declarations()` — one `EventDeclaration` per name, built from the same constants `AdminShell`
dispatches with (`AdminShell::events()`): who dispatches it, when, the payload key (`section` / `shell`), the subject
type (`SectionRender` / `ShellRender`) and that the subject is mutable. A dispatcher that does not implement the
contract is told nothing and every event fires the same. The falsifier,
`tests/TheEmitterDeclaresEveryEventItDispatchesTest.php`, renders through the real kernel with a spy dispatcher and
compares what was declared against what was dispatched — names, keys, subjects, the dispatching class — and goes red
when one declaration is deleted (measured before it was committed).
## Hosting a guest: what a section receives
A section is a guest of the panel, and the panel is the host — it tells the guest what it knows and paints what
the guest cannot know about itself (greenhouse `decisions/0210`, sharpened by the first real guest, the Desktop's
**Agent** section).
**The context.** Every section's component mounts with a `ComponentContext` the shell fills — the same one every
component of the page gets, under that component's own id:
| field | value |
|---|---|
| `componentId` | `milpa-admin-section-` |
| `principal` | the actor the gate authenticated — the `id` of the `AuthContext` a gate left under the request attribute `milpa.auth` (`passkey:` behind app-runtime's passkey gate) — or **`null` when nobody is signed in**. It is exactly what the topbar's `signed in as …` chip shows, so a guest that decides its state by the principal (the Desktop's *signed-out* vs *live*) always agrees with the topbar |
| `locale` | the language the page answers in — the app's `admin.locale`, or the request's `?lang=` when the catalog carries it |
| `route` | the panel's mount point (`admin.route`, default `/milpa/admin`) — for a link back into the panel |
| `meta['gate']` | the gate in effect, as the topbar chip names it: `loopback` \| `custom` \| `passkey` \| `open` \| `fallback` |
| `meta['section']` | the id of the section the panel is showing |
| `meta['query']` | the request's query params — how a **declared view** reads what `props['query']` gives the narrow shape |
The props are the section's own `props` plus `query` (the request's query params). A guest reads the context and
its props; it never reads the request, and it never reads a cookie — the panel does not either.
**The header.** Above every section — the panel's own included — the host paints the section header: the title
(a catalog key translated, or the literal) and the attribution, read from the catalogue's record of which plugin's
`adminSections()` returned it, never from the section:
The short class name is shown (`declared by DesktopAppPlugin` · `declarada por DesktopAppPlugin`), the full class
travels in `data-declared-by`. A guest brings the region; the host brings the header, the sidebar and the topbar.
**The sidebar.** Sections list under their `group`, one heading per distinct value, in the house order — **`admin`**
(the panel's own: Plugins, Routes, Settings, Stack, Dev tools) → **`app`** (the default — a plugin's sections) →
**`agent`** (the agent's own surfaces: the Desktop's Agent section) → any other group name, alphabetically —
case-insensitively and in its own alphabet (`año` sorts among the a's, `Zeta` after `beta`). The headings come from
the catalog (`ADMIN / APP / AGENT` · `ADMIN / APP / AGENTE`); a group the catalog does not know is headed by its own
name uppercased in its own alphabet (`año` → `AÑO`). The glyph a section declares as `icon` is painted before its
label.
**The order.** Within a group, sections sort by `order` (lower first; ties break by `id`, alphabetically). The panel
**opens** on the first section in that same (`order`, `id`) order across every group, so the convention matters: the
panel's own take **10..40** (Plugins 10, Routes 20, Settings 25, Stack 30, Dev tools 40); a guest picks an order
**after those** — greenhouse `decisions/0210` names 60 for the Desktop's Agent section — unless it means to be the
page the panel opens on. A guest at order 10 named `agent` would tie with Plugins, win the tie by id, and become the
front page.
## A section may declare a whole VIEW
One component is the narrow case. A plugin that brings its own UI declares a **view** — a tree of components, the
definitions and renderers it needs, the props per component and the signals the page must seed — and the panel
mounts it inline, in its own main, under its own header, with **one runtime for the whole page** (greenhouse
`decisions/0211`). No frame, no second document, no second Alpine: the panel's theme applies to the guest's region
— measured in a browser, flipping `` repaints the guest's own surfaces, which an iframe could not
do. One endpoint over one registry also makes a `RenderEffect` from a host component able to repaint a guest's —
the mechanism is there; this slice did not exercise it. What the panel does **not** do yet is reach INTO the
region: the topbar's search is the inert `dashboard-topbar` primitive and nothing wires it to a guest, so
`decisions/0211`'s S3-F1 falsifier («the topbar's search reaches the composer») is **not delivered**.
The constructor takes the same thing as `view:` — `AdminSection::ofView()` is the one-liner. A section declares a
view **or** a component, never both, and a view carries no section `props` (its props are per component).
**What the host does with it.**
- **It registers the tree.** Every name in `definitions` enters the panel's registry under a **layer of its own**,
labelled with the section's id. Names the panel registers itself (`metric-card`, `dashboard-*`, `admin-sidebar`,
`admin-section-header`) may be NAMED but never redefined (`ReservedComponentException`), and two sections binding
one name to **different** definitions throw `milpa/live`'s own `ComponentNameConflictException` naming the
component and both sections — the same rule everywhere: identity or a stateless class, never a structural compare.
The **renderer** is held to that same rule by the panel (`RendererConflictException`): two sections may legitimately
share one definition and still each bring their own renderer, and the renderer registry resolves the last one
registered — so without a rule of its own the first section's surface would be repainted by the second's renderer,
silently, in the one place whose whole point is that a collision is loud. Share the instance, or name your own
component. Either refusal leaves the book as it was.
- **It emits one runtime.** Every renderer that implements `DeclaresClientAssets` contributes its `.css` and `.js`;
the panel merges them (deduplicated by URL) and hands them to `LiveBoot::html()`, which emits — in ``,
after the panel's own stylesheets — the declared styles, the boot payload, `milpa-live.js`,
`milpa-live-remote.js`, the guest modules in declared order, and **Alpine last**, each `defer`, each URL once.
The panel hand-writes no runtime `