P3: routes (selected, fallback, queried)

Backend
- api/routes.rs models the Babel-style route shape; metric uses an
  untagged enum to round-trip both numeric hop counts and the
  literal "infinite" string the daemon emits for poisoned routes
- routes_snapshot() runs the three GETs concurrently with try_join
  so the snapshot is internally consistent
- poller spawns a second 5s loop emitting routes://updated; both
  loops are owned by the Poller and aborted together on stop_daemon

Frontend
- routes store mirrors the snapshot shape; tabbed view (radix-vue)
  with selected, fallback and queried lists
- RouteTable component shared by selected/fallback; metric column
  is colour-coded (0 green, low neutral, high yellow, infinite red)
- Queried subnets show a live `expires in 12s` countdown driven by
  a 1Hz tick ref instead of mutating the store
This commit is contained in:
syoul
2026-04-25 23:02:32 +02:00
parent c1a81a9065
commit 95e7cb4bd3
9 changed files with 382 additions and 34 deletions

View File

@@ -0,0 +1,58 @@
<script setup lang="ts">
import type { Metric, Route } from "@/lib/api";
defineProps<{
rows: Route[];
ready: boolean;
}>();
function metricLabel(m: Metric): string {
return typeof m === "number" ? String(m) : m;
}
function metricClass(m: Metric): string {
if (typeof m === "number") {
if (m === 0) return "text-emerald-500";
if (m < 10) return "text-foreground";
return "text-yellow-500";
}
return "text-destructive";
}
function rowKey(r: Route): string {
return `${r.subnet}|${r.nextHop}|${r.seqno}`;
}
</script>
<template>
<div class="overflow-hidden rounded-lg border border-border">
<table class="w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase text-muted-foreground">
<tr>
<th class="px-4 py-2 text-left font-medium">Subnet</th>
<th class="px-4 py-2 text-left font-medium">Next hop</th>
<th class="px-4 py-2 text-right font-medium">Metric</th>
<th class="px-4 py-2 text-right font-medium">Seqno</th>
</tr>
</thead>
<tbody class="divide-y divide-border">
<tr v-if="!rows.length">
<td colspan="4" class="px-4 py-8 text-center text-muted-foreground">
{{ ready ? "No routes." : "Daemon offline." }}
</td>
</tr>
<tr v-for="r in rows" :key="rowKey(r)" class="hover:bg-muted/30">
<td class="px-4 py-2 font-mono text-xs break-all">{{ r.subnet }}</td>
<td class="px-4 py-2 font-mono text-xs break-all">{{ r.nextHop }}</td>
<td
class="px-4 py-2 text-right font-mono text-xs"
:class="metricClass(r.metric)"
>
{{ metricLabel(r.metric) }}
</td>
<td class="px-4 py-2 text-right font-mono text-xs">{{ r.seqno }}</td>
</tr>
</tbody>
</table>
</div>
</template>