Files
jellyfin/web/AccountsSection.tsx
T
pastilhasandClaude Opus 5 e8aa85a35b settings: show who exists on each side, and which pairs are the same person
Provisioning was invisible. Connecting a server can create accounts, adopt accounts and leave
accounts alone, and the only record of which happened was a JSON field in a response nobody reads and
rows in a table nobody can see. "Did it work" could only be answered by opening Jellyfin and counting.

The nav entry is Settings rather than Connection, since it is now more than the connection.

The join is computed in the sidecar and shares `normalize` with the reconcile planner, so the screen
cannot disagree with the matching rule about whether Diogo and diogo are one person — a settings page
saying two would be lying about what the next connection will do. It joins by the link ROW first and
by folded name only where none exists: a recorded pairing is a fact, a matching name is a candidate.

Four states, because they mean different things — linked (adopted or created), names match but not
recorded yet, Officer account with no Jellyfin one, and a Jellyfin account with no Officer user. The
last is never touched, and a linked pairing whose Jellyfin account has vanished is shown as such
rather than silently re-matched by name.

"Not known" is kept distinct from "none": if the platform has not sent the account list, or the
server cannot be reached, it says so instead of rendering an empty table that would read as a fact
about the server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 22:01:43 +00:00

125 lines
5.8 KiB
TypeScript

import type { AccountPair } from './useJellyfinData';
import { ArrowLeftRight, CircleSlash, Loader2, UserPlus, Users } from 'lucide-react';
import { useJellyfinAccounts } from './useJellyfinData';
// Who exists on each side, shown side by side.
//
// The reason this is worth a screen: provisioning is invisible. Connecting a server can create accounts,
// adopt accounts, and leave accounts alone, and until now the only record of which happened was a JSON
// field in a response nobody reads and rows in a table nobody can see. An owner asking "did it work" had
// no way to find out except to open Jellyfin and count.
//
// Four states, and each one means something different:
//
// linked a row in `jellyfin_accounts` says these are the same person
// matchable the names fold to the same thing but nothing is recorded — what connecting WOULD adopt
// Officer only no Jellyfin account, and no name that matches one — what connecting WOULD create
// Jellyfin only somebody with no Officer account. Never touched, and that is deliberate: an absence
// in Officer is indistinguishable from a list that has not been told about them yet.
const TONE = {
linked: 'text-emerald-600',
matchable: 'text-amber-600',
officer: 'text-muted-foreground',
jellyfin: 'text-muted-foreground',
} as const;
const stateOf = (p: AccountPair) => {
if (p.linked && p.jellyfin) return { key: 'linked' as const, label: p.adopted ? 'linked (adopted)' : 'linked' };
if (p.linked) return { key: 'matchable' as const, label: 'linked, but the Jellyfin account is gone' };
if (p.matchable) return { key: 'matchable' as const, label: 'names match — not linked yet' };
if (p.officer) return { key: 'officer' as const, label: 'no Jellyfin account' };
return { key: 'jellyfin' as const, label: 'not an Officer account' };
};
const Cell = ({ name, sub, muted }: { name: string | null; sub?: string | null; muted?: boolean }) =>
name ? (
<div className="min-w-0">
<div className="truncate text-sm text-foreground">{name}</div>
{sub ? <div className="truncate text-[11px] text-muted-foreground">{sub}</div> : null}
</div>
) : (
<div className={`text-sm ${muted ? 'text-muted-foreground/40' : 'text-muted-foreground'}`}>—</div>
);
export const AccountsSection = ({ serverId }: { serverId: number | null }) => {
const { data, isLoading, error } = useJellyfinAccounts(serverId);
if (serverId === null) return null;
return (
<section className="mt-8">
<header className="flex items-center gap-2">
<Users className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium text-foreground">Accounts</h3>
{data?.known ? (
<span className="text-[11px] text-muted-foreground">
{data.counts.linked} linked · {data.counts.officerOnly} Officer only · {data.counts.jellyfinOnly} Jellyfin
only
</span>
) : null}
</header>
{isLoading ? (
<p className="mt-3 flex items-center gap-2 text-[11px] text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" /> reading both sides…
</p>
) : null}
{/* Distinct from "there are none". Not knowing means the platform has not sent the account list, or
this server could not be reached — rendering an empty table for either would state something
false about the server. */}
{error || (data && !data.known) ? (
<p className="mt-3 flex items-start gap-2 text-[11px] text-muted-foreground">
<CircleSlash className="mt-0.5 h-3 w-3 shrink-0" />
<span>{data && !data.known ? data.reason : 'could not read the accounts on this server'}</span>
</p>
) : null}
{data?.known ? (
<div className="mt-3 overflow-hidden rounded-lg border border-border">
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-3 border-b border-border bg-muted/40 px-3 py-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
<span>Officer</span>
<span className="w-4" />
<span>Jellyfin</span>
</div>
{data.pairs.map((pair) => {
const state = stateOf(pair);
return (
<div
key={`${pair.officer?.id ?? 'x'}-${pair.jellyfin?.id ?? 'x'}`}
className="grid grid-cols-[1fr_auto_1fr] items-center gap-3 border-b border-border/60 px-3 py-2 last:border-b-0"
>
<Cell
name={pair.officer?.username ?? null}
sub={pair.officer ? (pair.officer.name ?? pair.officer.role) : null}
muted
/>
<span className={`flex w-4 justify-center ${TONE[state.key]}`} title={state.label}>
{state.key === 'linked' ? (
<ArrowLeftRight className="h-3.5 w-3.5" />
) : state.key === 'matchable' ? (
<ArrowLeftRight className="h-3.5 w-3.5" />
) : (
<UserPlus className="h-3.5 w-3.5 opacity-30" />
)}
</span>
<div className="min-w-0">
<Cell name={pair.jellyfin?.name ?? null} muted />
<div className={`text-[11px] ${TONE[state.key]}`}>{state.label}</div>
</div>
</div>
);
})}
</div>
) : null}
<p className="mt-2 text-[11px] leading-relaxed text-muted-foreground">
Connecting a server links accounts whose usernames match, creates the ones Officer has and Jellyfin does not,
and never touches a Jellyfin account with no Officer user — an absence here is indistinguishable from someone
the list has not been told about yet.
</p>
</section>
);
};