A personal, modular dashboard built with a Go backend and Svelte frontend. Drop in the widgets you want — adding a new one is a few files each side.
Right now, this is entirely codified as opposed to pluggable within the browser.
This was made in conjunction with an AI Agent and purely being done as a way to refresh and relearn newer frameworks in conjunction with understanding the current agent model capabilities.
| Widget | Description |
|---|---|
| Word of the Day | Daily Japanese vocabulary pulled from your WaniKani Apprentice/Guru items, with readings, meanings, and example sentences |
| Upcoming Events | Next events from Google Calendar via a service account |
| Claude AI | Streaming chat with Claude Opus 4.6, with server-side conversation history and adaptive thinking |
- Backend — Go 1.25, stdlib
net/http(no framework) - Frontend — Svelte 5 + TypeScript + Vite + Tailwind CSS
- AI — Anthropic API (Claude Opus 4.6, streaming SSE)
- Calendar — Google Calendar API (service account)
- Persistence — bbolt embedded KV (default) or Redis
git clone https://github.com/dandydeveloper/dandy-dashboard
cd dandy-dashboard
cp .env.example .env
# Edit .env — ANTHROPIC_API_KEY is required at minimumTerminal 1 — backend (with hot reload via air):
make dev-backendTerminal 2 — frontend (Vite HMR):
make dev-frontendOpen http://localhost:5173.
docker compose up --buildFrontend is served by Nginx on port 80. The backend API is internal — Nginx proxies /api/* to it.
All configuration is via environment variables (.env file locally, secrets manager in production).
| Variable | Required | Default | Description |
|---|---|---|---|
ANTHROPIC_API_KEY |
Yes | — | From console.anthropic.com |
WANIKANI_API_TOKEN |
No | — | From wanikani.com/settings/personal_access_tokens — enables WaniKani word source |
GOOGLE_CREDENTIALS_JSON |
No | — | Path to service account JSON file, or raw JSON string |
GOOGLE_CALENDAR_ID |
No | primary |
Calendar ID from Google Calendar settings |
PORT |
No | 8080 |
Backend listen port |
ALLOWED_ORIGINS |
No | http://localhost:5173 |
Comma-separated CORS origins |
DASHBOARD_KEY |
No | — | Shared secret for X-Dashboard-Key header — set this in production |
DATA_DIR |
No | ./data |
Directory for the embedded bbolt database |
STORE_URL |
No | — | Redis URL (e.g. redis://localhost:6379) — overrides bbolt |
- Create a project in Google Cloud Console
- Enable the Google Calendar API
- Create a Service Account — no IAM role needed
- Download the JSON key → set as
GOOGLE_CREDENTIALS_JSON - In Google Calendar, share your calendar with the service account email → "See all event details"
The plugin system is explicit — no magic. A few files each side.
Backend — create internal/widgets/mywidget/:
// widget.go — implement the Widget interface
func (w *Widget) Slug() string { return "mywidget" }
func (w *Widget) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /data", w.handler.Data)
}Then register in cmd/server/main.go:
registry.Register(mywidget.New(cfg))Frontend — create web/src/widgets/mywidget/:
// index.ts
export const myWidget: WidgetDescriptor = {
id: 'mywidget',
title: 'My Widget',
description: '...',
component: MyWidgetComponent,
defaultSize: 'md',
}Then add one line to web/src/widgets/registry.ts:
export const widgetRegistry = [japaneseWidget, calendarWidget, myWidget]The widget appears in the resizable grid automatically.
dandy-dashboard/
├── cmd/server/main.go # Entry point — wires config, registry, server
├── internal/
│ ├── config/ # Env-based configuration
│ ├── httputil/ # JSON response helpers
│ ├── middleware/ # Logger, recover, request ID, CORS, API key
│ ├── store/ # bbolt / Redis abstraction
│ ├── widget/ # Widget interface + registry
│ └── widgets/
│ ├── claude/ # Claude AI chat (streaming SSE, session management)
│ ├── japanese/ # Word of the day (WaniKani + Jotoba)
│ └── calendar/ # Google Calendar events
├── docker/
│ ├── backend.Dockerfile
│ ├── frontend.Dockerfile
│ └── nginx.conf # Serves static files, proxies /api/* to backend
└── web/src/
├── widgets/
│ ├── types.ts # WidgetDescriptor interface
│ ├── registry.ts # All active widgets registered here
│ ├── claude/
│ ├── japanese/
│ └── calendar/
├── components/
│ ├── DashboardGrid.svelte # Resizable 12-column grid (layout persisted to localStorage)
│ └── WidgetCard.svelte # Shared card shell
└── stores/
└── layout.ts # Grid layout state + localStorage persistence
- API endpoints are optionally protected by a shared
X-Dashboard-Keyheader (DASHBOARD_KEYenv var) — enable this if the dashboard is publicly reachable - CORS is restricted to
ALLOWED_ORIGINS - Request bodies are capped at 512 KB; messages at 32 KB
- Chat sessions expire after 24 hours of inactivity
- Containers run as non-root
- Raw errors from external APIs are never forwarded to the client
MIT
