- 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 401 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, 401 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`) or
`tag` (a vision model). 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.
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.
### Remote commands (stacksjs/stacks#960)
Running a configured operation on a configured host over SSH. Deliberately NOT
a terminal: 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. An
interactive session is tracked separately - `Bun.spawn` has no PTY, and
`ssh -tt` gives a remote one but cannot propagate a window resize.
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.
**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`,
`LiveTerminalOutput`
- `JobMonitor`, `QueueStatus`
- `SettingsForm` (generic, used by all settings pages)
## Dashboard Actions
Located in `storage/framework/defaults/app/Actions/Dashboard/`:
- Settings actions - get and update typed settings
- Commerce actions - CRUD operations for commerce models
- Content actions - CRUD operations for content models
- Data actions - persisted model records and metrics
- Deployment actions - deploy, script, terminal, and history operations
- Job actions - job records and metrics
- Notification actions - notification records and delivery metrics
- Request actions - captured request analytics
## Model Dashboard Integration
Models with `dashboard: { highlight: true }` appear prominently:
```typescript
defineModel({
name: 'Product',
dashboard: { highlight: true }, // highlighted in dashboard
traits: {
useApi: { uri: 'products', routes: ['index', 'store', 'show', 'update', 'destroy'] }
}
})
```
The `useApi` trait auto-generates REST actions and routes for the model. The
generic model explorer discovers the model separately. It does not generate a
custom dashboard page.
Use a dashboard-scoped Action when a page needs an aggregate response or a
purpose-built transport shape. Register it under `/api/dashboard/*` in
`storage/framework/defaults/routes/dashboard-api.ts`. Sensitive reads and all
writes must use the route file's `guard()` boundary so local development stays
usable while non-local environments require authentication and an admin role.
The dashboard dev server delegates `/api/*` requests to the Stacks router. It
does not delegate root-level application API groups such as `/payments/*`.
Dashboard pages must call a registered `/api/dashboard/*` Action instead of
hard-coding the separate API server port. User-scoped payment data is the
exception to the local no-auth guard: register it with `authenticatedGuard()`
so the bearer token is required even on localhost.
For stateful settings, persist through a model with `useApi` and explicit
middleware, then expose a narrow dashboard Action for the page. Keep account
identity fields read-only when their source of truth is `config/*.ts`.
### Dashboard API client
Use the shared `dashboardApi()` client for every dashboard network request,
including requests in stores and guest pages. Do not call `fetch()` directly
from dashboard views, components, composables, or stores. The shared client
adds the stored bearer token, same-origin credentials, JSON serialization,
the double-submit CSRF header for mutations, and normalized response errors.
Pass `auth: false` only for a deliberately public route such as password-reset
or invitation-link lookup. This disables the bearer header, not CSRF
protection. Keep the route path aligned with the registered Stacks route, for
عرض على GitHub