● Zion Boggan
repos/Drophawk/frontend/app/sources/page.tsx
zionboggan.com ↗
106 lines · tsx
History for this file →
1
"use client";
2
 
3
import { motion } from "framer-motion";
4
import { AlertTriangle } from "lucide-react";
5
import { Card } from "@/components/ui/card";
6
import { Switch } from "@/components/ui/switch";
7
import { Skeleton } from "@/components/ui/skeleton";
8
import { SourceChip } from "@/components/shared/SourceChip";
9
import { RelativeTime } from "@/components/shared/RelativeTime";
10
import { useSources, useToggleSource } from "@/hooks/useSources";
11
import { staggerContainer, fadeUpItem } from "@/lib/motion";
12
import { formatLatency } from "@/lib/utils";
13
import type { SourceStatus } from "@/lib/types";
14
 
15
const STATUS_META: Record<SourceStatus, { label: string; color: string }> = {
16
  healthy: { label: "Healthy", color: "var(--color-accent-green)" },
17
  degraded: { label: "Degraded", color: "var(--color-accent-amber)" },
18
  down: { label: "Down", color: "var(--color-accent-red)" },
19
  disabled: { label: "Disabled", color: "var(--color-fg-disabled)" },
20
};
21
 
22
export default function SourcesPage() {
23
  const { data, isLoading } = useSources();
24
  const toggle = useToggleSource();
25
 
26
  return (
27
    <div className="mx-auto max-w-[1200px] space-y-5">
28
      <div>
29
        <h2 className="text-lg font-semibold tracking-[var(--tracking-tight)]">Sources</h2>
30
        <p className="mt-0.5 text-sm text-[var(--color-fg-tertiary)]">
31
          Every place DropHawk watches. Toggle a source to pause or resume polling.
32
        </p>
33
      </div>
34
 
35
      {isLoading || !data ? (
36
        <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
37
          {Array.from({ length: 6 }).map((_, i) => (
38
            <Skeleton key={i} className="h-[150px] rounded-[var(--radius-lg)]" />
39
          ))}
40
        </div>
41
      ) : (
42
        <motion.div
43
          variants={staggerContainer}
44
          initial="initial"
45
          animate="animate"
46
          className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"
47
        >
48
          {data.map((s) => {
49
            const meta = STATUS_META[s.status];
50
            return (
51
              <motion.div key={s.id} variants={fadeUpItem}>
52
                <Card className="flex h-full flex-col gap-3 p-4">
53
                  <div className="flex items-start gap-3">
54
                    <SourceChip id={s.id} size={32} />
55
                    <div className="min-w-0 flex-1">
56
                      <p className="truncate text-sm font-semibold text-[var(--color-fg-primary)]">{s.name}</p>
57
                      <span
58
                        className="mt-1 inline-flex items-center gap-1.5 text-xs font-medium"
59
                        style={{ color: meta.color }}
60
                      >
61
                        <span className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: meta.color }} aria-hidden />
62
                        {meta.label}
63
                      </span>
64
                    </div>
65
                    <Switch
66
                      checked={s.enabled}
67
                      onCheckedChange={(enabled) => toggle.mutate({ id: s.id, enabled })}
68
                      aria-label={`${s.enabled ? "Disable" : "Enable"} ${s.name}`}
69
                    />
70
                  </div>
71
 
72
                  <dl className="grid grid-cols-3 gap-2 border-t border-[var(--color-border-subtle)] pt-3 text-center">
73
                    <Metric label="p50" value={formatLatency(s.latency_ms_p50)} />
74
                    <Metric label="p95" value={formatLatency(s.latency_ms_p95)} />
75
                    <Metric label="24h" value={s.events_seen_24h.toLocaleString()} />
76
                  </dl>
77
 
78
                  <div className="flex items-center justify-between text-xs text-[var(--color-fg-tertiary)]">
79
                    <span>Every {s.poll_interval_s}s</span>
80
                    {s.last_success_at ? <RelativeTime iso={s.last_success_at} /> : <span>never</span>}
81
                  </div>
82
 
83
                  {s.last_error ? (
84
                    <p className="flex items-start gap-1.5 rounded-[var(--radius-sm)] bg-[color-mix(in_oklch,var(--color-accent-red)_10%,transparent)] p-2 text-xs text-[var(--color-accent-red)]">
85
                      <AlertTriangle className="mt-0.5 h-3 w-3 shrink-0" aria-hidden />
86
                      <span className="truncate">{s.last_error}</span>
87
                    </p>
88
                  ) : null}
89
                </Card>
90
              </motion.div>
91
            );
92
          })}
93
        </motion.div>
94
      )}
95
    </div>
96
  );
97
}
98
 
99
function Metric({ label, value }: { label: string; value: string }) {
100
  return (
101
    <div>
102
      <dd className="font-mono text-sm tabular text-[var(--color-fg-primary)]">{value}</dd>
103
      <dt className="text-[10px] uppercase tracking-wider text-[var(--color-fg-tertiary)]">{label}</dt>
104
    </div>
105
  );
106
}