import { useState } from 'react'; import { api } from '../api/client'; import { useAction } from '../lib/hooks'; import { useGateway } from '../lib/gateway'; import { useToast } from '../lib/toast'; import { num, when } from '../lib/format'; import { Banner, Button, Card, Chip, Empty, Field, Grid, PageHead, Tiles } from '../components/ui'; /* Nothing here changes anything. It re-runs the shared weighted scorer over one person's prepared pool, after Emby permission and parental-control filtering, and shows every component and evidence reason behind the order. */ interface Weighted { weight?: number; evidence?: number; } interface Profile { genres?: Record; studios?: Record; actors?: Record; directors?: Record; franchises?: Record; runtimeRanges?: Record; ageRatings?: Record; communityRatings?: Record; releasePeriods?: Record; contentTypes?: Record; } interface InspectorItem { title: string; type?: string; year?: number; runtimeMinutes?: number; genres?: string[]; baseRank?: number; baseScore?: number; affinityScore?: number; compatibilityScore?: number; compatibilityLabel?: string; preparedReason?: string; preparedEvidenceTitle?: string; eligibleRows?: string[]; exposure?: { impressions?: number; focuses?: number; selects?: number }; explanation?: { total?: number; components?: Record; reasonCodes?: string[] }; } interface InspectorResponse { poolCandidates: number; permissionEligible: number; items: InspectorItem[] | null; profile: Profile | null; actions: { action: string; title?: string; itemId?: string }[] | null; profileMeta?: { sourceEvents?: number; algorithmVersion?: string; poolBuiltAt?: string }; } const DIMENSIONS: [string, keyof Profile][] = [ ['Genre', 'genres'], ['Studio', 'studios'], ['Actor', 'actors'], ['Director', 'directors'], ['Franchise', 'franchises'], ['Runtime', 'runtimeRanges'], ['Age rating', 'ageRatings'], ['Community rating', 'communityRatings'], ['Release period', 'releasePeriods'], ['Content type', 'contentTypes'], ]; function affinities(profile: Profile | null) { if (!profile) return []; return DIMENSIONS.flatMap(([dimension, key]) => Object.entries(profile[key] ?? {}).map(([name, value]) => ({ dimension, name, weight: value.weight ?? 0, evidence: value.evidence ?? 0, })), ).sort((a, b) => Math.abs(b.weight) - Math.abs(a.weight)); } const signed = (value: number) => `${value >= 0 ? '+' : ''}${value.toFixed(3)}`; export function InspectorPage() { const { status } = useGateway(); const { wrap } = useToast(); const { busy, run } = useAction(); const [userId, setUserId] = useState(''); const [context, setContext] = useState('default'); const [minutes, setMinutes] = useState('0'); const [at, setAt] = useState(''); const [result, setResult] = useState(null); const [hint, setHint] = useState('Choose a person to inspect their recommendations.'); const [error, setError] = useState(''); const users = status?.requestUsers ?? []; const runTest = () => run('run', async () => { if (!userId) { setError('Choose a person to pressure-test.'); return; } setError(''); setHint('Running the permission check and the scorer…'); const params = new URLSearchParams({ userId, context, minutes: minutes || '0', limit: '100' }); if (at) params.set('at', new Date(at).toISOString()); const payload = await wrap(() => api.get(`/admin/api/recommendations?${params.toString()}`), ); if (payload) { setResult(payload); setHint(`Scored at ${new Date().toLocaleTimeString()}.`); } else { setHint('Pressure test failed.'); } }); const top = affinities(result?.profile ?? null).slice(0, 24); const actions = result?.actions ?? []; const items = result?.items ?? []; const meta = result?.profileMeta ?? {}; return ( <> {hint} } >
setMinutes(event.target.value)} /> setAt(event.target.value)} />
{result ? ( <> {top.length === 0 ? ( No repeated affinity evidence yet; cold-start priors apply. ) : (
{top.map((entry) => ( {entry.dimension}: {entry.name} {signed(entry.weight)} · n={num(entry.evidence)} ))}
)} {actions.length === 0 ? ( No explicit recommendation actions. ) : (
{actions.map((action, index) => ( {action.action}: {action.title || action.itemId} ))}
)}
{items.length === 0 ? ( No candidates survived this context, the explicit exclusions and the permission filter. ) : ( {items.map((item, index) => { const explanation = item.explanation ?? {}; const components = Object.entries(explanation.components ?? {}).sort( (a, b) => Math.abs(b[1]) - Math.abs(a[1]), ); const exposure = item.exposure ?? {}; const facts = [ item.type, item.year, item.runtimeMinutes ? `${item.runtimeMinutes} min` : null, ...(item.genres ?? []), ] .filter(Boolean) .join(' · '); return ( {Number(explanation.total ?? 0).toFixed(3)}} >

{item.preparedReason || 'No legacy prepared explanation'} {item.compatibilityLabel ? ` · ${item.compatibilityLabel}` : ''}

{(explanation.reasonCodes ?? []).map((code) => ( {code} ))} {components.map(([name, value]) => ( {name}={signed(value)} ))}
Pool, row and exposure detail

Base rank {num(item.baseRank ?? 0)} · base {Number(item.baseScore ?? 0).toFixed(3)} · affinity {Number(item.affinityScore ?? 0).toFixed(3)} · compatibility{' '} {Number(item.compatibilityScore ?? 0).toFixed(3)} · impressions{' '} {num(exposure.impressions ?? 0)} · focuses {num(exposure.focuses ?? 0)} · selects{' '} {num(exposure.selects ?? 0)}

{(item.eligibleRows ?? []).map((row) => ( {row} ))}
{item.preparedEvidenceTitle ? (

Prepared evidence: {item.preparedEvidenceTitle}

) : null}
); })}
)} ) : null} ); }