with one click
dashboard
// Build or modify the user's dashboard: widgets, pages, layouts, or custom UI.
// Build or modify the user's dashboard: widgets, pages, layouts, or custom UI.
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | dashboard |
| description | Build or modify the user's dashboard: widgets, pages, layouts, or custom UI. |
A React app embedded in the main Vesta app that serves as the user's life HQ, a personal command center for health, finances, productivity, habits, goals, and anything else they want to track and manage. Uses a sidebar layout with page-based navigation. The agent configures pages, sidebar items, and content by editing config.tsx and creating page components.
Ask the user three things before writing code, then build only after they've answered:
Exception, dreamer auto-builds. During a dream pass, the agent may add widgets without asking. See the dream skill for when and how.
~/agent/skills/dashboard/app/src/
āāā App.tsx ā layout shell (sidebar + content area)
āāā config.tsx ā EDIT THIS: define pages, sidebar nav, branding
āāā main.tsx ā do NOT modify
āāā index.css ā do NOT modify (synced from main app)
āāā pages/ ā page components (one per sidebar nav item)
āāā examples/ ā reference components (read for inspiration, BUT scale down their sizes)
āāā components/
ā āāā ui/ ā shadcn components (synced, do NOT modify)
ā āāā app-sidebar.tsx ā sidebar component (reads from config)
ā āāā site-header.tsx ā header with page title
ā āāā nav-main.tsx ā main nav items
āāā widgets/ ā reusable widget components
āāā lib/
ā āāā parent-bridge.ts ā auth + API helpers
ā āāā utils.ts ā synced utility (do NOT modify)
āāā hooks/ ā synced hooks (do NOT modify)
You can freely edit: config.tsx, App.tsx, anything in pages/, components/, widgets/, and any new files you create.
Do NOT modify: main.tsx, index.css, lib/utils.ts, hooks/, components/ui/
The dashboard uses a sidebar + page layout controlled by config.tsx. Each pages entry creates a sidebar nav item. Clicking it renders that page's component. Pages can have children to create collapsible sub-pages in the sidebar.
When adding a widget without a specified page, choose a fitting page name yourself. Group related widgets under a meaningful category (e.g., "Health" with <HeartIcon />, "Finance" with <DollarSignIcon />). If a suitable page already exists, add the widget there.
The dashboard is a high-density UI, not a standard app interface. Default shadcn components are too large for it: override them so everything feels compact. Large elements are the exception.
1. Typography
text-smtext-xs text-muted-foregroundtext-lg or text-xl font-semiboldtext-lg+ for genuinely large numbers or when the user asks for it.2. Padding, Spacing & Layout
<div className="rounded-2xl bg-secondary p-3 text-sm">p-2. Reserve p-4 for the rare case it actually needs the room.gap-2 (preferred) or gap-3. Reserve gap-4.space-y-2 instead of space-y-4.3. Buttons & Controls
<Button size="sm" className="h-8 px-2 text-xs">h-8 text-xs. Reserve full-width inputs for cases that genuinely need them.<Button> size for when the user asks for it.4. Grid Span Rules
src/pages/my-page.tsx with whatever layout fits the content (single column, tables, etc.).Example (grid layout page) for a widget-heavy page: a responsive auto-fill grid with compact gaps.
export function MyPage() {
return (
<div className="grid gap-2 grid-cols-[repeat(auto-fill,minmax(280px,1fr))]">
<SmallWidget />
<SmallWidget />
</div>
)
}
pages array in config.tsx:import { MyPage } from "./pages/my-page"
import { StarIcon } from "lucide-react"
// Inside config.pages:
{ id: "my-page", title: "My Page", icon: <StarIcon />, component: MyPage },
1. No client-side external API fetches. The dashboard runs in a browser; cross-origin requests to third-party APIs (weather, finance) fail due to CORS. Create a skill that fetches data server-side, expose it as an endpoint, and call it from the dashboard using apiFetch from @/lib/parent-bridge.
2. Persist user data server-side. The user reads the dashboard across devices, so the source of truth lives in a skill with API endpoints. Hardcoded data and localStorage shouldn't act as the canonical store.
3. localStorage is for local visual state only. Use it for device-specific UI states (sidebar order, collapsed sections, selected tabs). Prefix keys with vesta-dashboard-.
4. Loading and missing data:
[] or {} so .reduce(), .map(), and charts don't crash.5. Data freshness: Default to fetching on mount (useEffect). For data that updates through the day, add a compact refresh button. Reserve polling (setInterval) for live data like timers or stock tickers, and ask the user how often it should auto-refresh.
shadcn/SKILL.md and its linked rules.<Flame /> for streaks, <CheckCircle /> for completed).text-green-500, bg-amber-100, border-pink-400) for badges, progress bars, and status indicators.Rebuild, re-register with vestad, restart the preview server, and notify the Vesta app:
cd ~/agent/skills/dashboard/app && npx vite build
PORT=$(curl -sk -X POST https://localhost:$VESTAD_PORT/agents/$AGENT_NAME/services \
-H "X-Agent-Token: $AGENT_TOKEN" -H 'Content-Type: application/json' -d '{"name":"dashboard","public":true}' | python3 -c "import sys,json; print(json.load(sys.stdin)['port'])")
screen -S dashboard -X quit 2>/dev/null
screen -dmS dashboard sh -c "cd ~/agent/skills/dashboard/app && npx vite preview --port $PORT --host 0.0.0.0"
# Wait for the server to be ready
for i in $(seq 1 20); do curl -s -o /dev/null http://localhost:$PORT && break; sleep 0.5; done
# Smoke test: fetch the page and check for runtime errors
SMOKE=$(curl -s http://localhost:$PORT/ | head -50)
if ! echo "$SMOKE" | grep -q '<div id="root"'; then
echo "ERROR: Dashboard failed to load. Check the build output."
fi
# Notify the app to reload the dashboard iframe
curl -sk -X POST https://localhost:$VESTAD_PORT/agents/$AGENT_NAME/services/dashboard/invalidate -H "X-Agent-Token: $AGENT_TOKEN"
--base to vite preview. Vestad strips the /agents/{name}/{service}/ prefix when proxying, so the local server must serve at /. With --base set, /assets/... requests come in stripped and 404. The HTML uses relative ./assets/... already (vite config base: "./"), which resolves correctly under the proxy path in the browser.?v=... query. Vite's content hashes change automatically when source changes, so normally this isn't an issue. If you ever get a stuck 404 with no source change, temporarily add Date.now() to entryFileNames in vite.config.ts, rebuild, then revert.screen -ls | grep dashboardcurl -sk https://localhost:$VESTAD_PORT/agents/$AGENT_NAME/servicescd ~/agent/skills/dashboard/app && npx vite build and fix reported errors; confirm app/dist/ exists before starting preview..../services/dashboard/invalidate curl from the block above (the parent app keeps the iframe until invalidated).screen -r dashboard, then detach with Ctrl+A then d. If the session is wedged, screen -S dashboard -X quit and rerun the restart line from the block above.POST .../services curl alone and inspect the body; the python3 one-liner errors on bad JSON. Verify VESTAD_PORT, AGENT_NAME, and AGENT_TOKEN.apiFetch paths, skill server down, or auth not ready yet (waitForAuth in parent-bridge.ts).