Files
silo-server/web/src/pages/AdminDevices.tsx
T

1663 lines
59 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { Link, useParams, useSearchParams } from "react-router";
import {
Activity,
AlertTriangle,
ArrowUpRight,
ChevronRight,
Clock,
Search,
Sparkles,
Subtitles,
X,
} from "lucide-react";
import {
type AdminDeviceSetting,
useAdminDeviceDetail,
useAdminDevices,
useDeleteAdminUserDeviceSetting,
useDeleteAllAdminUserDeviceSettingsForDevice,
useUpdateAdminUserDeviceSetting,
} from "@/hooks/queries/admin/users";
import type { AdminDeviceSummary } from "@/api/types";
import { ConfirmDialog } from "@/components/ConfirmDialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Skeleton } from "@/components/ui/skeleton";
import {
DeviceProfileTabs,
PlatformTile,
UNKNOWN_PROFILE_ID,
classifyPlatform,
formatRelative,
platformKindLabel,
platformLabel,
type DeviceProfileTabEntry,
type PlatformKind,
} from "@/components/admin/deviceOverrides";
import { ALL_DEVICE_SETTING_KEYS } from "@/lib/settingsManifest";
import { AdminSubtitleAppearanceDialog } from "@/components/admin/AdminSubtitleAppearanceDialog";
import { cn } from "@/lib/utils";
// ─────────────────────────────────────────────────────────────────────
// types & constants
// ─────────────────────────────────────────────────────────────────────
type OverrideRange = "all" | "none" | "1-2" | "3-5" | "6+";
type RecencyBucket = "all" | "<24h" | "<7d" | "<30d" | ">30d";
type GroupBy = "user" | "platform" | "activity";
type SavedView = "anomalies" | "recent" | "hdr" | "subtitle" | "dormant";
const DAY_MS = 86_400_000;
const PLATFORM_ORDER: PlatformKind[] = ["tv", "mobile", "tablet", "desktop", "unknown"];
const OVERRIDE_RANGES: Exclude<OverrideRange, "all">[] = ["none", "1-2", "3-5", "6+"];
const RECENCY_BUCKETS: Exclude<RecencyBucket, "all">[] = ["<24h", "<7d", "<30d", ">30d"];
// ─────────────────────────────────────────────────────────────────────
// helpers
// ─────────────────────────────────────────────────────────────────────
function ageInDays(iso: string | undefined): number | null {
if (!iso) return null;
const t = new Date(iso).getTime();
if (Number.isNaN(t)) return null;
return (Date.now() - t) / DAY_MS;
}
function bucketRecency(iso: string | undefined): Exclude<RecencyBucket, "all"> {
const d = ageInDays(iso);
if (d == null) return ">30d";
if (d < 1) return "<24h";
if (d < 7) return "<7d";
if (d < 30) return "<30d";
return ">30d";
}
function bucketOverrideCount(n: number): Exclude<OverrideRange, "all"> {
if (n === 0) return "none";
if (n <= 2) return "1-2";
if (n <= 5) return "3-5";
return "6+";
}
/**
* Flag devices whose override pattern looks unusual relative to their
* platform peers. We're conservative here — only data we have at the
* list level (counts, last-update timestamps) — but two real signals:
* 1. override_count is significantly higher than the platform median
* 2. device hasn't been touched in 30+ days but still has overrides
* Deeper per-key divergence detection requires the detail endpoint and
* is left for a follow-up.
*/
/**
* Reason a device was flagged anomalous. Carries the actual numbers so the
* detail pane can show *why* — "6 overrides · 2× iOS median (3)" beats a
* vague "diverges from platform peers" every time.
*/
export interface DeviceAnomalyReason {
/** Which signals triggered the flag — both can be true. */
outlier: boolean;
stale: boolean;
/** Override count on this device. */
overrideCount: number;
/** Median override count across same-platform peers. */
platformMedian: number;
/** Platform classification used for the comparison. */
platform: PlatformKind;
/** Days since `last_updated`, rounded down. Null if unknown. */
staleDays: number | null;
/** One-line summary suitable for a 2-line readout cell. */
summary: string;
/** Longer human explanation, used for tooltips and inline reason text. */
detail: string;
}
function detectAnomalies(devices: AdminDeviceSummary[]): Map<string, DeviceAnomalyReason> {
const flagged = new Map<string, DeviceAnomalyReason>();
if (devices.length === 0) return flagged;
const byPlatform = new Map<PlatformKind, number[]>();
devices.forEach((d) => {
const k = classifyPlatform(d.device_platform);
const arr = byPlatform.get(k) ?? [];
arr.push(d.override_count ?? 0);
byPlatform.set(k, arr);
});
const medianByPlatform = new Map<PlatformKind, number>();
byPlatform.forEach((arr, k) => {
const sorted = [...arr].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
medianByPlatform.set(k, sorted[mid] ?? 0);
});
devices.forEach((d) => {
const platform = classifyPlatform(d.device_platform);
const count = d.override_count ?? 0;
const median = medianByPlatform.get(platform) ?? 0;
const outlier = count >= 6 || (median > 0 && count >= median * 2 && count >= 4);
const days = ageInDays(d.last_updated);
const stale = (days ?? 0) > 30 && count > 0;
if (!outlier && !stale) return;
const platformText = platformKindLabel(platform).toLowerCase();
const summaryParts: string[] = [];
const detailParts: string[] = [];
if (outlier) {
const ratio = median > 0 ? Math.round((count / median) * 10) / 10 : null;
summaryParts.push(
ratio != null && ratio >= 1.2
? `${count} overrides · ${ratio}× ${platformText} median`
: `${count} overrides`,
);
detailParts.push(
median > 0
? `Has ${count} overrides — ${ratio}× the ${platformText} median of ${median}.`
: `Has ${count} overrides — well above the typical ${platformText} device.`,
);
}
if (stale) {
const staleDaysFloor = Math.floor(days ?? 0);
summaryParts.push(`stale ${staleDaysFloor}d`);
detailParts.push(
`Last activity ${staleDaysFloor} days ago — overrides may no longer reflect how the device is used.`,
);
}
flagged.set(d.device_id, {
outlier,
stale,
overrideCount: count,
platformMedian: median,
platform,
staleDays: stale && days != null ? Math.floor(days) : null,
summary: summaryParts.join(" · "),
detail: detailParts.join(" "),
});
});
return flagged;
}
function deviceKeyHints(device: AdminDeviceSummary): string {
// We don't have key info on the summary, so we approximate "HDR-related"
// and "Subtitle custom" using the override count + platform — good enough
// as a soft hint for saved-view filtering until we add a key facet to the
// summary endpoint.
return [device.device_platform, device.device_name].filter(Boolean).join(" ").toLowerCase();
}
// ─────────────────────────────────────────────────────────────────────
// page
// ─────────────────────────────────────────────────────────────────────
export default function AdminDevices() {
const { userId: userIdParam, deviceId: deviceIdParam } = useParams<{
userId?: string;
deviceId?: string;
}>();
const [searchParams] = useSearchParams();
const currentUserId = Number(userIdParam ?? 0);
const currentDeviceId = deviceIdParam ?? "";
const currentProfileId = searchParams.get("profile");
const { data: devices = [], isLoading } = useAdminDevices();
// filters
const [search, setSearch] = useState("");
const [platforms, setPlatforms] = useState<Set<PlatformKind>>(new Set());
const [overrideRange, setOverrideRange] = useState<Set<Exclude<OverrideRange, "all">>>(new Set());
const [recency, setRecency] = useState<Set<Exclude<RecencyBucket, "all">>>(new Set());
const [activeView, setActiveView] = useState<SavedView | null>(null);
const [groupBy, setGroupBy] = useState<GroupBy>("user");
// ⌘K focuses the global search input
const searchRef = useRef<HTMLInputElement>(null);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
e.preventDefault();
searchRef.current?.focus();
searchRef.current?.select();
}
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
// anomaly detection
const anomalies = useMemo(() => detectAnomalies(devices), [devices]);
// facet counts (computed on the full set, not the filtered set, so the
// sidebar always shows accurate "what would I get" numbers)
const platformCounts = useMemo(() => {
const m = new Map<PlatformKind, number>();
devices.forEach((d) => {
const k = classifyPlatform(d.device_platform);
m.set(k, (m.get(k) ?? 0) + 1);
});
return m;
}, [devices]);
const overrideRangeCounts = useMemo(() => {
const m = new Map<Exclude<OverrideRange, "all">, number>();
devices.forEach((d) => {
const b = bucketOverrideCount(d.override_count ?? 0);
m.set(b, (m.get(b) ?? 0) + 1);
});
return m;
}, [devices]);
const recencyCounts = useMemo(() => {
const m = new Map<Exclude<RecencyBucket, "all">, number>();
devices.forEach((d) => {
const b = bucketRecency(d.last_updated);
m.set(b, (m.get(b) ?? 0) + 1);
});
return m;
}, [devices]);
const savedViewCounts = useMemo(
() => ({
anomalies: anomalies.size,
recent: devices.filter((d) => (ageInDays(d.last_updated) ?? Infinity) < 7).length,
hdr: devices.filter((d) => /tv|appletv|tvos|shield/i.test(deviceKeyHints(d))).length,
subtitle: devices.filter((d) => (d.override_count ?? 0) >= 3).length,
dormant: devices.filter((d) => (ageInDays(d.last_updated) ?? 0) > 30).length,
}),
[anomalies, devices],
);
// filter pipeline
const filteredDevices = useMemo(() => {
const query = search.trim().toLowerCase();
return devices.filter((device) => {
// saved view
if (activeView === "anomalies" && !anomalies.has(device.device_id)) return false;
if (activeView === "recent" && (ageInDays(device.last_updated) ?? Infinity) >= 7)
return false;
if (activeView === "hdr" && !/tv|appletv|tvos|shield/i.test(deviceKeyHints(device)))
return false;
if (activeView === "subtitle" && (device.override_count ?? 0) < 3) return false;
if (activeView === "dormant" && (ageInDays(device.last_updated) ?? 0) <= 30) return false;
// platform
if (platforms.size > 0 && !platforms.has(classifyPlatform(device.device_platform)))
return false;
// override count
if (
overrideRange.size > 0 &&
!overrideRange.has(bucketOverrideCount(device.override_count ?? 0))
)
return false;
// recency
if (recency.size > 0 && !recency.has(bucketRecency(device.last_updated))) return false;
// free-text search
if (query) {
const profileText = (device.profiles ?? [])
.map((p) => `${p.profile_name} ${p.profile_id}`)
.join(" ");
const haystack = [
device.device_name,
device.device_id,
device.username,
device.email,
device.device_platform,
profileText,
]
.filter(Boolean)
.join(" ")
.toLowerCase();
if (!haystack.includes(query)) return false;
}
return true;
});
}, [devices, anomalies, activeView, platforms, overrideRange, recency, search]);
// group pivot
const groups = useMemo(() => buildGroups(filteredDevices, groupBy), [filteredDevices, groupBy]);
// header stats (computed on full set so they don't change as you filter)
const totalUsers = useMemo(() => new Set(devices.map((d) => d.user_id)).size, [devices]);
const totalProfiles = useMemo(
() => devices.reduce((sum, d) => sum + (d.profiles?.length ?? 0), 0),
[devices],
);
const totalOverrides = useMemo(
() => devices.reduce((sum, d) => sum + (d.override_count ?? 0), 0),
[devices],
);
const hasAnyFilter =
activeView !== null ||
platforms.size > 0 ||
overrideRange.size > 0 ||
recency.size > 0 ||
search.length > 0;
const clearAllFilters = () => {
setActiveView(null);
setPlatforms(new Set());
setOverrideRange(new Set());
setRecency(new Set());
setSearch("");
};
return (
<div className="mx-auto w-full max-w-[1920px] space-y-5 px-4 py-4 sm:px-6 sm:py-6 lg:px-8 xl:px-10">
{/* page header */}
<div className="page-header items-end gap-5">
<div className="space-y-3">
<h1 className="page-title text-[clamp(2rem,4vw,3rem)]">Devices</h1>
<p className="page-subtitle max-w-2xl text-sm sm:text-base">
Inspect, tune, and reset per-profile playback overrides across the fleet. Filter by
user, platform, or override pattern. Press{" "}
<kbd className="bg-surface/70 border-border/70 text-foreground/80 inline-flex items-center rounded border px-1.5 py-0.5 font-mono text-[10.5px]">
⌘K
</kbd>{" "}
to jump.
</p>
</div>
<div className="relative w-full sm:w-[340px]">
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Input
ref={searchRef}
className="bg-background/60 h-9 pr-14 pl-9 text-[13px]"
placeholder="Search devices, users, IDs, override keys…"
value={search}
onChange={(event) => setSearch(event.target.value)}
/>
{search ? (
<button
type="button"
onClick={() => setSearch("")}
className="text-muted-foreground hover:text-foreground absolute top-1/2 right-2 -translate-y-1/2 rounded p-1 transition-colors"
aria-label="Clear search"
>
<X className="h-3.5 w-3.5" />
</button>
) : (
<kbd className="border-border/70 text-muted-foreground absolute top-1/2 right-2.5 -translate-y-1/2 rounded border px-1.5 py-0.5 font-mono text-[10px]">
⌘K
</kbd>
)}
</div>
</div>
{/* fleet pulse */}
{!isLoading && devices.length > 0 && (
<FleetPulse
totalUsers={totalUsers}
totalDevices={devices.length}
totalProfiles={totalProfiles}
totalOverrides={totalOverrides}
anomalyCount={anomalies.size}
devices={devices}
groupBy={groupBy}
onGroupByChange={setGroupBy}
/>
)}
{/* main 3-pane */}
<div className="grid gap-4 lg:grid-cols-[200px_minmax(300px,360px)_minmax(0,1fr)] xl:grid-cols-[220px_minmax(320px,380px)_minmax(0,1fr)]">
{/* filter rail */}
<FilterRail
activeView={activeView}
onViewChange={setActiveView}
savedViewCounts={savedViewCounts}
platforms={platforms}
onPlatformsChange={setPlatforms}
platformCounts={platformCounts}
overrideRange={overrideRange}
onOverrideRangeChange={setOverrideRange}
overrideRangeCounts={overrideRangeCounts}
recency={recency}
onRecencyChange={setRecency}
recencyCounts={recencyCounts}
hasAnyFilter={hasAnyFilter}
onClearAll={clearAllFilters}
/>
{/* device list */}
<section className="surface-panel flex flex-col overflow-hidden rounded-xl border-0">
<div className="border-border/60 text-muted-foreground flex items-center justify-between border-b px-3 py-2.5 text-[10.5px] tracking-[0.08em] uppercase">
<span>
{filteredDevices.length}
{filteredDevices.length !== devices.length && (
<span className="text-muted-foreground/60"> of {devices.length}</span>
)}{" "}
{filteredDevices.length === 1 ? "device" : "devices"}
</span>
{hasAnyFilter && (
<button
type="button"
onClick={clearAllFilters}
className="hover:text-foreground transition-colors"
>
clear all
</button>
)}
</div>
<div className="overlay-scroll max-h-[calc(100vh-22rem)] flex-1 overflow-y-auto lg:max-h-[calc(100vh-18rem)]">
{isLoading ? (
<div className="space-y-1.5 p-2.5">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-[58px] w-full rounded-md" />
))}
</div>
) : filteredDevices.length === 0 ? (
<EmptyFleet hasDevices={devices.length > 0} onClear={clearAllFilters} />
) : (
<div className="divide-border/60 divide-y">
{groups.map((group) => (
<DeviceGroup
key={`${groupBy}:${group.key}`}
group={group}
anomalies={anomalies}
currentUserId={currentUserId}
currentDeviceId={currentDeviceId}
currentProfileId={currentProfileId}
forceOpen={hasAnyFilter || groups.length === 1}
/>
))}
</div>
)}
</div>
</section>
{/* detail */}
<section className="surface-panel overflow-hidden rounded-xl border-0">
{currentUserId > 0 && currentDeviceId ? (
<DeviceDetailPanel
key={`${currentUserId}:${currentDeviceId}`}
userId={currentUserId}
deviceId={currentDeviceId}
initialProfileId={currentProfileId}
anomaly={anomalies.get(currentDeviceId) ?? null}
/>
) : (
<EmptyDetail />
)}
</section>
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────────────
// Fleet pulse strip — stats + density histogram + group-by toggle
// ─────────────────────────────────────────────────────────────────────
function FleetPulse({
totalUsers,
totalDevices,
totalProfiles,
totalOverrides,
anomalyCount,
devices,
groupBy,
onGroupByChange,
}: {
totalUsers: number;
totalDevices: number;
totalProfiles: number;
totalOverrides: number;
anomalyCount: number;
devices: AdminDeviceSummary[];
groupBy: GroupBy;
onGroupByChange: (v: GroupBy) => void;
}) {
// sorted override counts for the histogram
const sortedCounts = useMemo(
() => [...devices].map((d) => d.override_count ?? 0).sort((a, b) => b - a),
[devices],
);
const maxCount = sortedCounts[0] ?? 0;
return (
<div className="surface-panel-subtle text-muted-foreground flex flex-wrap items-center gap-x-6 gap-y-3 rounded-xl px-4 py-3 text-[12px]">
<div className="flex flex-wrap items-baseline gap-x-4 gap-y-1">
<Stat label="users" value={totalUsers} />
<Dot />
<Stat label="devices" value={totalDevices} />
<Dot />
<Stat label="profiles" value={totalProfiles} />
<Dot />
<Stat label="overrides" value={totalOverrides} />
{anomalyCount > 0 && (
<>
<Dot />
<Stat label="anomalies" value={anomalyCount} tone="destructive" />
</>
)}
</div>
<div className="text-muted-foreground/80 flex items-center gap-2 font-mono text-[10px] tracking-[0.06em] uppercase">
<span className="hidden md:inline">density</span>
<Histogram counts={sortedCounts} max={maxCount} />
</div>
<div className="ml-auto flex items-center gap-2">
<span className="text-muted-foreground/70 text-[10px] tracking-[0.08em] uppercase">
group by
</span>
<SegmentedControl
value={groupBy}
onChange={onGroupByChange}
options={[
{ value: "user", label: "User" },
{ value: "platform", label: "Platform" },
{ value: "activity", label: "Activity" },
]}
/>
</div>
</div>
);
}
function Stat({ label, value, tone }: { label: string; value: number; tone?: "destructive" }) {
return (
<span className="inline-flex items-baseline gap-1.5">
<span
className={cn(
"font-medium tabular-nums",
tone === "destructive" ? "text-destructive" : "text-foreground",
)}
>
{value}
</span>
<span className="text-muted-foreground/70 text-[10px] tracking-[0.08em] uppercase">
{label}
</span>
</span>
);
}
function Dot() {
return <span className="text-muted-foreground/30">·</span>;
}
function Histogram({ counts, max }: { counts: number[]; max: number }) {
// 28 bars feels right for the "fleet at a glance" — if there are more
// devices we sample, fewer we just render what we have.
const SLOTS = 28;
const slots: number[] = useMemo(() => {
if (counts.length === 0) return Array.from({ length: SLOTS }, () => 0);
if (counts.length <= SLOTS) {
return [...counts, ...Array(SLOTS - counts.length).fill(0)];
}
const step = counts.length / SLOTS;
return Array.from({ length: SLOTS }, (_, i) => counts[Math.floor(i * step)] ?? 0);
}, [counts]);
const safeMax = Math.max(max, 1);
return (
<span className="flex h-6 items-end gap-[2px]" aria-hidden>
{slots.map((n, i) => {
const h = Math.max((n / safeMax) * 100, n > 0 ? 16 : 6);
const tone =
n === 0
? "bg-border/60"
: n >= safeMax * 0.6
? "bg-foreground/85"
: "bg-muted-foreground/70";
return (
<span
key={i}
className={cn("w-[3px] rounded-[1px] transition-colors", tone)}
style={{ height: `${h}%` }}
/>
);
})}
</span>
);
}
function SegmentedControl<T extends string>({
value,
onChange,
options,
}: {
value: T;
onChange: (v: T) => void;
options: { value: T; label: string }[];
}) {
return (
<div className="border-border/70 bg-background/40 inline-flex overflow-hidden rounded-md border">
{options.map((opt, i) => {
const active = value === opt.value;
return (
<button
key={opt.value}
type="button"
onClick={() => onChange(opt.value)}
className={cn(
"px-2.5 py-1 text-[11.5px] transition-colors",
i > 0 && "border-border/60 border-l",
active
? "bg-surface-hover/70 text-foreground"
: "text-muted-foreground hover:bg-surface-hover/40 hover:text-foreground",
)}
>
{opt.label}
</button>
);
})}
</div>
);
}
// ─────────────────────────────────────────────────────────────────────
// Filter rail — saved views + facets
// ─────────────────────────────────────────────────────────────────────
function FilterRail({
activeView,
onViewChange,
savedViewCounts,
platforms,
onPlatformsChange,
platformCounts,
overrideRange,
onOverrideRangeChange,
overrideRangeCounts,
recency,
onRecencyChange,
recencyCounts,
hasAnyFilter,
onClearAll,
}: {
activeView: SavedView | null;
onViewChange: (v: SavedView | null) => void;
savedViewCounts: Record<SavedView, number>;
platforms: Set<PlatformKind>;
onPlatformsChange: (next: Set<PlatformKind>) => void;
platformCounts: Map<PlatformKind, number>;
overrideRange: Set<Exclude<OverrideRange, "all">>;
onOverrideRangeChange: (next: Set<Exclude<OverrideRange, "all">>) => void;
overrideRangeCounts: Map<Exclude<OverrideRange, "all">, number>;
recency: Set<Exclude<RecencyBucket, "all">>;
onRecencyChange: (next: Set<Exclude<RecencyBucket, "all">>) => void;
recencyCounts: Map<Exclude<RecencyBucket, "all">, number>;
hasAnyFilter: boolean;
onClearAll: () => void;
}) {
const togglePlatform = (k: PlatformKind) => {
const next = new Set(platforms);
if (next.has(k)) next.delete(k);
else next.add(k);
onPlatformsChange(next);
};
const toggleRange = (b: Exclude<OverrideRange, "all">) => {
const next = new Set(overrideRange);
if (next.has(b)) next.delete(b);
else next.add(b);
onOverrideRangeChange(next);
};
const toggleRecency = (b: Exclude<RecencyBucket, "all">) => {
const next = new Set(recency);
if (next.has(b)) next.delete(b);
else next.add(b);
onRecencyChange(next);
};
// available platforms (only render facets that have at least one device)
const availablePlatforms = PLATFORM_ORDER.filter((k) => (platformCounts.get(k) ?? 0) > 0);
return (
<aside className="surface-panel hidden h-fit flex-col gap-5 overflow-hidden rounded-xl border-0 px-3 py-4 text-[12.5px] lg:flex">
<FilterGroup
label="Saved views"
action={
hasAnyFilter ? (
<button
type="button"
onClick={onClearAll}
className="text-muted-foreground hover:text-foreground italic transition-colors"
>
clear
</button>
) : null
}
>
<SavedViewRow
icon={<AlertTriangle className="h-3.5 w-3.5" />}
label="Anomalies"
count={savedViewCounts.anomalies}
active={activeView === "anomalies"}
tone="destructive"
onClick={() => onViewChange(activeView === "anomalies" ? null : "anomalies")}
/>
<SavedViewRow
icon={<Clock className="h-3.5 w-3.5" />}
label="Updated < 7d"
count={savedViewCounts.recent}
active={activeView === "recent"}
onClick={() => onViewChange(activeView === "recent" ? null : "recent")}
/>
<SavedViewRow
icon={<Sparkles className="h-3.5 w-3.5" />}
label="HDR-capable"
count={savedViewCounts.hdr}
active={activeView === "hdr"}
onClick={() => onViewChange(activeView === "hdr" ? null : "hdr")}
/>
<SavedViewRow
icon={<Subtitles className="h-3.5 w-3.5" />}
label="Heavy customizers"
count={savedViewCounts.subtitle}
active={activeView === "subtitle"}
onClick={() => onViewChange(activeView === "subtitle" ? null : "subtitle")}
/>
<SavedViewRow
icon={<Activity className="h-3.5 w-3.5" />}
label="Dormant 30d"
count={savedViewCounts.dormant}
active={activeView === "dormant"}
onClick={() => onViewChange(activeView === "dormant" ? null : "dormant")}
/>
</FilterGroup>
{availablePlatforms.length > 1 && (
<FilterGroup label="Platform">
{availablePlatforms.map((k) => (
<FacetCheckbox
key={k}
label={platformKindLabel(k)}
count={platformCounts.get(k) ?? 0}
checked={platforms.has(k)}
onToggle={() => togglePlatform(k)}
/>
))}
</FilterGroup>
)}
<FilterGroup label="Override count">
{OVERRIDE_RANGES.map((b) => (
<FacetCheckbox
key={b}
label={b === "none" ? "none" : b}
count={overrideRangeCounts.get(b) ?? 0}
checked={overrideRange.has(b)}
onToggle={() => toggleRange(b)}
/>
))}
</FilterGroup>
<FilterGroup label="Last seen">
{RECENCY_BUCKETS.map((b) => (
<FacetCheckbox
key={b}
label={b}
count={recencyCounts.get(b) ?? 0}
checked={recency.has(b)}
onToggle={() => toggleRecency(b)}
/>
))}
</FilterGroup>
</aside>
);
}
function FilterGroup({
label,
action,
children,
}: {
label: string;
action?: ReactNode;
children: ReactNode;
}) {
return (
<div>
<div className="text-muted-foreground/70 mb-2 flex items-center justify-between px-1 text-[10px] font-semibold tracking-[0.12em] uppercase">
<span>{label}</span>
{action}
</div>
<div className="space-y-0.5">{children}</div>
</div>
);
}
function SavedViewRow({
icon,
label,
count,
active,
tone,
onClick,
}: {
icon: ReactNode;
label: string;
count: number;
active: boolean;
tone?: "destructive";
onClick: () => void;
}) {
return (
<button
type="button"
onClick={onClick}
className={cn(
"group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors",
active
? "bg-surface-hover/70 text-foreground"
: "text-muted-foreground hover:bg-surface-hover/40 hover:text-foreground",
)}
>
<span
className={cn(
"shrink-0 transition-colors",
tone === "destructive"
? "text-destructive/85"
: active
? "text-foreground/90"
: "text-muted-foreground/70 group-hover:text-foreground/80",
)}
>
{icon}
</span>
<span className="flex-1 truncate">{label}</span>
<span
className={cn(
"font-mono text-[10.5px] tabular-nums",
active ? "text-foreground/80" : "text-muted-foreground/60",
)}
>
{count}
</span>
</button>
);
}
function FacetCheckbox({
label,
count,
checked,
onToggle,
}: {
label: string;
count: number;
checked: boolean;
onToggle: () => void;
}) {
return (
<label
className={cn(
"group flex cursor-pointer items-center justify-between rounded-md px-2 py-1.5 transition-colors",
checked
? "bg-surface-hover/60 text-foreground"
: "text-muted-foreground hover:bg-surface-hover/30 hover:text-foreground",
)}
>
<span className="flex items-center gap-2">
<input
type="checkbox"
className="border-border/80 bg-background/60 checked:bg-foreground checked:border-foreground checked:text-background h-3 w-3 cursor-pointer appearance-none rounded-[3px] border transition-colors checked:bg-[image:url('data:image/svg+xml;utf8,<svg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2012%2012%22%20fill=%22none%22%20stroke=%22%23141417%22%20stroke-width=%222.4%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22><polyline%20points=%222.5,6.5%205,9%209.5,3.5%22/></svg>')] checked:bg-[length:9px_9px] checked:bg-center checked:bg-no-repeat"
checked={checked}
onChange={onToggle}
/>
<span>{label}</span>
</span>
<span
className={cn(
"font-mono text-[10.5px] tabular-nums",
checked ? "text-foreground/80" : "text-muted-foreground/60",
)}
>
{count}
</span>
</label>
);
}
// ─────────────────────────────────────────────────────────────────────
// Device list — grouped, with override gauge + anomaly indicator
// ─────────────────────────────────────────────────────────────────────
interface DeviceGroupData {
key: string;
label: string;
href?: string;
devices: AdminDeviceSummary[];
meta: { devices: number; profiles: number; overrides: number; lastUpdated?: string };
anomalyCount: number;
}
function buildGroups(devices: AdminDeviceSummary[], groupBy: GroupBy): DeviceGroupData[] {
type Entry = { key: string; label: string; href?: string; list: AdminDeviceSummary[] };
const byKey = new Map<string, Entry>();
const keyFor = (d: AdminDeviceSummary): { key: string; label: string; href?: string } => {
if (groupBy === "user") {
return {
key: `u:${d.user_id}`,
label: d.username || d.email || `User ${d.user_id}`,
href: `/admin/users/${d.user_id}`,
};
}
if (groupBy === "platform") {
const k = classifyPlatform(d.device_platform);
return { key: `p:${k}`, label: platformKindLabel(k) };
}
// activity
const b = bucketRecency(d.last_updated);
const labels: Record<Exclude<RecencyBucket, "all">, string> = {
"<24h": "Last 24 hours",
"<7d": "This week",
"<30d": "This month",
">30d": "Older than 30 days",
};
return { key: `a:${b}`, label: labels[b] };
};
devices.forEach((d) => {
const { key, label, href } = keyFor(d);
const existing = byKey.get(key);
if (existing) {
existing.list.push(d);
} else {
byKey.set(key, { key, label, href, list: [d] });
}
});
const all = Array.from(byKey.values()).map<DeviceGroupData>((entry) => {
const list = entry.list;
const overrides = list.reduce((s, d) => s + (d.override_count ?? 0), 0);
const profiles = list.reduce((s, d) => s + (d.profiles?.length ?? 0), 0);
const lastUpdated = list
.map((d) => d.last_updated)
.filter(Boolean)
.sort()
.pop();
return {
key: entry.key,
label: entry.label,
href: entry.href,
devices: list,
meta: { devices: list.length, profiles, overrides, lastUpdated },
anomalyCount: 0, // filled in by caller (it has the anomaly set)
};
});
// Stable, useful ordering per pivot
if (groupBy === "user") {
all.sort((a, b) => a.label.localeCompare(b.label));
} else if (groupBy === "platform") {
const order = new Map(PLATFORM_ORDER.map((k, i) => [platformKindLabel(k), i]));
all.sort((a, b) => (order.get(a.label) ?? 99) - (order.get(b.label) ?? 99));
} else {
const order = new Map<string, number>([
["Last 24 hours", 0],
["This week", 1],
["This month", 2],
["Older than 30 days", 3],
]);
all.sort((a, b) => (order.get(a.label) ?? 99) - (order.get(b.label) ?? 99));
}
return all;
}
function DeviceGroup({
group,
anomalies,
currentUserId,
currentDeviceId,
currentProfileId,
forceOpen,
}: {
group: DeviceGroupData;
anomalies: Map<string, DeviceAnomalyReason>;
currentUserId: number;
currentDeviceId: string;
currentProfileId: string | null;
forceOpen: boolean;
}) {
const groupAnomalies = group.devices.filter((d) => anomalies.has(d.device_id)).length;
const containsActive = group.devices.some(
(d) => d.user_id === currentUserId && d.device_id === currentDeviceId,
);
// Default behavior: collapsed. We override to open when (a) the active
// device lives in this group, or (b) the parent told us to force open
// (search/filter active, or only one group). User clicks toggle this
// local state; whenever the external triggers change we resync, so a
// newly-applied filter expands everything as expected. We use the
// "adjusting state on prop change" pattern (rather than useEffect) to
// avoid cascading renders.
const externalOpen = forceOpen || containsActive;
const [open, setOpen] = useState(externalOpen);
const [prevExternal, setPrevExternal] = useState(externalOpen);
if (prevExternal !== externalOpen) {
setPrevExternal(externalOpen);
setOpen(externalOpen);
}
return (
<section>
<header className="bg-surface/60 border-border/40 sticky top-0 z-10 flex items-baseline justify-between gap-2 border-b px-3 py-2 backdrop-blur">
<button
type="button"
onClick={() => setOpen((v) => !v)}
aria-expanded={open}
aria-controls={`device-group-${group.key}`}
className="text-foreground hover:text-foreground/80 group flex min-w-0 flex-1 items-center gap-1.5 text-left transition-colors"
>
<ChevronRight
className={cn(
"text-muted-foreground/70 group-hover:text-foreground h-3 w-3 shrink-0 transition-transform",
open && "rotate-90",
)}
/>
<span className="truncate text-[12.5px] font-semibold">{group.label}</span>
</button>
<div className="flex shrink-0 items-center gap-2">
{group.href && (
<Link
to={group.href}
onClick={(e) => e.stopPropagation()}
className="text-muted-foreground/70 hover:text-foreground inline-flex items-center transition-colors"
aria-label={`Open ${group.label}`}
>
<ArrowUpRight className="h-3 w-3" />
</Link>
)}
{groupAnomalies > 0 && (
<span className="text-destructive bg-destructive/10 inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] tracking-[0.04em]">
<AlertTriangle className="h-2.5 w-2.5" />
{groupAnomalies}
</span>
)}
<span className="text-muted-foreground/70 font-mono text-[10.5px] tabular-nums">
{group.meta.devices}d · {group.meta.profiles}p · {group.meta.overrides}k
</span>
</div>
</header>
{open && (
<ul id={`device-group-${group.key}`} className="space-y-0.5 px-1.5 pb-1.5">
{group.devices.map((device) => (
<li key={`${device.user_id}:${device.device_id}`}>
<DeviceRow
device={device}
isAnomaly={anomalies.has(device.device_id)}
active={device.user_id === currentUserId && device.device_id === currentDeviceId}
activeProfileId={currentProfileId}
/>
</li>
))}
</ul>
)}
</section>
);
}
function DeviceRow({
device,
isAnomaly,
active,
activeProfileId,
}: {
device: AdminDeviceSummary;
isAnomaly: boolean;
active: boolean;
activeProfileId: string | null;
}) {
const kind = classifyPlatform(device.device_platform);
const profileSuffix = activeProfileId ? `?profile=${encodeURIComponent(activeProfileId)}` : "";
const href = `/admin/devices/${device.user_id}/${encodeURIComponent(device.device_id)}${profileSuffix}`;
const recency = bucketRecency(device.last_updated);
return (
<Link
to={href}
className={cn(
"group relative flex items-center gap-2.5 rounded-md px-2 py-1.5 transition-colors",
active ? "bg-surface-hover/70" : "hover:bg-surface-hover/40",
)}
>
<span
aria-hidden
className={cn(
"absolute top-1.5 bottom-1.5 left-0 w-[2px] rounded-r transition-opacity",
active ? "bg-foreground opacity-100" : "opacity-0",
)}
/>
<PlatformTile kind={kind} />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
<span className="text-foreground truncate text-[13px] font-medium">
{device.device_name || "Unnamed device"}
</span>
{isAnomaly && (
<span
className="bg-destructive/90 ring-destructive/20 h-1.5 w-1.5 shrink-0 rounded-full ring-2"
title="Anomaly: override pattern diverges from platform peers, or device is dormant"
aria-label="anomaly"
/>
)}
</div>
<div className="text-muted-foreground/80 mt-0.5 flex items-center gap-1.5 truncate text-[11px]">
<span className="truncate">{platformLabel(device.device_platform)}</span>
<span className="text-muted-foreground/40">·</span>
<span
className={cn(
"tabular-nums",
recency === "<24h" && "text-success/80",
recency === ">30d" && "text-muted-foreground/60",
)}
>
{formatRelative(device.last_updated)}
</span>
</div>
</div>
<OverrideGauge count={device.override_count ?? 0} active={active} />
</Link>
);
}
function OverrideGauge({ count, active }: { count: number; active: boolean }) {
const SLOTS = 5;
// Map count to slots: 0→0, 1-2→1-2, 3→3, 4-5→4, 6+→5 (full)
const filled = count === 0 ? 0 : count >= 6 ? 5 : Math.min(count, 5);
return (
<div className="flex shrink-0 items-center gap-1.5">
<span className="flex items-end gap-[2px]" aria-hidden>
{Array.from({ length: SLOTS }).map((_, i) => (
<span
key={i}
className={cn(
"h-3 w-[3px] rounded-[1px] transition-colors",
i < filled ? "bg-foreground/85" : "bg-border",
)}
/>
))}
</span>
<span
className={cn(
"min-w-[14px] text-right font-mono text-[11px] tabular-nums",
active
? "text-foreground"
: count === 0
? "text-muted-foreground/50"
: "text-muted-foreground/85",
)}
>
{count}
</span>
</div>
);
}
// ─────────────────────────────────────────────────────────────────────
// Empty states
// ─────────────────────────────────────────────────────────────────────
function EmptyFleet({ hasDevices, onClear }: { hasDevices: boolean; onClear: () => void }) {
return (
<div className="flex flex-col items-center gap-3 px-4 py-14 text-center">
<p className="text-foreground text-sm font-medium">
{hasDevices ? "No devices match your filters" : "No device overrides yet"}
</p>
<p className="text-muted-foreground max-w-xs text-[12.5px] leading-relaxed">
{hasDevices
? "Try a different search term or clear a few filters."
: "Overrides appear here as soon as a user tunes a per-device setting."}
</p>
{hasDevices && (
<Button variant="outline" size="sm" onClick={onClear}>
Clear filters
</Button>
)}
</div>
);
}
function EmptyDetail() {
return (
<div className="flex min-h-[420px] flex-col items-center justify-center gap-2 px-6 py-16 text-center">
<h2 className="text-foreground text-base font-medium">Select a device</h2>
<p className="text-muted-foreground max-w-sm text-[13px] leading-relaxed">
Pick a device from the list to inspect its per-profile overrides, tune controls, or reset
individual keys.
</p>
</div>
);
}
// ─────────────────────────────────────────────────────────────────────
// Detail panel — header with instrument readout + profile tabs
// ─────────────────────────────────────────────────────────────────────
function DeviceDetailPanel({
userId,
deviceId,
initialProfileId,
anomaly,
}: {
userId: number;
deviceId: string;
initialProfileId: string | null;
/** Reason this device was flagged, or null if no anomaly. */
anomaly: DeviceAnomalyReason | null;
}) {
const isAnomaly = anomaly !== null;
const { data, isLoading } = useAdminDeviceDetail(userId, deviceId);
const updateSetting = useUpdateAdminUserDeviceSetting();
const deleteSetting = useDeleteAdminUserDeviceSetting();
const deleteProfileOverrides = useDeleteAllAdminUserDeviceSettingsForDevice();
const [settingToReset, setSettingToReset] = useState<AdminDeviceSetting | null>(null);
const [profileToReset, setProfileToReset] = useState<{ id: string; name: string } | null>(null);
// The editor target carries `isOverride` alongside the setting so the
// editor (raw JSON or the subtitle-appearance panel) can decide whether
// to surface a "Reset" affordance. The current setting always lives in
// `setting`, even for synthesized default rows from the manifest.
const [jsonEditor, setJsonEditor] = useState<{
setting: AdminDeviceSetting;
isOverride: boolean;
} | null>(null);
const [jsonValue, setJsonValue] = useState("");
const closeJsonEditor = () => {
setJsonEditor(null);
setJsonValue("");
};
// When on, the profile tabs render every device-scoped setting from the
// manifest — not just the ones currently overridden — so the admin can
// create overrides on settings that haven't been touched yet.
const [showAllSettings, setShowAllSettings] = useState(false);
const profileTabs = useMemo<DeviceProfileTabEntry[]>(() => {
if (!data) return [];
const grouped = new Map<string, DeviceProfileTabEntry>();
for (const setting of data.settings) {
const profileId = setting.profile_id || UNKNOWN_PROFILE_ID;
const existing = grouped.get(profileId);
if (existing) {
existing.settings.push(setting);
} else {
grouped.set(profileId, {
profileId,
profileName: setting.profile_name || profileId,
settings: [setting],
});
}
}
return Array.from(grouped.values());
}, [data]);
if (isLoading) {
return (
<div className="space-y-4 p-5">
<Skeleton className="h-16 w-full rounded-md" />
<Skeleton className="h-32 w-full rounded-md" />
<Skeleton className="h-32 w-full rounded-md" />
</div>
);
}
if (!data) {
return (
<div className="flex min-h-[320px] flex-col items-center justify-center gap-1 text-center">
<div className="text-foreground text-sm font-medium">Device not found</div>
<div className="text-muted-foreground max-w-xs text-xs">
The device may have been removed, or its overrides may have been cleared.
</div>
</div>
);
}
const kind = classifyPlatform(data.device_platform);
const totalOverrides = data.settings.length;
const lastUpdate = data.settings
.map((s) => s.updated_at)
.filter(Boolean)
.sort()
.pop();
return (
<div className="flex h-full flex-col">
<ConfirmDialog
open={settingToReset !== null}
onOpenChange={(open) => {
if (!open) setSettingToReset(null);
}}
title="Reset this override?"
description="The override will be removed and the device will fall back to the profile default."
confirmLabel="Reset override"
variant="destructive"
onConfirm={() => {
if (settingToReset) {
deleteSetting.mutate({
userId: data.user_id,
profileId: settingToReset.profile_id,
deviceId: settingToReset.device_id,
key: settingToReset.key,
});
}
setSettingToReset(null);
}}
/>
<ConfirmDialog
open={profileToReset !== null}
onOpenChange={(open) => {
if (!open) setProfileToReset(null);
}}
title={profileToReset ? `Reset overrides for ${profileToReset.name}?` : "Reset profile"}
description="Every override for this profile on this device will be cleared."
confirmLabel="Reset all"
variant="destructive"
onConfirm={() => {
if (profileToReset) {
deleteProfileOverrides.mutate({
userId: data.user_id,
profileId: profileToReset.id,
deviceId: data.device_id,
});
}
setProfileToReset(null);
}}
/>
{/*
Subtitle appearance gets the same rich panel that end users see in
the player — preview, pill groups, color swatches — so admin
overrides land in exactly the shape clients consume. Other JSON
settings fall through to the raw textarea editor below.
*/}
<AdminSubtitleAppearanceDialog
setting={
jsonEditor && jsonEditor.setting.key === "subtitle_appearance" ? jsonEditor.setting : null
}
isOverride={jsonEditor?.isOverride ?? false}
onClose={closeJsonEditor}
onSave={(setting, value) =>
updateSetting.mutate({
userId: data.user_id,
profileId: setting.profile_id,
deviceId: setting.device_id,
key: setting.key,
value,
})
}
onReset={(setting) =>
deleteSetting.mutate({
userId: data.user_id,
profileId: setting.profile_id,
deviceId: setting.device_id,
key: setting.key,
})
}
saving={updateSetting.isPending}
/>
<Dialog
open={jsonEditor !== null && jsonEditor.setting.key !== "subtitle_appearance"}
onOpenChange={(open) => {
if (!open) closeJsonEditor();
}}
>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle className="font-mono text-sm">
{jsonEditor?.setting.key ?? "JSON"}
</DialogTitle>
</DialogHeader>
<div className="space-y-3">
<p className="text-muted-foreground text-[12.5px]">
Edit the raw value. Invalid JSON is saved as-is and may cause clients to fall back to
defaults.
</p>
<textarea
spellCheck={false}
className="border-border bg-background focus:border-foreground/40 min-h-[260px] w-full rounded-md border px-3 py-2 font-mono text-[13px] leading-relaxed transition-colors outline-none"
value={jsonValue}
onChange={(event) => setJsonValue(event.target.value)}
/>
<div className="flex justify-end gap-2">
<Button variant="outline" size="sm" onClick={closeJsonEditor}>
Cancel
</Button>
<Button
size="sm"
onClick={() => {
if (!jsonEditor) return;
const target = jsonEditor.setting;
updateSetting.mutate(
{
userId: data.user_id,
profileId: target.profile_id,
deviceId: target.device_id,
key: target.key,
value: jsonValue,
},
{
onSuccess: () => closeJsonEditor(),
},
);
}}
>
Save override
</Button>
</div>
</div>
</DialogContent>
</Dialog>
<header className="border-border/60 flex flex-wrap items-start justify-between gap-4 border-b px-5 py-4 sm:px-6">
<div className="flex min-w-0 items-start gap-3">
<PlatformTile kind={kind} size="lg" />
<div className="min-w-0 space-y-1">
<div className="flex flex-wrap items-center gap-2">
<h2 className="text-foreground text-lg leading-tight font-semibold sm:text-xl">
{data.device_name || "Unnamed device"}
</h2>
{isAnomaly && (
<span className="text-destructive border-destructive/30 bg-destructive/10 inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10.5px] tracking-[0.04em] uppercase">
<AlertTriangle className="h-3 w-3" />
anomaly
</span>
)}
</div>
<div className="text-muted-foreground flex flex-wrap items-center gap-x-2 gap-y-1 text-[12px]">
<span className="text-foreground/80 font-mono">{data.device_id}</span>
<span className="text-muted-foreground/40">·</span>
<span>{platformLabel(data.device_platform)}</span>
<span className="text-muted-foreground/40">·</span>
<Link
to={`/admin/users/${data.user_id}`}
className="text-foreground/85 hover:text-foreground inline-flex items-center gap-0.5 font-medium transition-colors"
>
{data.username}
<ArrowUpRight className="h-3 w-3" />
</Link>
<span className="text-muted-foreground/40">·</span>
<span>updated {formatRelative(data.last_updated)}</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setShowAllSettings((v) => !v)}
disabled={profileTabs.length === 0}
aria-pressed={showAllSettings}
className={cn(
"border-border/70 hover:bg-surface-hover/60 inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-[12px] transition-colors disabled:cursor-not-allowed disabled:opacity-40",
showAllSettings
? "bg-foreground/10 text-foreground border-foreground/30"
: "text-muted-foreground hover:text-foreground",
)}
title={
profileTabs.length === 0
? "Add an override on this device first to surface every setting"
: showAllSettings
? "Collapse to overridden settings only"
: `Show every device setting (${ALL_DEVICE_SETTING_KEYS.length})`
}
>
{showAllSettings ? (
<>Overrides only</>
) : (
<>Show all {ALL_DEVICE_SETTING_KEYS.length} settings</>
)}
</button>
<Button variant="outline" size="sm" asChild>
<Link to={`/admin/users/${data.user_id}`}>
Open user
<ArrowUpRight className="h-3 w-3" />
</Link>
</Button>
</div>
</header>
{/* instrument readout */}
<div className="border-border/60 grid grid-cols-2 gap-x-6 gap-y-3 border-b px-5 py-4 sm:grid-cols-4 sm:px-6">
<ReadoutCell
label="Overrides"
value={String(totalOverrides)}
sub={`across ${profileTabs.length} ${profileTabs.length === 1 ? "profile" : "profiles"}`}
tone="primary"
/>
<ReadoutCell
label="Profiles touched"
value={
profileTabs
.slice(0, 2)
.map((p) => p.profileName)
.join(", ") || "—"
}
sub={
profileTabs.length > 2
? `+ ${profileTabs.length - 2} more`
: profileTabs.map((p) => `${p.settings.length}`).join(" / ")
}
/>
<ReadoutCell
label="Status"
value={isAnomaly ? "Anomaly" : "Normal"}
sub={
anomaly
? anomaly.summary
: `aligned with ${platformKindLabel(kind).toLowerCase()} fleet`
}
subTitle={anomaly?.detail}
tone={isAnomaly ? "destructive" : undefined}
/>
<ReadoutCell
label="Last update"
value={formatRelative(lastUpdate || data.last_updated)}
sub={lastUpdate ? lastUpdate.split("T")[0] : "—"}
/>
</div>
{/*
flex-1 + min-h-0 lets the tabs component own its own scroll region.
DeviceProfileTabs pins the tab strip + profile metadata bar at the
top and scrolls only the override rows below — so the scrollbar
appears next to the rows, not next to the profiles strip.
*/}
<div className="flex min-h-0 flex-1 flex-col">
{totalOverrides === 0 ? (
<div className="px-5 py-5 sm:px-6">
<div className="border-border/60 rounded-md border border-dashed px-6 py-12 text-center">
<p className="text-foreground text-sm font-medium">No overrides on this device</p>
<p className="text-muted-foreground mx-auto mt-1 max-w-sm text-xs leading-relaxed">
This device has been seen but isn't overriding any per-profile playback settings.
</p>
</div>
</div>
) : (
<DeviceProfileTabs
key={initialProfileId ?? "default"}
profiles={profileTabs}
initialProfileId={initialProfileId}
showAllSettings={showAllSettings}
device={{
userId: data.user_id,
deviceId: data.device_id,
deviceName: data.device_name,
devicePlatform: data.device_platform,
}}
deviceStaleDays={anomaly?.staleDays ?? null}
onResetProfile={(profileId, profileName) =>
setProfileToReset({ id: profileId, name: profileName })
}
onEditJson={(setting, isOverride) => {
setJsonEditor({ setting, isOverride });
setJsonValue(setting.value);
}}
onResetSetting={(setting) => setSettingToReset(setting)}
onChangeSetting={(setting, value) =>
updateSetting.mutate({
userId: data.user_id,
profileId: setting.profile_id,
deviceId: setting.device_id,
key: setting.key,
value,
})
}
updatePending={updateSetting.isPending}
resetPending={deleteProfileOverrides.isPending}
/>
)}
</div>
</div>
);
}
function ReadoutCell({
label,
value,
sub,
/**
* Optional override for the sub-line tooltip. When omitted, the sub text
* is used as the title — fine for short subs, but inadequate when the
* sub is a one-line summary that has a longer detail explanation behind
* it (e.g., the Status cell on an anomaly).
*/
subTitle,
tone,
}: {
label: string;
value: string;
sub?: string;
subTitle?: string;
tone?: "primary" | "destructive";
}) {
return (
<div className="min-w-0">
<div className="text-muted-foreground/70 text-[10px] font-semibold tracking-[0.12em] uppercase">
{label}
</div>
<div
className={cn(
"mt-1 truncate font-mono text-[14px]",
tone === "primary"
? "text-foreground"
: tone === "destructive"
? "text-destructive"
: "text-foreground/90",
)}
title={value}
>
{value}
</div>
{sub && (
<div
className="text-muted-foreground/70 mt-0.5 truncate text-[11px]"
title={subTitle ?? sub}
>
{sub}
</div>
)}
</div>
);
}