rakka-dashboard 0.2.1

Live web UI over a running rakka system — REST + WebSocket + embedded React SPA, Prometheus / OTLP exporters.
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/api";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ClusterRing } from "@/components/viz/ClusterRing";
import { ReachabilityHeatmap } from "@/components/viz/ReachabilityHeatmap";
import { Table, TBody, THead, Th, Td, Tr } from "@/components/ui/table";

export default function Cluster() {
  const { data, isLoading } = useQuery({
    queryKey: ["cluster"],
    queryFn: api.clusterState,
  });

  if (isLoading || !data) {
    return (
      <Card>
        <CardContent className="py-6 text-sm text-muted-foreground">loading…</CardContent>
      </Card>
    );
  }

  return (
    <div className="grid gap-3 md:grid-cols-2">
      <Card>
        <CardHeader>
          <CardTitle>
            Topology{" "}
            <Badge variant="outline">{data.members.length} members</Badge>
          </CardTitle>
        </CardHeader>
        <CardContent>
          <ClusterRing members={data.members} />
        </CardContent>
      </Card>

      <Card>
        <CardHeader>
          <CardTitle>Gossip version</CardTitle>
        </CardHeader>
        <CardContent>
          <Table>
            <THead>
              <Tr>
                <Th>Address</Th>
                <Th>Clock</Th>
              </Tr>
            </THead>
            <TBody>
              {data.gossip_version.length === 0 && (
                <Tr>
                  <Td colSpan={2} className="py-4 text-center text-muted-foreground">
                    no gossip version data
                  </Td>
                </Tr>
              )}
              {data.gossip_version.map(([addr, v]) => (
                <Tr key={addr}>
                  <Td className="font-mono text-xs">{addr}</Td>
                  <Td className="tabular-nums">{v}</Td>
                </Tr>
              ))}
            </TBody>
          </Table>
        </CardContent>
      </Card>

      <Card className="md:col-span-2">
        <CardHeader>
          <CardTitle>Members</CardTitle>
        </CardHeader>
        <CardContent>
          <Table>
            <THead>
              <Tr>
                <Th>Address</Th>
                <Th>Status</Th>
                <Th>Roles</Th>
                <Th>Reachable</Th>
                <Th>Up #</Th>
              </Tr>
            </THead>
            <TBody>
              {data.members.map((m) => (
                <Tr key={m.address}>
                  <Td className="font-mono text-xs">{m.address}</Td>
                  <Td>
                    <Badge
                      variant={
                        m.status === "Up"
                          ? "success"
                          : m.status === "Down"
                            ? "destructive"
                            : "outline"
                      }
                    >
                      {m.status}
                    </Badge>
                  </Td>
                  <Td>
                    <div className="flex flex-wrap gap-1">
                      {m.roles.map((r) => (
                        <Badge key={r} variant="outline">{r}</Badge>
                      ))}
                    </div>
                  </Td>
                  <Td>
                    <Badge variant={m.reachable ? "success" : "destructive"}>
                      {m.reachable ? "yes" : "no"}
                    </Badge>
                  </Td>
                  <Td className="tabular-nums">{m.up_number}</Td>
                </Tr>
              ))}
            </TBody>
          </Table>
        </CardContent>
      </Card>

      <Card className="md:col-span-2">
        <CardHeader>
          <CardTitle>Reachability</CardTitle>
        </CardHeader>
        <CardContent>
          <ReachabilityHeatmap records={data.reachability_records} />
        </CardContent>
      </Card>
    </div>
  );
}