Files
server-26/drb-frontend/app/faq/page.tsx
T
Logan CusanoandClaude Opus 5.5 bf206334e3 fix(frontend): finish the token migration — raw grey classes -> theme tokens
UI_REDESIGN chunk 4 was never finished: 39 app/component files still used
raw Tailwind greys, kept readable in light mode only by the !important
override sheet. In dark mode text-gray-600 is ~2.3:1 on surface, and the
light override maps it to #94a3b8 (~2.6:1 on white) — unreadable in both.

Mechanical mapping (886 classes): gray-100/200 -> ink, 300/400 -> ink-2,
500/600/700 -> ink-muted, bg 950/900/800 -> page/surface/raised,
border 800 -> line, 700/600 -> line-strong, plus hover/divide/placeholder.
Alpha suffixes kept (tokens are color-mix, so /60 works). bg-gray-700,
text-white and marketing pages left alone. text-[9px] -> text-[10px].

Frontend unverified: no Node on the authoring box (#207).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:29:13 -04:00

116 lines
5.2 KiB
TypeScript

"use client";
import { useState } from "react";
import type { ReactNode } from "react";
import { Badge } from "@/components/ui/Badge";
import { LinkButton } from "@/components/ui/Button";
import { UnbuiltMarker } from "@/components/ui/UnbuiltMarker";
import { MachineOutputNotice } from "@/components/ui/MachineOutputNotice";
const FAQS: { q: string; a: ReactNode }[] = [
{
q: "What hardware do I need to run a node?",
a: "A node is a small field SDR device running our edge-node software — it needs an SDR dongle capable of receiving your local P25 or analog trunked system, and a network connection to reach your DRB account. Full setup instructions are provided once you add a node.",
},
{
q: "What's the difference between a 'call' and an 'incident'?",
a: "A call is a single radio transmission. An incident is the thing you actually care about — a pursuit, a fire, an accident — built by correlating related calls together, sometimes across multiple talkgroups or nodes. Incidents are the primary view; calls are the evidence behind them.",
},
{
q: "Does DRB do the transcription and AI work itself, or is that a separate cost?",
a: (
<>
Transcription and incident correlation run automatically on every recorded call and are
included — they are not billed as an add-on.
{/* Gate A / A2 (server-26#46) — qualified on the same screen as the claim. */}
<MachineOutputNotice className="mt-3 not-italic" />
</>
),
},
{
q: "Can I listen to live radio traffic without opening the dashboard?",
a: "Yes — the Discord bot can join a voice channel and relay live audio from any of your nodes, so your team can listen without a separate scanner app.",
},
{
q: "How does node ownership and team access work?",
a: "Admins have full access. Operators are scoped to a specific list of nodes they own — they see and manage only those. Viewers get read-only access to everything the org exposes. You manage all of this from Settings → Members.",
},
{
q: "What happens if I go over my plan's node or seat limit?",
a: "You'll see a plan-limit notice in Settings → Billing before anything is blocked. In this demo build there's no live enforcement wired up yet — see the Billing settings page for what's stubbed vs. real.",
},
{
// Gate A / A1 (server-26#46): plan-tiered retention windows are an unbuilt
// entitlement — there is no TTL and no deletion sweep anywhere in the
// product (server-26#44). The claim is marked unbuilt inline, on this
// screen, rather than quietly dropped.
q: "How long is call and incident history kept?",
a: (
<>
<UnbuiltMarker>Retention limits — not yet available</UnbuiltMarker>
<p className="mt-2">
Today nothing is deleted automatically: calls, recordings and incidents stay searchable and
linked to their incidents for as long as your account is open. Per-plan retention windows and
automatic deletion are not built yet, so we make no commitment about how long anything is kept
or when it goes away. If you need data removed, ask us and we will remove it by hand.
</p>
</>
),
},
{
q: "Is DMR supported?",
a: "Not yet — DMR is on the roadmap but the current release only decodes P25 and analog trunked systems.",
},
];
function ChevronIcon({ open }: { open: boolean }) {
return (
<svg
width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
strokeLinecap="round" strokeLinejoin="round"
className={`text-ink-muted shrink-0 transition-transform ${open ? "rotate-180" : ""}`}
>
<polyline points="6 9 12 15 18 9" />
</svg>
);
}
export default function FaqPage() {
const [openIndex, setOpenIndex] = useState<number | null>(0);
return (
<div className="max-w-screen-md mx-auto px-4 md:px-6 py-16 md:py-20">
<div className="text-center">
<Badge tone="brand">FAQ</Badge>
<h1 className="text-display-sm md:text-display text-white mt-5">Frequently asked questions</h1>
<p className="text-ink-2 mt-4">Can&apos;t find what you&apos;re looking for? Sign in and reach out from your account.</p>
</div>
<div className="mt-12 divide-y divide-line border-t border-b border-line">
{FAQS.map((item, i) => {
const open = openIndex === i;
return (
<div key={i}>
<button
onClick={() => setOpenIndex(open ? null : i)}
className="w-full flex items-center justify-between gap-4 py-5 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 rounded-lg"
aria-expanded={open}
>
<span className="text-white font-semibold text-sm md:text-base">{item.q}</span>
<ChevronIcon open={open} />
</button>
{open && (
<div className="text-ink-2 text-sm leading-relaxed pb-5 pr-8 animate-fade-in">{item.a}</div>
)}
</div>
);
})}
</div>
<div className="text-center mt-16">
<LinkButton href="/login" size="lg">Get started</LinkButton>
</div>
</div>
);
}