rakka-dashboard 0.2.1

Live web UI over a running rakka system — REST + WebSocket + embedded React SPA, Prometheus / OTLP exporters.
import { useMemo, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { api, type DeadLetterRecord } from "@/lib/api";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Table, THead, TBody, Th, Tr, Td } from "@/components/ui/table";
import { formatRelative } from "@/lib/utils";
import { useEventsStore } from "@/store/events";
import type { TelemetryEvent } from "@/lib/ws";

function isDeadLetter(e: TelemetryEvent): e is TelemetryEvent & { kind: "dead_letter" } {
  return e.kind === "dead_letter";
}

export default function DeadLetters() {
  const [filter, setFilter] = useState("");
  const [follow, setFollow] = useState(true);

  const { data = [], isLoading } = useQuery({
    queryKey: ["dead-letters"],
    queryFn: () => api.deadLetters(200),
  });

  const live = useEventsStore((s) =>
    s.events.filter(isDeadLetter).map((e) => e as unknown as DeadLetterRecord),
  );

  const merged = useMemo(() => {
    if (!follow) return data;
    const seen = new Set<number>();
    const combined: DeadLetterRecord[] = [];
    for (const r of live.slice().reverse()) {
      if (!seen.has(r.seq)) {
        seen.add(r.seq);
        combined.push(r);
      }
    }
    for (const r of data) {
      if (!seen.has(r.seq)) {
        seen.add(r.seq);
        combined.push(r);
      }
    }
    return combined;
  }, [data, live, follow]);

  const filtered = useMemo(() => {
    const q = filter.trim().toLowerCase();
    if (!q) return merged;
    return merged.filter((r) =>
      [r.recipient, r.sender ?? "", r.message_type, r.message_preview]
        .join(" ")
        .toLowerCase()
        .includes(q),
    );
  }, [merged, filter]);

  return (
    <Card>
      <CardHeader className="gap-2">
        <div className="flex items-center justify-between">
          <CardTitle>
            Dead letters <Badge variant="outline">{filtered.length}</Badge>
          </CardTitle>
          <label className="flex items-center gap-2 text-xs text-muted-foreground">
            <input
              type="checkbox"
              checked={follow}
              onChange={(e) => setFollow(e.target.checked)}
            />
            live follow
          </label>
        </div>
        <input
          type="search"
          placeholder="filter by recipient, sender, or type…"
          value={filter}
          onChange={(e) => setFilter(e.target.value)}
          className="h-9 rounded-md border bg-background px-3 text-sm"
        />
      </CardHeader>
      <CardContent>
        <Table>
          <THead>
            <Tr>
              <Th>Seq</Th>
              <Th>Recipient</Th>
              <Th>Sender</Th>
              <Th>Type</Th>
              <Th>Preview</Th>
              <Th>When</Th>
            </Tr>
          </THead>
          <TBody>
            {isLoading && (
              <Tr>
                <Td colSpan={6} className="py-4 text-center text-muted-foreground">
                  loading…
                </Td>
              </Tr>
            )}
            {!isLoading && filtered.length === 0 && (
              <Tr>
                <Td colSpan={6} className="py-6 text-center text-muted-foreground">
                  no dead letters
                </Td>
              </Tr>
            )}
            {filtered.slice(0, 500).map((r) => (
              <Tr key={r.seq}>
                <Td className="tabular-nums text-muted-foreground">{r.seq}</Td>
                <Td className="font-mono text-xs">{r.recipient}</Td>
                <Td className="font-mono text-xs">{r.sender ?? "—"}</Td>
                <Td>
                  <Badge variant="outline">{r.message_type}</Badge>
                </Td>
                <Td className="max-w-[320px] truncate text-xs text-muted-foreground">
                  {r.message_preview}
                </Td>
                <Td className="text-xs text-muted-foreground">
                  {formatRelative(r.timestamp)}
                </Td>
              </Tr>
            ))}
          </TBody>
        </Table>
      </CardContent>
    </Card>
  );
}