'use client' import { useEffect, useRef, useState } from 'react' import { authFetch, getApiBase } from '../../lib/auth' import { FEATURES, type FeatureAccess } from '../../lib/features' import type { Row } from './IdentityReviewPanel' type Account = { id: number; username: string; email: string | null; profile_id: number | null; last_login_at: string | null } type Preview = { accounts: Account[]; keep_id: number; recommended_id: number; revision: string; can_confirm: boolean; issues: string[] proposed: Account & { jellyfin_user_id: string; seerr_user_id: number; features: FeatureAccess; expires_at: string | null; is_blocked: boolean; auto_search_enabled: boolean } } export default function DuplicateAccountRepair({ row, onClose, onSaved }: { row: Row; onClose: () => void; onSaved: () => void }) { const dialog = useRef(null) const controller = useRef(null) const [preview, setPreview] = useState(null) const [busy, setBusy] = useState(false) const [saving, setSaving] = useState(false) const [acknowledged, setAcknowledged] = useState(false) const [error, setError] = useState('') const submit = async (confirm = false, keepId?: number) => { const abort = new AbortController() controller.current?.abort(); controller.current = abort setError(''); setAcknowledged(false) if (confirm) setSaving(true) else setBusy(true) try { const response = await authFetch(`${getApiBase()}/admin/identities/duplicates/${confirm ? 'confirm' : 'check'}`, { method: 'POST', signal: abort.signal, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: row.user.id, ...(keepId ? { keep_id: keepId } : {}), ...(confirm ? { keep_id: preview?.keep_id, revision: preview?.revision } : {}) }), }) const data = await response.json() if (!response.ok) throw new Error(typeof data.detail === 'string' ? data.detail : 'Could not review these accounts.') if (!abort.signal.aborted) { if (confirm) onSaved(); else setPreview(data) } } catch (err) { if (!abort.signal.aborted) { setError(err instanceof Error ? err.message : 'Repair failed. Preview again.'); setPreview(null) } } finally { if (!abort.signal.aborted) { setBusy(false); setSaving(false) } } } useEffect(() => { const previous = document.activeElement as HTMLElement | null const overflow = document.body.style.overflow document.body.style.overflow = 'hidden'; dialog.current?.showModal() void submit() return () => { controller.current?.abort(); document.body.style.overflow = overflow; previous?.focus() } }, []) return { event.preventDefault(); if (!saving) onClose() }}>

Repair duplicate accounts

Review the Magent accounts for {row.user.username}. This repair keeps one account linked to the verified Jellyfin identity.

{busy &&

Checking live service IDs and duplicate ownership...

} {error &&

{error}

} {!preview && !busy && } {preview &&

The recommended row already owns the Jellyfin link, or is the oldest row when neither owns it.

{preview.accounts.map((account) =>
Magent {account.id}{account.id === preview.keep_id ? ' · Keep' : ' · Consolidate'}

{account.username}

{account.email || 'No email'} · Profile {account.profile_id ?? 'None'}

Last login: {account.last_login_at ? new Date(account.last_login_at).toLocaleString() : 'Never'}

)}

Resulting account

{preview.proposed.username} · Magent {preview.keep_id} · Seerr {preview.proposed.seerr_user_id ?? 'Not verified'}

Jellyfin / Jellystat: {preview.proposed.jellyfin_user_id ?? 'Not verified'}

Email: {preview.proposed.email || 'None'} · Profile: {preview.proposed.profile_id ?? 'None'}

Access: {preview.proposed.is_blocked ? 'Blocked' : 'Not blocked'} · Expiry: {preview.proposed.expires_at ? new Date(preview.proposed.expires_at).toLocaleString() : 'None'} · Automatic search: {preview.proposed.auto_search_enabled ? 'Enabled' : 'Disabled'}

    {FEATURES.map((feature) =>
  • {feature.label}: {preview.proposed.features[feature.key] ? 'Enabled' : 'Disabled'}
  • )}

Request, issue, invitation and login activity history is retained. The selected account keeps its email and profile. Any block, earlier expiry or disabled permission on either row is preserved.

Extra Magent rows are removed from the active directory after their details are archived. Their outstanding emails are cancelled and their email subscriptions are not inherited. The kept account retains its own subscriptions where still eligible. Password reset links must be requested again.

Jellyfin, Seerr and Jellystat accounts and media are unchanged. This action does not merge different Jellyfin identities or delete upstream users.

{preview.issues.length > 0 &&
    {preview.issues.map((issue) =>
  • {issue}
  • )}
}
}
}