Files
memby/admin-ui/src/pages/Accounts.tsx
T

113 lines
4.7 KiB
TypeScript
Raw Normal View History

2026-08-14 09:40:03 +12:00
import { Link } from 'react-router-dom';
import { useQuery } from '../lib/hooks';
import { initials, num, presence, recent, when } from '../lib/format';
import { Banner, Empty, Loading, Note, PageHead, Tag, Tiles } from '../components/ui';
import type { KnownClient } from '../api/types';
/* A directory, and only a directory. Everything you can *do* to a person lives on their own
page: this list used to render a full seventeen-control settings editor for every account
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. */
interface Account {
id: string;
username: string;
2026-08-17 11:41:36 +12:00
initials: string;
2026-08-14 09:40:03 +12:00
lastSeen: string;
devices: KnownClient[] | null;
recommendations?: { prompted?: boolean; completed?: boolean };
}
interface AccountsResponse {
accounts: Account[] | null;
}
export function AccountsPage() {
const { data, error, loading } = useQuery<AccountsResponse>('/admin/api/accounts', {
pollMs: 60_000,
});
const accounts = data?.accounts ?? [];
const devices = accounts.flatMap((account) => account.devices ?? []);
const completed = accounts.filter((account) => account.recommendations?.completed).length;
const queued = accounts.filter(
(account) => account.recommendations?.prompted && !account.recommendations?.completed,
).length;
return (
<>
<PageHead title="Memby users" intro="Who uses Memby, and the devices they are signed in on." />
<Banner message={error} />
<Note tone="info">
This is the Memby user list, not the Emby user directory. A person appears here only after
signing in to the Memby app. Removing access signs their Memby devices out and does not delete
or change their Emby account.
</Note>
{loading ? (
<Loading />
) : (
<>
<Tiles
tiles={[
{ label: 'Memby users', value: num(accounts.length), icon: 'people', tone: 'note' },
{ label: 'signed-in devices', value: num(devices.length), icon: 'tv', tone: 'info' },
{
label: 'active in the last quarter hour',
value: num(devices.filter((device) => recent(device.lastSeen)).length),
icon: 'pulse',
tone: 'ok',
},
{ label: 'recommendation setups completed', value: num(completed), icon: 'check', tone: 'ok' },
{ label: 'setup prompts queued', value: num(queued), icon: 'sparkle', tone: 'note' },
]}
/>
<section className="card flush">
{accounts.length === 0 ? (
<Empty>
No one has signed in to Memby yet. Emby-only accounts are intentionally not listed here.
</Empty>
) : (
accounts.map((account) => {
const list = account.devices ?? [];
const active = list.filter((device) => recent(device.lastSeen)).length;
const state = account.recommendations?.completed
? { label: 'personalised', tone: 'ok' as const }
: account.recommendations?.prompted
? { label: 'prompt queued', tone: 'warn' as const }
: { label: 'not invited', tone: undefined };
const seen = presence(account.lastSeen);
return (
<Link className="list-row" key={account.id} to={`/admin/accounts/${encodeURIComponent(account.id)}`}>
<span className="list-main">
2026-08-17 11:41:36 +12:00
<span className="avatar">{account.initials || initials(account.username)}</span>
2026-08-14 09:40:03 +12:00
<span>
<span className="list-title">
{account.username || 'Unnamed user'}
<span className="dot-state" data-tone={seen.tone} title={seen.label} />
</span>
<span className="list-meta">
{num(list.length)} device{list.length === 1 ? '' : 's'}
{active ? ` · ${active} active now` : ''} · last seen {when(account.lastSeen)}
</span>
</span>
</span>
<span className="list-actions">
<Tag tone={state.tone}>{state.label}</Tag>
<span className="crumb">Manage</span>
</span>
</Link>
);
})
)}
</section>
</>
)}
</>
);
}
/** The account page reads the same list to find the person it is about. */
export type { Account, AccountsResponse };