@matthamlin/dash
v0.0.2
Published
Dash is a local-first attention dashboard. It collects signals from plugins, stores them as normalized attention items, ranks the active inbox, and gives a small web UI for reviewing, snoozing, dismissing, completing, or opening the source of each item.
Readme
Dash
Dash is a local-first attention dashboard. It collects signals from plugins, stores them as normalized attention items, ranks the active inbox, and gives a small web UI for reviewing, snoozing, dismissing, completing, or opening the source of each item.
The app is packaged as @matthamlin/dash. In local use it runs as a Node/Bun CLI-backed web app
with a SQLite database under the user's data directory. The same Hono API can also run as a
Cloudflare Worker with a D1 database binding for deployed API persistence.
What Dash Does
Dash is intended to answer one question: "What needs my attention now?"
It does that by:
- Registering source plugins such as Codex sessions, GitHub, Dodo, and command harnesses.
- Refreshing plugins into a shared attention item model.
- Ranking active items by priority, due dates, source urgency, recency, snoozes, pins, and manual boosts.
- Recording plugin refreshes, plugin actions, and schedule runs as auditable run records.
- Producing scheduled Morning Brief items from the current inbox, optionally through a harness plugin such as Codex, Claude, Pi, or OpenCode.
The dashboard UI has two main routes:
/shows the ranked inbox, selected item details, item actions, and recent runs./settingsshows plugin health/toggles and Morning Brief schedule settings.
Getting Started
Install dependencies from the repo root:
bun installBuild Dash:
bun run --cwd apps/dash buildStart the packaged local app:
bun run --cwd apps/dash start -- --port 3867Dash starts on http://127.0.0.1:3867/ by default and opens the browser automatically. Use
--no-open for automation:
bun run --cwd apps/dash start -- --no-open --host 127.0.0.1 --port 3867For UI development, use the Vite dev server:
bun run --cwd apps/dash devThe CLI command loads the built RSC server from dist/rsc/index.js, so run the build before
using dash start or the package-local start script.
Local Data
Local Dash uses SQLite through bun:sqlite when running under Bun and node:sqlite otherwise.
The database file is named dash.sqlite.
The default data directory is resolved in this order:
DASH_DATA_DIR, when set.$XDG_DATA_HOME/dash, whenXDG_DATA_HOMEis set.~/Library/Application Support/dashon macOS.%APPDATA%/Dashon Windows.~/.local/share/dashon other platforms.
For isolated testing or demos, set DASH_DATA_DIR:
DASH_DATA_DIR="$(mktemp -d)" bun run --cwd apps/dash start -- --no-openAvailable Commands
Run these from the repo root unless noted.
| Command | Purpose |
| -------------------------------------------- | ---------------------------------------------------------- |
| bun run --cwd apps/dash dev | Start the Vite development server. |
| bun run --cwd apps/dash build | Build the RSC app and CLI entrypoint into dist/. |
| bun run --cwd apps/dash start -- --no-open | Start the built local dashboard without opening a browser. |
| bun run --cwd apps/dash test | Run Dash unit and Node test suites. |
| bun run --cwd apps/dash type-check | Type-check the app. |
| bun run e2e:dash | Build Dash and run the Playwright dashboard smoke suite. |
| bun run format:updated | Format changed files. |
| bun run ci | Run the repo quality gate. |
Before handing off changes in this repo, run:
bun run format:updated
bun run ciArchitecture
Dash has a deliberately small runtime split:
src/server.tsxcreates the Worker-compatible Hono app, mounts API routes, then delegates page rendering to Guava RSC routes.src/local-server.tswraps the built app with a Node server, static asset serving, local storage, local built-in plugins, and the in-process scheduler.src/cli.tsis the publisheddashexecutable. It finds an available port, loads the built app, starts the local server, and handles shutdown.src/dash-api.tsowns the HTTP API shared by local and Worker runtimes.src/dash-store.tsowns the persistence model and database queries.src/dash-local-database.tsadapts Bun/Node SQLite to the store interface.src/worker-api.tsadapts a Cloudflare D1 binding namedDBto the same store interface.src/dash-plugin.tsdefines the plugin contract, registry, refresh flow, actions, health state, and config defaults.src/dash-inbox.tsranks items and maps item actions to state patches or source targets.src/dash-scheduler.tsowns the Morning Brief schedule and the timer-backed local scheduler.src/dash-dashboard.tsxandsrc/dash-client-data.tsxprovide the client UI and API-backed client state.
The UI uses React 19, Guava RSC, Hono, Tailwind CSS, Vite, and the local Switchboard package for client-side data state.
Persistence Model
The store creates and maintains these tables:
items: source-owned attention items keyed by plugin/source identity.item_state: user-owned state for read, done, dismissed, snoozed, pinned, and manual boost data.plugins: registered plugin metadata, enablement, health, and last refresh time.plugin_config: persisted plugin configuration JSON.settings: app-level settings such as the default harness plugin.schedules: schedule definitions, currently including the default Morning Brief.runs: plugin refresh, plugin action, and schedule run history.schema_migrations: lightweight schema bookkeeping.
Source data and user state are split so a plugin can refresh an item without erasing local review state.
HTTP API
The API is mounted under /api/v1.
| Route | Purpose |
| --------------------------------------- | ------------------------------------------------------------------------------------------ |
| GET /health | Health check. |
| GET /items | List stored attention items. |
| GET /inbox | List ranked inbox items. Supports includeDone, includeDismissed, and includeSnoozed. |
| POST /items/refresh | Upsert items for a plugin. Useful for manual ingestion and tests. |
| PATCH /items/:itemId/state | Apply item state directly. |
| POST /items/:itemId/actions/:actionId | Run a built-in item action, source open action, or plugin action. |
| GET /plugins | Sync and list registered plugins. |
| PUT /plugins/:pluginId | Update plugin metadata and enabled state. |
| GET /plugins/:pluginId/config | Read stored plugin config. |
| PUT /plugins/:pluginId/config | Replace stored plugin config. |
| POST /plugins/:pluginId/refresh | Refresh one plugin. |
| POST /plugins/refresh | Refresh every registered plugin. |
| GET /runs | List recent run records. |
| GET /settings | Read app settings. |
| PUT /settings | Update app settings. |
| GET /schedules | Ensure and list schedules. |
| PUT /schedules/:scheduleId | Create or update a schedule. |
| POST /schedules/:scheduleId/run | Run a schedule immediately. |
When the Worker runtime has no DB binding, API routes that need storage return
503 Dash API store is not configured.
Plugins
Plugins turn external or local signals into attention items. A plugin is an object with:
pluginId: stable source identifier.name: display name.description: optional display/help text.configSchema: typed config fields and defaults.refresh(context): returns attention items, health, and optional run output.actions: optional item or harness actions.
The registry persists plugin defaults on sync, skips disabled plugins during refresh, catches failed
plugins without stopping the rest of a refresh-all run, updates plugin health, and records each
refresh/action in runs.
Built-in Plugins
The Worker registry currently includes only:
manual: stores attention items submitted directly through the API.
The local server registry includes the Worker plugins plus:
codex-harness: scans local Codex session logs and can run prompt actions throughcodex.claude-harness: runs scheduled prompts through the Claude CLI.pi-harness: runs scheduled prompts through the Pi CLI.opencode-harness: runs scheduled prompts through the OpenCode CLI.github: collects GitHub issues and pull requests through the localghCLI.dodo: collects local repo tasks from.dodo/tasks.jsonlfiles.
Harness plugins expose the shared run-harness-prompt action. The Morning Brief schedule can call
that action to turn ranked dashboard data into a generated brief.
Adding A Plugin
Create a plugin factory that returns AttentionPlugin, then register it in the appropriate
registry:
- Add deploy-safe plugins to
src/builtin-plugins.ts. - Add local-only plugins that shell out, scan local files, or depend on local credentials to
src/local-built-in-plugins.ts.
Minimal shape:
import type { AttentionPlugin } from "./dash-plugin.ts";
export function createExamplePlugin(): AttentionPlugin {
return {
pluginId: "example",
name: "Example",
description: "Collects example attention items.",
configSchema: {
fields: [
{
key: "maxItems",
label: "Max items",
type: "number",
defaultValue: 10,
},
],
},
async refresh(context) {
let maxItems = typeof context.config.maxItems === "number" ? context.config.maxItems : 10;
context.log("Refreshing example plugin", { maxItems });
return {
items: [
{
pluginId: context.pluginId,
sourceId: "example-1",
kind: "example",
title: "Example item",
priority: "normal",
metadata: {
sourceUrgency: 1,
},
},
],
health: {
status: "healthy",
message: "Example plugin refreshed",
},
};
},
};
}Use stable sourceId values. Reusing the same pluginId and sourceId lets Dash update an item in
place while preserving its user state.
Plugin Item Guidance
Good attention items include:
- A stable
sourceId. - A compact
kindsuch asgithub.pr,dodo.task, orcodex.session. - A clear
title. bodytext that explains why the item matters.urlor metadata such assourcePath,filePath, orpathwhen the item can be opened.prioritywhen the source has a strong signal.dueAtwhen the source has a real deadline.metadata.sourceUrgencyfor source-specific ranking pressure.metadata.dashActionswhen the item should expose plugin-backed actions.
Avoid using refresh output as a side-effect channel. A refresh should collect and normalize signals; actions should perform mutations or external follow-up work.
Scheduling
Dash currently ships one default schedule: morning-brief.
The schedule:
- Is created lazily by
ensureDefaultSchedules. - Starts disabled.
- Runs daily after its configured local time.
- Refreshes selected source plugins, or all non-manual non-harness source plugins if no selection is configured.
- Ranks the current inbox and builds a prompt from the top items.
- Optionally sends that prompt to the selected/default harness plugin.
- Stores the result as a manual
brief.morningattention item. - Records the schedule run in
runs.
The local server starts startDashScheduler, which checks due schedules every minute while
dash start is running. That timer is the local runtime path. A future cron or OS scheduler should
call the same runDueSchedules(...) logic rather than replacing the in-process local behavior.
Deployment
Dash is configured as a Cloudflare Worker app in wrangler.jsonc.
To deploy persistent API routes, configure a D1 database binding named DB:
{
"d1_databases": [
{
"binding": "DB",
"database_name": "dash",
"database_id": "...",
},
],
}Then run:
bun run --cwd apps/dash deployLocal-only plugins are intentionally not registered in the Worker runtime because they depend on
local files, local CLIs, or local credentials. Promote a plugin to builtin-plugins.ts only when it
is safe and useful in the Worker environment.
Testing
Dash has focused tests around:
- CLI option parsing.
- API behavior.
- Local server behavior.
- Store persistence and item state.
- Inbox ranking and item actions.
- Plugin registry refresh/action behavior.
- Built-in plugins.
- Scheduler and Morning Brief behavior.
- Package metadata for the published CLI.
Run the app suite:
bun run --cwd apps/dash testRun the app E2E suite:
bun run e2e:dashThe E2E suite builds the app, starts the packaged CLI with an isolated temporary DASH_DATA_DIR,
seeds an inbox item through the API, and verifies the dashboard can complete it.
Maintenance Notes
- Keep published runtime dependencies free of
workspace:*entries. Local workspace packages used for build/test integration belong indevDependencies;src/__tests__/package.test.tsenforces that the published CLI has no workspace runtime dependencies. - Keep local-only behavior in
local-server.tsorlocal-built-in-plugins.ts. The Worker path should remain deploy-safe and not assume access to local files, local shells, or user credentials. - Preserve the shared API/store boundary. Local SQLite and Worker D1 both adapt to the
DashDatabaseinterface used bycreateDashStore. - Make plugin refreshes idempotent. Use stable source identities and return the current source state instead of appending duplicate items.
- Record external mutations as plugin actions. Actions get their own run records and can return
itemStatePatchwhen the UI should update the item after the action succeeds. - Update tests near the layer being changed. Plugin contract changes should cover
dash-plugin.test.ts; ranking changes should coverdash-inbox.test.ts; schedule changes should coverdash-scheduler.test.ts; local server packaging changes should cover E2E where possible. - Keep scheduler changes compatible with local
dash start. The current app expects due schedules to run while the local server is alive. - When adding config fields, provide defaults in the plugin schema and consider whether the current settings UI needs a new editor surface. Plugin config can already be read/written through the API.
Troubleshooting
Dash has not been built. Run bun run build before dash start.
Build the app first:
bun run --cwd apps/dash buildDash API store is not configured
The Worker runtime does not have a DB binding. Add the D1 binding in wrangler.jsonc, or run the
local CLI-backed server.
No available port found starting at 3867
Pass a different port:
bun run --cwd apps/dash start -- --port 5195Plugin refresh reports an auth or command failure.
Check the plugin's config through the settings/API and verify the local CLI it uses is installed and
authenticated. The github plugin requires gh auth status to pass before it collects items.
