Reference
Administration
Resource screens, role editing, jobs, site settings and inline editing.
Admin
The built-in admin is available at /nb-admin/.
The admin uses DaisyUI components. The following shortcodes from core/modules/admin/lib/ handle data loading for admin views:
[#get-resource-records resource=users role=table#]- loads all records + table-filtered fields intodata.recordsanddata.fields[#get-resource-record resource=users uuid=abc123#]- loads a single record for editing, with i18n and encryption handling[#get-resource-meta resource=users#]- loads only field definitions intodata.fields
The legacy _dep_ admin UI has been removed. Active admin routes and templates live in the non-_dep_ admin paths.
Dashboard
/nb-admin/ ([#dashboard#], core/modules/admin/lib/dashboard/) is organized around what a user needs to act on, not one card per subsystem. It renders up to five bands, plus any application bands, each present only when it has something to show for the current user's role:
-
Needs attention - failed jobs, a recent fatal error, low disk space, or (for
pull-core-updates) stale assets: core changed build inputs (css/,js/, Tailwind config,package*.json, core templates, scripts, styles, text or static files) since the commit inext/static/app.core, soapp.css/app.jsneed a rebuild.app_build_stale()incore/lib/app-build.phpdecides; a missing stamp or unknown commit never warns. The git-pull API returns the same flag asassets_stale. Absent entirely if the role has none of the relevantview-*features. -
Site status - is the site current and healthy, in one glance. Every item follows the same shape: a dark, small uppercase label (Data/Core/Ext/System), then the one fact that actually answers "is this OK" rendered large but in a lighter tone (so labels anchor the eye and values don't shout), with supporting detail as a small caption underneath. For Data, the large fact is when ("18 hours ago", with the specific resource named in the caption below it - e.g. "Projects updated" - so it's directly checkable against the matching row in Your data, not an anonymous number). For Core/Ext, the large fact is status ("Up to date" or "N updates", amber when something's pending), with the raw "Updated X ago" timestamp demoted to the caption, plus an inline Update now for roles with
pull-core-updates/pull-ext-updates. For System (view-debug), the large fact is a plain-language status - "OK" or "Low resources" (below 1GB RAM or 500MB disk free) - with the actual RAM/disk numbers as the caption and a View debug link into/nb-admin/debug. Everything here is a fact (with an action attached where one applies), never bare navigation - that's what Manage below is for. -
Visits - for roles with
view-stats: visits, pageviews and bot/scanner requests over the last 30 days with the change against the 30 days before, and one bar per day (today partial, from the running log). PHP passes UTC hourly counts fromstats_recent_days()incore/lib/stats.php; thedashboard_statsAlpine component groups them into the viewer's local days. See Request statistics. -
Application bands - every
ext/tpl/dashboard-band.tplandext/modules/<name>/tpl/dashboard-band.tpl, rendered after Visits in name order (dashboard_app_bands()). This is how an application or ext module adds its own card without touching core. The template decides its own visibility (render nothing when the user may not see it) and uses the same section styling as Visits. -
Your data - the resources the current role can see, with record counts, disk usage, and last-updated time per resource.
-
Manage - a section card like the others (
rounded-2xl bg-neutral-50 p-6 shadow, same heading style as Your data), containing grouped clusters, one per resource, laid out with equal height (items-stretchon the row) so the row reads as one set rather than cards of mismatched size. Each cluster follows a strict internal order: a dark small uppercase heading, a caption fact (sametext-neutral-500as Site status), then - only if the cluster genuinely has one - its single most important pill (e.g.Media Library (N),Jobs (N), orAdd userwhen a cluster's main draw is a create action rather than a list), and finally a secondary row where everything else (remaining entry pills likeUsers (N)/Roles (N), plus every other action) renders as plain underlined text links, all sharing one canonical link style (dashboard_secondary_link_class()indashboard.php, reused as-is by the Site status "Update now"/"View debug" links and everyquick-action-*.tplpartial - one style everywhere, not a per-context variant). A cluster has exactly one pill-weight element, never two competing for attention:- Users & roles: "N active sessions" caption,
Add userpill (the create action is the main reason to visit this cluster), thenUsers (N)/Roles (N)/Sign out everyone (N)as secondary text links. - Media library: "Updated X ago" caption (the library's own last-modified time),
Media Library (N)pill, thenClear thumbnail cache/Delete unused mediaas secondary text links. - System: "Scheduler last ran" caption,
Jobs (N)pill (N = currently queued jobs - Jobs lives here, not its own cluster, since queue/scheduler and general site maintenance are the same "background infrastructure" concern), thenRun jobs now/Clear data cacheas secondary text links andView system logfor roles withview-system-log. Data cache (ext/data/.tmp/cache/_data, the per-resourcedata_read_all()result cache fromcore/lib/data.php- normally self-invalidating on resource modification, but clearable here for troubleshooting) lives here rather than in Media library - it's a site-wide cache, not a media one, even though thumbnail cache (media-specific,ext/static/_thumb_) does belong in Media library.
"Add role" is deliberately not a cluster action - not a daily action, so Roles is a plain secondary link, not a create button. The System cluster doubles as the only entry point into
/nb-admin/jobsanywhere in the admin. Site settings is not a Manage cluster - it has no fact of its own to show, so it's a small gear-icon button in the dashboard's own header (top-right,core/modules/admin/uri/nb-admin/dashboard-settings-btn.tpl, gated onedit-.config) instead.Needs attention's items follow a similar "fact, then a light secondary pill action" shape (
core/modules/admin/lib/dashboard/attention.tpl) - no em dashes separating message from action; the action sits on its own line below the message instead.Every Site status item and Manage cluster heading (
dashboard_status_heading()indashboard.php) uses the project's actual brand color (text-primary, fromext/tailwind.theme.js) rather than plain gray - one consistent accent, not a different hue per category. A per-category rainbow was tried and rejected (read as visual noise); a small colored icon badge per heading was also tried and rejected (it indented the label relative to the caption/value below it, breaking the column's flush-left edge).text-primarystays reserved for these headings only - amber/red keep their separate meaning (attention/warning) elsewhere on the page. - Users & roles: "N active sessions" caption,
Users, Roles, and Media Library are not in the nimblybar's Resources sidebar dropdown (core/tpl/nimblybar/menu-resources.tpl) - that list shows only the project's own ext-defined resources. Users/Roles/Media Library are reachable from the dashboard's Manage clusters instead, keeping the sidebar scoped to "this project's data."
Roles and permissions editor
/nb-admin/roles/(id) and /nb-admin/roles/add share one permissions matrix component (core/modules/admin/lib/role-permissions/, [#role-permissions [#role-id#]#] / [#role-permissions new#]) so the add and edit flows never drift apart. The matrix renders one row per resource (including the hidden .content, .config, .files, .jobs resources) with per-operation checkboxes plus a manage-<resource> column, and a separate (all) toggle for super-admin roles. Unchecking (all) never clears the individual checkboxes underneath - they are only hidden while (all) is active, so switching it off restores whatever was set before.
Identity (name/description) and permissions are saved together from a single form (core/modules/admin/lib/role-identity/) via one PUT/POST to the generic /api/v1/roles endpoint - there is no separate role-permissions API. The client (matrix-sync.js) collapses a row's checked operations into manage-<resource> when every operation is checked, and builds the final comma-joined features string sent as a plain field alongside the role's name/description.
.files_meta, files_unused and files_usage are not resources with their own row in the matrix - they are internal, derived data. permission_normalize_hidden_alias() (core/modules/user/lib/permissions.php) aliases them to .files, so granting a role any operation on the Files row also covers file metadata lookups, the unused-file check and the "Used in" filter the media picker relies on. Do not add separate matrix rows or permission tokens for them.
Jobs and Site settings
/nb-admin/jobs (core/modules/admin/lib/jobs-panel/) is a diagnostic view over the job queue and scheduler - queued/running/done/failed counts, a table of jobs with their last error, and "Run due jobs now" / "Prune old jobs" actions, gated on view-.jobs / manage-.jobs. "Prune old jobs" removes both done jobs (aged off completed_at) and terminally failed jobs with no retries left (aged off _modified) older than 30 days - the same rule the scheduled jobs:prune --days=30 task applies, so a fixed-and-forgotten failure eventually drops off the Needs attention band on its own. Each done/failed row also has its own Delete button (gated on manage-.jobs) for clearing an individual job immediately regardless of age - queued/running rows show it disabled, since deleting an in-flight job could orphan a lock or a running process.
/nb-admin/settings (core/modules/admin/lib/site-settings/) is one flat page gated on edit-.config. It edits the site-wide .config/site record: name, description, languages, text direction (direction, ltr/rtl, rendered as <html dir>) and admin sidebar position (nimblybar.side). Name, description, direction and side are per-language values, and each field has its own language switch. On save, direction and side are filled in for every site language: Arabic, Persian and Hebrew start rtl with the sidebar on the right, and other languages start ltr and left. For users with manage-system, it also holds the custom pages switch with its page types, and the navigation editing switch (.config/managed_pages). Each switch shows only when there is a pages resource or navigation slots to switch on. Pages appear under Resources in the Nimbly bar; the Navigation admin tab is hidden for everyone while its switch is off. This is distinct from the per-page title editor behind the nimblybar gear icon, which edits page_settings.page_title for the current page only.
Resource record switcher
Generic resource edit pages (/nb-admin/(resource)/(id)) include a record switcher (core/modules/admin/lib/resource-switcher/) so an editor can jump between records without going back to the list. It picks a UI based on record count: a handful of records render as pills, up to ~100 render as a native <select> (browsers already support type-ahead-to-jump), and beyond that it switches to a debounced live-search input backed by resource_get()'s search/limit GET params (core/modules/api/lib/api.php) rather than loading every record client-side.
Rich content fields: end-to-end
This section shows the full flow for an editable HTML field: resource definition → template output → inline admin editing.
Step 1: Define the field in .meta
"intro": {
"name": "Intro",
"type": "html",
"buttons": "bold,italic",
"admin_col": false,
"i18n": true
},
"body": {
"name": "Body",
"type": "html",
"buttons": "h2,h3,h4,bold,italic,orderedlist,unorderedlist,quote,anchor",
"media": true,
"media_sizes": "sm-90,md-70,lg-60,xl-50,xxl-40",
"admin_col": false,
"i18n": true
}
buttonsis mandatory - always specify the exact toolbar buttons neededmedia: trueenables the media browser; requiresmedia_sizesadmin_col: falseis mandatory for html fields (too large for table view)- Add
i18n: trueif the site is multi-language
Step 2: Load the record in route.inc
<?php
router_deny();
$parts = router_match(__FILE__);
if ($parts === false) return;
load_library('data');
$record = data_read('articles', $parts[0]);
if (!$record) return;
set_variable('article', $record);
router_accept();
Step 3: Output in the template
Use [#get-html#] to output an editable html field. When a logged-in admin views the page, the field becomes inline-editable directly in the frontend.
<div class="prose">
[#get-html article.intro#]
</div>
<div class="prose prose-lg">
[#get-html article.body#]
</div>
For multi-language content, [#get-html#] resolves i18n field objects automatically. Add a language suffix when you explicitly need one language:
[#get-html article.body#]
[#get-html article.body.nl#]
Step 4: Enable inline editing
Add [#html#] to the route and ensure the user module is active (auto-loaded). Logged-in editors (edit-inline-content) then get the page actions pill: Edit switches inline editing on and off (asking to save or discard unsaved changes), and the settings button opens page settings or the record's admin page (page-settings-link). The pill can be dragged anywhere and docks in a corner when dropped near one. While editing, the focused field shows its field bar with Save.
Inline editing attributes
For finer control over the inline editor (e.g. different toolbar than the .meta default, or to enable media on a field that doesn't have it in .meta), add data-nb-edit directly to a wrapper element:
<div data-nb-edit="event.[#record.uuid#].main_text" data-nb-edit-options='{
"buttons":"h2,h3,bold,italic,anchor",
"media": true,
"media_sizes":"md-70,lg-60"}'>
[#get-html record.main_text#]
</div>
For inline image replacement, use data-nb-edit-img on the image wrapper:
<div data-nb-edit-img="event.[#record.uuid#].main_img">
[#get-img-html [#record.main_img#] sizes="xs-100,md-50"#]
</div>
Both attributes only activate for logged-in admins. The value format is resource.uuid.field.