Entwickler-Doku
Wie du eigene Widgets (Plugins) baust und wie die API funktioniert. Es gilt eine Regel über allem: Daten fließen nur in eine Richtung.
Die goldene Regel
Daten fließen ausschließlich in eine Richtung. /api/ingest ist der einzige Schreibpfad; jedes Widget und jede Read-Route ist read-only und schreibt nie in die DB.
Hooks → /api/ingest → SQLite → UI
Ein Widget (Plugin) erstellen
Das Dashboard ist ein Plugin-Grid. Jedes Widget lebt unter src/plugins/<id>/ und wird in src/plugins/registry.ts registriert. Drei Schritte:
1. Die Komponente — src/plugins/my-widget/widget.tsx
Eine React-Komponente, in <Panel> gewickelt. Lies Daten read-only über eine API-Route und aktualisiere bei jedem Live-Tick.
"use client";
import { useEffect, useState } from "react";
import { Sparkles } from "lucide-react";
import { Panel } from "@/components/panel";
import { WidgetState } from "@/components/widget-state";
import { useLive } from "@/components/live-provider";
import { useT } from "@/lib/i18n";
export function MyWidget() {
const { t } = useT();
const { tick } = useLive(); // re-fetch on each live event
const [value, setValue] = useState<number | null>(null);
useEffect(() => {
fetch("/api/my-widget")
.then((r) => r.json())
.then((d) => setValue(d.value))
.catch(() => {});
}, [tick]);
return (
<Panel title={t("myWidget.title")} icon={<Sparkles className="h-4 w-4 text-accent" />} info={t("myWidget.info")}>
{value === null ? <WidgetState title={t("common.loading")} loading /> : <div className="text-3xl font-mono">{value}</div>}
</Panel>
);
}2. (Optional) Eine Read-Route — src/app/api/my-widget/route.ts
Nur lesen — niemals schreiben. Bei fehlenden Daten leer zurückgeben (nie 5xx).
import { NextResponse } from "next/server";
import { getDb } from "@/lib/db";
export const runtime = "nodejs";
export async function GET() {
const db = getDb();
const row = db.prepare("SELECT COUNT(*) AS value FROM events").get() as { value: number };
return NextResponse.json({ value: row.value });
}3. Registrieren — src/plugins/registry.ts
{
id: "my-widget",
title: "My widget",
span: "lg:col-span-3", // width on the 6-col grid
height: H, // shared height constant
icon: Sparkles,
category: "tools", // overview | sessions | activity | tools | economy | quality
description: "myWidget.info",
// optional per-widget settings (rendered in the settings drawer,
// persisted via /api/plugins/config):
settings: [{ key: "limit", type: "number", label: "myWidget.limit", default: 50, min: 10, max: 300 }],
component: MyWidget,
}Konventionen
- Alle UI-Texte nach src/lib/i18n.tsx — immer DE und EN.
- Icon-only-Buttons brauchen aria-label; Dialoge schließen mit Esc.
- Golden Rule einhalten: kein Widget schreibt jemals in die DB.
- lint, test und build müssen vor einem PR grün sein.
Drittanbieter-Plugins
Externe Widgets kommen nach plugins.local/<name>/plugin.tsx und exportieren `export const plugin: Widget`. Sie werden beim Build automatisch gescannt und ins Grid gemischt — kein Eingriff in den Kerncode nötig.
Wie die API funktioniert
Schreiben: /api/ingest (der einzige Schreibpfad)
Wird ausschließlich von Claude-Code-Hooks gefüttert. POST mit Header X-Hook-Event und JSON-Body mit session_id. Der Server lauscht nur auf 127.0.0.1; optional erzwingt MC_HOOK_TOKEN ein geteiltes Geheimnis (Header X-Hook-Token).
curl -X POST http://127.0.0.1:3000/api/ingest \
-H "X-Hook-Event: PostToolUse" \
-H "Content-Type: application/json" \
-d '{"session_id":"abc","cwd":"/repo","tool_name":"Read","tool_input":{"file_path":"/x.ts"},"tool_response":{"ok":true}}'Event-Typen: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, SessionEnd, Notification, PreCompact. Unbekannte Felder werden toleriert; fehlt session_id, wird der Event ignoriert.
Lesen: Read-Routes (read-only)
Jedes Widget liest aus einer Route. Alle sind read-only und liefern bei fehlenden Daten leere Ergebnisse (nie 5xx). Eine Auswahl:
/api/health /api/sessions /api/events /api/stats /api/graph /api/usage /api/budget /api/tools /api/tools/latency /api/errors /api/reliability /api/files /api/sankey /api/tags /api/token-burn /api/mcp /api/prompts /api/subagents /api/compactions /api/streak /api/velocity /api/calendar /api/anomaly /api/git/branches /api/session-duration /api/usage/projects /api/usage/sessions
Live: SSE
/api/stream ist ein Server-Sent-Events-Strom. Der LiveProvider abonniert ihn; jeder Event erhöht einen tick, woraufhin Widgets neu laden — so wird das Dashboard in Echtzeit aktualisiert.
Maschinen-lesbar
/api/openapi liefert die vollständige OpenAPI-3.1-Spezifikation, /api/metrics einen Prometheus-Scrape, /api/export CSV/JSON und /api/digest einen HTML-Report.