● Zion Boggan
repos/Sightline/frontend/src/app/page.tsx
zionboggan.com ↗
86 lines · tsx
History for this file →
1
"use client";
2
import { useEffect, useState } from "react";
3
import Link from "next/link";
4
import { api, CameraStats } from "@/lib/api";
5
 
6
export default function Command() {
7
  const [stats, setStats] = useState<CameraStats | null>(null);
8
  const [sats, setSats] = useState(0);
9
  const [err, setErr] = useState<string | null>(null);
10
 
11
  useEffect(() => {
12
    api.cameraStats().then(setStats)
13
      .catch((e) => setErr(e.status === 401 ? "Sign in to view the command dashboard." : "Load failed."));
14
    api.satellites().then((r) => setSats(r.count)).catch(() => {});
15
  }, []);
16
 
17
  const maxOp = Math.max(1, ...(stats?.by_operator || []).map((o) => o.count));
18
  const maxSt = Math.max(1, ...(stats?.by_state || []).map((s) => s.count));
19
 
20
  return (
21
    <div>
22
      <div className="row" style={{ justifyContent: "space-between" }}>
23
        <div>
24
          <h1>Command</h1>
25
          <p className="sub">Real public camera &amp; satellite aggregation - nationwide.</p>
26
        </div>
27
        <Link href="/map"><button className="primary">Open Live Map →</button></Link>
28
      </div>
29
      {err && <div className="card" style={{ borderColor: "var(--danger)", marginTop: 12 }}>{err} <Link href="/login">Sign in →</Link></div>}
30
 
31
      <div className="grid" style={{ marginTop: 16, gridTemplateColumns: "repeat(auto-fill,minmax(200px,1fr))" }}>
32
        <div className="kpi-card"><div className="lbl">Ground cameras</div><div className="val accent">{stats?.total?.toLocaleString() ?? "-"}</div></div>
33
        <div className="kpi-card"><div className="lbl">Satellite cameras</div><div className="val">{sats || stats?.satellite_cameras || "-"}</div></div>
34
        <div className="kpi-card"><div className="lbl">States covered</div><div className="val">{stats?.by_state.length ?? "-"}</div></div>
35
        <div className="kpi-card"><div className="lbl">Operators</div><div className="val">{stats?.by_operator.length ?? "-"}</div></div>
36
        <div className="kpi-card"><div className="lbl">Flock cameras</div><div className="val accent">{stats?.by_operator.find((o) => o.operator.toLowerCase().includes("flock"))?.count.toLocaleString() ?? "-"}</div></div>
37
      </div>
38
 
39
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16, marginTop: 8 }}>
40
        <div>
41
          <h2>Cameras by operator</h2>
42
          <div className="card">
43
            {(stats?.by_operator || []).slice(0, 10).map((o) => (
44
              <div key={o.operator} style={{ marginBottom: 8 }}>
45
                <div className="row" style={{ justifyContent: "space-between", fontSize: 12 }}>
46
                  <span>{o.operator}</span><span className="mono muted">{o.count.toLocaleString()}</span>
47
                </div>
48
                <div style={{ height: 6, borderRadius: 3, marginTop: 3, background: "var(--panel-3)" }}>
49
                  <div style={{ height: 6, borderRadius: 3, width: `${(o.count / maxOp) * 100}%`,
50
                                background: o.operator.toLowerCase().includes("flock") ? "var(--accent)" : "var(--accent-2)" }} />
51
                </div>
52
              </div>
53
            ))}
54
            {!stats && <div className="muted">Loading…</div>}
55
          </div>
56
        </div>
57
        <div>
58
          <h2>Top states by coverage</h2>
59
          <div className="card scroll" style={{ maxHeight: 360 }}>
60
            <table>
61
              <thead><tr><th>State</th><th>Cameras</th><th></th></tr></thead>
62
              <tbody>
63
                {(stats?.by_state || []).slice(0, 18).map((s) => (
64
                  <tr key={s.state}>
65
                    <td><Link href={`/states`}>{s.state}</Link></td>
66
                    <td className="mono">{s.count.toLocaleString()}</td>
67
                    <td style={{ width: "55%" }}>
68
                      <div style={{ height: 5, borderRadius: 3, width: `${(s.count / maxSt) * 100}%`, background: "var(--accent)" }} />
69
                    </td>
70
                  </tr>
71
                ))}
72
              </tbody>
73
            </table>
74
          </div>
75
        </div>
76
      </div>
77
 
78
      <h2>Data provenance</h2>
79
      <div className="card muted" style={{ fontSize: 12.5 }}>
80
        Ground cameras: <b className="dimtext">OpenStreetMap / DeFlock</b> (man_made=surveillance, surveillance:type=ALPR) -
81
        real public camera locations & operators. Satellite cameras: <b className="dimtext">CelesTrak</b> orbital elements,
82
        positions propagated live via SGP4. All sources public; provenance retained per record.
83
      </div>
84
    </div>
85
  );
86
}