- name
- stacks-dashboard
- description
- Use when building or customizing the Stacks admin dashboard, including dashboard pages, model management views, analytics widgets, commerce dashboards, content management, settings panels, deployment monitoring, job/queue management, or the 408 built-in dashboard components. Covers the dashboard system at storage/framework/defaults/.
- license
- MIT
- compatibility
- Bun >= 1.3.0, TypeScript
- allowed-tools
- Read Edit Write Bash Grep Glob
# Stacks Dashboard
The Stacks admin dashboard provides a full-featured admin panel with 100+ route views, 408 components, and a multi-section layout.
## Key Paths
- Dashboard components: `storage/framework/defaults/resources/components/Dashboard/`
- Dashboard route views: `storage/framework/defaults/views/dashboard/`
- Dashboard layouts: `storage/framework/defaults/views/dashboard/layouts/`
- Dashboard actions: `storage/framework/defaults/app/Actions/Dashboard/`
- Dashboard page endpoints: `storage/framework/defaults/routes/dashboard-api.ts`
- Dashboard navigation registry: `storage/framework/defaults/resources/functions/dashboard/sidebar.ts`
- Configuration: `config/ui.ts`
## Dashboard Sections
Dashboard route views are mounted at the dashboard server root. Do not prefix
page links with `/dashboard`. The `/api/dashboard/*` prefix is reserved for
dashboard data Actions.
### Analytics and Monitoring
- `/` - main dashboard overview
- `/analytics` - analytics hub and chart navigation
- `/analytics/web`, `/analytics/pages`, `/analytics/referrers` - HTTP analytics
- `/analytics/countries`, `/analytics/devices`, `/analytics/browsers` - audience breakdowns
- `/analytics/events`, `/analytics/blog`, `/analytics/marketing` - domain analytics
- `/requests` - captured HTTP request metrics
- `/errors`, `/monitoring/errors` - error tracking and analysis
- `/jobs`, `/jobs/history` - background job monitoring
- `/queue` - queue management and metrics
- `/queries`, `/queries/slow`, `/queries/history` - query analysis
### Commerce
- `/commerce/dashboard` - commerce overview
- `/commerce/pos` - point of sale
- `/commerce/products` - product management
- `/commerce/orders` - order management and processing
- `/commerce/customers` - customer profiles and history
- `/commerce/payments` - payment tracking
- `/commerce/coupons` - coupon and promotion management
- `/commerce/gift-cards` - gift card management
- `/commerce/categories`, `/commerce/manufacturers`, `/commerce/units` - catalog metadata
- `/commerce/variants`, `/commerce/reviews`, `/commerce/taxes` - catalog operations
- `/commerce/waitlist/products`, `/commerce/waitlist/restaurant` - waitlists
- `/commerce/delivery` and `/commerce/delivery/*` - delivery, shipping, driver, and license management
### Content Management
- `/content/dashboard` - content overview
- `/content/posts` - blog post CRUD
- `/content/pages` - page management
- `/content/authors` - author profiles
- `/content/categories` - content categorization
- `/content/tags` - tag management
- `/content/comments` - comment moderation
- `/content/files`, `/content/blog`, `/content/seo` - files, blog operations, and SEO
### The file manager's two layers (stacksjs/stacks#2577)
Worth knowing before adding anything to it, because the split is not obvious
from the endpoints:
- **Storage operations** map to a `StorageAdapter` method and go straight to the
disk: list, upload, create folder, rename, visibility, duplicate, delete.
- **Metadata** - favourites and tags - has nowhere to live on a disk (extended
attributes do not survive a copy; S3 object metadata is set at write time, so
starring a 2 GB video would rewrite 2 GB). It lives in `storage_items`, keyed
by `(disk, path)`, written by `PUT /files/favorite` and `PUT /files/tags`.
**The disk is authoritative and the table is advisory.** The listing comes from
the disk and rows are joined onto it, so a path with no row is a file with
nothing recorded - which is most files. Renames and deletes made THROUGH the
dashboard reconcile eagerly (a folder is a prefix update, because moving a
folder moves everything under it); a completed listing sweeps rows for paths it
did not see, which is free because the walk already enumerated them. A TRUNCATED
listing sweeps nothing - it has not proved a path is absent.
A file renamed outside the dashboard loses its metadata, and that is by design:
a rename and a copy-then-delete are the same two events to a bucket listing, so
reconciling would be guessing.
### The media pipeline (stacksjs/stacks#2578)
None of the three things an upload might need can happen inside the request: a
transcode is minutes, a vision call is a round trip to a third party. So an
upload dispatches and the dashboard shows state.
- `storage_item_tasks`, one row per `(disk, path, kind)`, kind being
`optimize` (images, via `ts-images`), `transcode` (video, via `ts-videos`),
`tag` (a vision model) or `preview` (the file manager thumbnail, #308). They succeed and fail independently, which is why this
is not a column on `storage_items` - a video whose transcode finished and
whose tagging failed is a normal state.
- `dispatchDashboardFileTasks` decides from the CONTENT TYPE what a file needs.
Most uploads are documents and get nothing. A transcode waits for a video
profile, because the ladder is derived from the source dimensions.
- A dispatch failure is RECORDED, not thrown: a queue that is down leaves a
visible failure rather than an upload that fails or a file that is silently
never processed.
- `runTask` owns the queued -> running -> done/failed transitions so the three
jobs cannot disagree about them. It rethrows after recording, because the row
and the queue answer different questions - the queue decides whether to retry,
the row is what somebody looking at the file sees.
- `POST /files/reprocess` re-runs everything, or the kinds you name.
- `preview` is decided by `file-preview.ts`, from the type the file CLAIMS
(the disk's answer, or the extension when that is `octet-stream`), and the
job checks the bytes agree before drawing. Images, WAV waveforms and TrueType
specimens become `.variants/<path>/preview.png`; text becomes a
`preview.txt` snippet. SVG, video, compressed audio, PDF, CFF/WOFF fonts,
HEIC and TIFF are recorded `skipped` with the reason in `error` - no
homegrown renderer draws them, and SVG is never rasterized. Never add a
renderer that shells out: that is the rule the skips exist to keep.
- `skipped` is terminal: `runTask` records a thrown `StorageTaskSkipped`
without rethrowing, so the queue does not retry it, and `aggregateTaskState`
ignores it unless nothing else applied.
Derivatives are written back to the same disk under `.variants/<path>/`. The
leading dot keeps them out of the listing, which skips hidden components - a
folder of thirty derivatives beside every photo makes the browser useless.
A rename or delete through the dashboard moves or removes that folder with the
file, after the storage operation succeeds and best effort, since the task rows
already followed and a missing derivative is regenerated by reprocessing.
### Remote commands (stacksjs/stacks#960)
Running a configured operation on a configured host over SSH: the request
names a host KEY and a command KEY, both from `config/remote.ts`, so there is
nothing to escape and no shell to reach. The interactive terminal beside it is
covered below.
Four things make it safe to expose, and each is a rule to keep:
- **Host keys are verified.** `StrictHostKeyChecking=yes` against the host's
declared `knownHosts`. Do NOT reuse `sshExec` from `@stacksjs/ts-cloud` for
anything long-lived: it disables host key checking on purpose, for boxes a
minute old whose keys cannot be known.
- **Hosts and commands come from config, never the request.** A `RemoteCommand`
carries an `argv` ARRAY that is never interpolated.
- **The routes do NOT use `guard()`.** That helper drops auth entirely under
`APP_ENV=local|development|test`, which here would be an unauthenticated
command runner on any dev machine on the network. They use
`authenticatedGuard`, and `remote-routes.test.ts` asserts it.
- **Authorization fails CLOSED.** The `run-remote-command` gate receives the
host and command keys; with no gate defined, every run is refused. This is the
opposite of the websocket authenticator in `@stacksjs/realtime`, which
proceeds when none is installed.
Runs are recorded before AND after - a run recorded only on completion loses the
command that hung and the one whose process died with the box. The audit sink
writes to the application log rather than the dashboard's own database, which is
the thing an operator with dashboard access could edit.
### Remote terminals (stacksjs/stacks#960)
`/remote` in the dashboard also opens an interactive shell on a host that sets
`terminal: true`. Every command-runner rule above holds, plus:
- **A host opts in, and a gate decides.** `terminal: true` on the host, and the
`open-remote-terminal` gate (receives the host key). No gate, no session.
- **A session belongs to the user who opened it.** 128-bit id; another user
naming it gets the same 404 as a missing id.
- **It does not outlive its purpose.** 15 minutes without input, 2 hours in all,
30 seconds after the last viewer leaves: the remote shell is killed, never
left running. At most 4 per user.
- **What is typed is audited line by line** in the application log, so hosts
should take keys and passwordless sudo, not typed passwords.
Transport: a local PTY (`Bun.spawn({ terminal })`, which Bun now has) running
`ssh -tt` with the pinned host key, so a browser resize reaches the remote side.
Output reaches the browser as server-sent events, base64 and sequence-numbered
for replay on reconnect; input and resize are small POSTs that each pass auth,
CSRF and the session-owner check. Sessions live in the server process.
In the browser it is `<stacks-remote-terminal host="...">` (xterm.js), from
`functions/remote-terminal-element.ts`. Two rules that module follows:
- **Nothing browser-only at module top level.** `defaults/functions` is also
auto-imported on the SERVER; a top-level `class extends HTMLElement` threw
there and took every function in the barrel down. xterm and the class load
inside `registerRemoteTerminal()`.
- **Survive being moved.** stx's `:if` detaches and re-inserts the SAME nodes, so
the element acts on a removal only if it is still detached a tick later, and
starts a fresh session whenever it is attached again.
`remote-terminal.test.ts` runs a real session through a throwaway user-level
`sshd` on a loopback port when one is installed (macOS and the CI image both
have one), including a host whose key does not match the pinned one.
**There is no ffmpeg.** #2578 asked whether video was in scope given the
external binary, its licensing and its provisioning; `@stacksjs/video` is built
on `ts-videos`, which encodes itself, so that question was already answered.
Tags go through `taggables` + `taggable_models` with `taggable_type =
'storage_items'` - the trait the CMS already uses. Do NOT declare a
`belongsToMany` to the `Tag` model for this: `taggable_models.tag_id` resolves
against `taggables`, which is a different table from `tags`.
### Data Management
- `/data/dashboard` - data overview
- `/data/users` - user management
- `/data/subscribers` - subscriber management
- `/data/teams` - team management
- `/data/activity` - persisted activity
- `/models`, `/models/{model}` - generic model registry and explorer
- `/notifications/dashboard`, `/notifications/history` - notification operations
### Mail
- `/inbox` - inbound messages from the configured mailbox provider
- `/inbox/activity` - inbound and outbound delivery activity
- `/inbox/captured` - outbound messages captured by the local log mail driver
- `/inbox/settings` - mailbox display and behavior preferences
Captured mail uses `GET /api/dashboard/email/captured` and
`GET /api/dashboard/email/captured/{id}`. Read captures through the shared
parser in `Actions/Dashboard/Email/captured-mail.ts`; do not scrape files in an
STX component or duplicate the log-driver format. Render all inbound and
captured HTML through `Email/EmailBodyPreview.stx`. It owns the sandboxed
`srcdoc` iframe, restrictive content policy, and no-referrer boundary. Never
inject message HTML into the dashboard document.
### Marketing
- `/marketing/campaigns` - campaign management
- `/marketing/lists` - email list management
- `/marketing/social-posts` - social post management
- `/marketing/reviews` - marketing review workflows
### Library
- `/library/components` - component browser
- `/functions` - function registry and scaffold
- `/releases` - release management
- `/packages`, `/dependencies` - package and dependency inspection
### Settings
- `/settings` - typed `config/*.ts` browser and editor
- `/settings/appearance` - dashboard appearance
- `/settings/billing` - account billing
- `/settings/mail` - mail configuration
- `/environment` - environment summary
- `/access-tokens` - access-token management
- `/cloud`, `/dns`, `/mailboxes` - infrastructure-specific settings
### Deployments
- `/deployments` - deployment history, deployment controls, the custom
TypeScript deploy-script editor, and visibility-aware live terminal output
- `/deployments/{id}` - one persisted Deployment model record
The deployment page composes `DeploymentList`, `DeploymentTable`,
`DeploymentPreviewDialog`, `DeployScript`, and `LiveTerminalOutput`. The
Preview action first collects the environment and optional domain, then calls
the guarded `POST /api/dashboard/deployments/preview` Action. That Action runs
the native `buddy deploy --dry-run --json` planner and returns its versioned,
non-mutating plan. The dialog renders the ordered operations and resolved sites
before the user may continue to the separate real deployment confirmation.
Script reads and atomic writes use
`GET|PUT /api/dashboard/deployments/script`. The terminal uses
`GET /api/dashboard/deployments/terminal` and pauses polling while the document
is hidden. Do not create separate `/deployments/scripts` or
`/deployments/live-terminal` pages.
Deployment recovery uses `POST /api/dashboard/deployments/rollback/preview`
followed by `POST /api/dashboard/deployments/rollback`. The preview must run
the native `buddy deploy:rollback --dry-run` path successfully, and execution
must re-run that preview, compare its revision, and require the typed target
environment confirmation. Never implement rollback by editing release links,
restarting services directly, or guessing a prior release from Deployment
model rows. Deployment rows are application history. Preserved releases and
activation are owned by ts-cloud.
### Operations control plane
The Operations sidebar entry deliberately remains one item. Section navigation
lives in `Dashboard/Operations/OperationsNavigation.stx`:
- `/operations/changes` - unified change review, active work, release approvals
- `/operations/scheduler` - registered task runs and persisted pause state
- `/operations/recovery` - destinations, policies, recovery points, restore drills
- `/operations/migrations` - model diff, schema effects, ledger reconciliation
- `/operations/incidents` - native alerts, health rules, ownership, silence state
- `/operations/audit` - append-only operator events and correlations
Operational state belongs in the ts-cloud control plane initialized by
`Operations/control-plane.ts`. Use its stores for durable operations, events,
releases, approvals, alerts, backups, actors, and environments. Do not create a
parallel dashboard-only JSON file or duplicate those entities in application
models. Application domain records still follow the normal `app/Models` and
`useApi` convention. Use dashboard Actions for aggregate operational views and
guard every route in `dashboard-api.ts`.
Every mutating operator action must resolve the authenticated actor and append
or correlate a control-plane event. Use `trackOperatorOperation()` for bounded
synchronous work and the native durable queue for long-running backup, restore,
or provider work. Empty states must reflect real absence of configuration or
events. Never seed operational pages with sample incidents, releases, backups,
or health data.
Migration execution must start from `previewPendingMigrations()`, audit the
ledger against live schema effects, hash the reviewed plan, and recheck it
immediately before `buddy migrate`. Only `reconcileMigrationLedger()` may
repair provable ledger drift. Partial and unverifiable migrations require human
review and must not be silently recorded.
### Utilities
- `/health`, `/insights`, `/logs` - operational health and logs
- `/servers`, `/serverless`, `/realtime` - runtime infrastructure
- `/management/permissions` - RBAC management
- `/kanban` - model-backed board management
- `/ci`, `/buddy` - CI and Buddy workflows
## Dashboard Components (250+)
### Layout Components
- `Navbar` - top navigation bar
- `Sidebar` - fixed desktop side navigation provided by the STX runtime
- `MobileSidebar` - responsive drawer around the same sidebar content
- `DashboardLayout` - reusable layout wrapper
### UI Components
- Buttons, Modals, Toasts, Alerts, Dropdowns
- Tables with sorting, filtering, pagination
- Forms with validation
- Charts and analytics widgets
- File upload components
- Rich text editors
### Action controls
- Use `Dashboard/UI/Button.stx` for every dashboard action. Do not add page-local
primary, secondary, success, warning, or danger button styles.
- The `primary` variant is the canonical Deployment `Deploy` treatment:
`bg-gradient-to-b from-blue-500 to-blue-600`.
- Keep native buttons only for controls whose visual state is their meaning,
such as tabs, sort headers, color choices, and full-surface modal backdrops.
- Use `variant="secondary"` for supporting actions and `variant="danger"` for
destructive confirmation actions.
- Use `tag="a"` whenever `href` is reactive, for example
`<Button tag="a" :href="detailsPath()">Open details</Button>`. Server rendering
cannot infer an anchor from a client-only reactive URL.
- When a submit action lives in a shared Modal footer, give the form a stable
`id` and associate the action with `<Button type="submit" form="form-id">`.
Do not duplicate the footer inside the form or use script-driven submission.
- Prefer component events and named slots over string callback props or
`data-action` markers. A `data-action` attribute is only valid when an active
host integration consumes that exact action.
### Feature Components
- `ProductForm`, `ProductList`, `ProductVariants`
- `OrderTable`, `OrderDetail`, `OrderStatusUpdate`
- `UserTable`, `UserForm`, `UserProfile`
- `PostEditor`, `PostList`, `PostPublish`
- `CouponForm`, `CouponList`
- `EmailCompose`, `EmailList`, `EmailDetail`
- `DeploymentList`, `DeploymentTable`, `DeploymentDetail`, `DeployScript`,
Voir sur GitHub