This commit is contained in:
ponzischeme89
2026-08-18 08:41:48 +12:00
parent 1da91e40a1
commit 36d171e51b
50 changed files with 4972 additions and 377 deletions
+39 -1
View File
@@ -1,6 +1,6 @@
import { Link } from 'react-router-dom';
import { useQuery } from '../lib/hooks';
import { initials, num, presence, recent, when } from '../lib/format';
import { initials, num, presence, recent, watchTime, when } from '../lib/format';
import { Banner, Empty, Loading, Note, PageHead, Tag, Tiles } from '../components/ui';
import type { KnownClient } from '../api/types';
@@ -9,6 +9,21 @@ import type { KnownClient } from '../api/types';
at once, which meant the page grew with the household and an operator scrolled past four
other people's preferences to reach the one they came for. */
/* Watch time comes from Tracearr, and `matched` is the field that matters: a household
running no Tracearr, and a person Tracearr has never seen, both arrive as zeroes. Drawing
those as "0 min this week" would have an operator asking why somebody stopped watching
when the real answer is that nothing was asked. */
interface WatchTime {
matched: boolean;
tracearrUsername?: string;
weekMs: number;
monthMs: number;
totalMs: number;
weekSessions: number;
monthSessions: number;
lastWatchedAt?: string;
}
interface Account {
id: string;
username: string;
@@ -16,6 +31,7 @@ interface Account {
lastSeen: string;
devices: KnownClient[] | null;
recommendations?: { prompted?: boolean; completed?: boolean };
watchTime?: WatchTime;
}
interface AccountsResponse {
@@ -32,6 +48,10 @@ export function AccountsPage() {
const queued = accounts.filter(
(account) => account.recommendations?.prompted && !account.recommendations?.completed,
).length;
/* Summed from the same rows the list draws, so the tile and the column underneath it can
never disagree — a separate total query is how those two come apart. */
const tracked = accounts.filter((account) => account.watchTime?.matched);
const weekMs = tracked.reduce((total, account) => total + (account.watchTime?.weekMs ?? 0), 0);
return (
<>
@@ -60,6 +80,16 @@ export function AccountsPage() {
},
{ label: 'recommendation setups completed', value: num(completed), icon: 'check', tone: 'ok' },
{ label: 'setup prompts queued', value: num(queued), icon: 'sparkle', tone: 'note' },
...(tracked.length
? [
{
label: 'watched by the household this week',
value: watchTime(weekMs),
icon: 'pulse' as const,
tone: 'data' as const,
},
]
: []),
]}
/>
@@ -78,6 +108,7 @@ export function AccountsPage() {
? { label: 'prompt queued', tone: 'warn' as const }
: { label: 'not invited', tone: undefined };
const seen = presence(account.lastSeen);
const watched = account.watchTime;
return (
<Link className="list-row" key={account.id} to={`/admin/accounts/${encodeURIComponent(account.id)}`}>
<span className="list-main">
@@ -90,10 +121,17 @@ export function AccountsPage() {
<span className="list-meta">
{num(list.length)} device{list.length === 1 ? '' : 's'}
{active ? ` · ${active} active now` : ''} · last seen {when(account.lastSeen)}
{/* The month sits beside the week because a quiet week only means
something next to the month around it. Both are omitted rather
than zeroed for somebody Tracearr has never seen. */}
{watched?.matched
? ` · watched ${watchTime(watched.weekMs)} this week, ${watchTime(watched.monthMs)} this month`
: ''}
</span>
</span>
</span>
<span className="list-actions">
{watched?.matched ? <Tag tone="data">{watchTime(watched.weekMs)}</Tag> : null}
<Tag tone={state.tone}>{state.label}</Tag>
<span className="crumb">Manage</span>
</span>