Zurück zum Dashboard

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.