// frontend\src\components\ui\RecorderSettings.tsx
'use client'

import { useEffect, useRef, useState, type Dispatch, type ReactNode, type SetStateAction } from 'react'
import Button from './Button'
import Card from './Card'
import LabeledSwitch from './LabeledSwitch'
import Task from './Task'
import TaskList from './TaskList'
import type { TaskItem } from './TaskList'
import PostgresUrlModal from './PostgresUrlModal'
import { CheckIcon, ChevronDownIcon, XMarkIcon } from '@heroicons/react/24/solid'
import { startRegistration } from '@simplewebauthn/browser'

type RecorderSettings = {
  databaseUrl?: string
  hasDbPassword?: boolean
  recordDir: string
  doneDir: string
  ffmpegPath?: string
  autoAddToDownloadList?: boolean
  autoStartAddedDownloads?: boolean
  enableConcurrentDownloadsLimit?: boolean
  maxConcurrentDownloads?: number
  useProviderApis?: boolean
  useChaturbateApi?: boolean
  useMyFreeCamsApi?: boolean
  autoDeleteSmallDownloads?: boolean
  autoDeleteSmallDownloadsBelowMB?: number
  autoDeleteSmallDownloadsKeepFavorites?: boolean
  autoDeleteLowRatedDownloads?: boolean
  autoDeleteLowRatedDownloadsMaxStars?: number
  blurPreviews?: boolean
  teaserPlayback?: 'still' | 'hover' | 'all'
  teaserAudio?: boolean
  lowDiskPauseBelowGB?: number
  enableNotifications?: boolean

  generateAssetsMeta?: boolean
  generateAssetsThumb?: boolean
  generateAssetsTeaser?: boolean
  generateAssetsSprites?: boolean
  generateAssetsAnalyze?: boolean
  enrichPostworkEnabled?: boolean

  trainingRecognitionEnabled?: boolean
  trainingDetectorEpochs?: number
}

type AuthDevice = {
  id: string
  label: string
  type?: string
  createdAt?: string
  configuredAt?: string
  lastUsedAt?: string
  active?: boolean
  signCount?: number
  backupState?: boolean
  backupEligible?: boolean
}

type AuthSecurityStatus = {
  authenticated?: boolean
  totpEnabled?: boolean
  totpConfigured?: boolean
  passkeyConfigured?: boolean
  passkeyCount?: number
  totpDevice?: AuthDevice | null
  passkeys?: AuthDevice[]
}

type AIServerStatus = {
  ok: boolean
  running: boolean
  url?: string
  statusCode?: number
  error?: string
}

type AppLog = {
  ok: boolean
  exists?: boolean
  path?: string
  sizeBytes?: number
  log?: string
  error?: string
}

type DiskStatus = {
  emergency: boolean
  pauseGB: number
  resumeGB: number
  freeBytes: number
  freeBytesHuman: string
  recordPath?: string
}

function unwrapWebAuthnOptions<T = any>(options: any): T {
  const unwrapped = options?.publicKey ?? options

  if (!unwrapped?.challenge) {
    throw new Error('Passkey Setup fehlgeschlagen: challenge fehlt in den WebAuthn-Options.')
  }

  return unwrapped as T
}

function formatSecurityDate(value?: string) {
  const raw = String(value ?? '').trim()
  if (!raw) return 'Unbekannt'

  const d = new Date(raw)
  if (!Number.isFinite(d.getTime())) return 'Unbekannt'

  return d.toLocaleString('de-DE', {
    dateStyle: 'medium',
    timeStyle: 'short',
  })
}

function statusPill(active: boolean, activeText = 'Aktiv', inactiveText = 'Aus') {
  return (
    <span
      className={[
        'shrink-0 rounded-full px-2 py-1 text-[11px] font-bold ring-1',
        active
          ? 'bg-emerald-50 text-emerald-800 ring-emerald-200 dark:bg-emerald-500/15 dark:text-emerald-100 dark:ring-emerald-400/30'
          : 'bg-gray-100 text-gray-700 ring-gray-200 dark:bg-white/10 dark:text-gray-200 dark:ring-white/10',
      ].join(' ')}
    >
      {active ? activeText : inactiveText}
    </span>
  )
}

function SecurityDeviceRow(props: {
  icon: string
  title: string
  subtitle: string
  meta?: string
  badge?: string
  actions?: ReactNode
}) {
  return (
    <div className="flex items-start gap-3 rounded-xl border border-gray-200 bg-white p-3 dark:border-white/10 dark:bg-gray-950/40">
      <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-gray-100 text-lg shadow-sm ring-1 ring-gray-200 dark:bg-white/10 dark:ring-white/10">
        {props.icon}
      </div>

      <div className="min-w-0 flex-1">
        <div className="flex flex-wrap items-center gap-2">
          <div className="truncate text-sm font-semibold text-gray-900 dark:text-white">
            {props.title}
          </div>

          {props.badge ? (
            <span className="rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-700 ring-1 ring-gray-200 dark:bg-white/10 dark:text-gray-200 dark:ring-white/10">
              {props.badge}
            </span>
          ) : null}
        </div>

        <div className="mt-0.5 text-xs text-gray-600 dark:text-gray-300">
          {props.subtitle}
        </div>

        {props.meta ? (
          <div className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
            {props.meta}
          </div>
        ) : null}
      </div>

      {props.actions ? (
        <div className="shrink-0">
          {props.actions}
        </div>
      ) : null}
    </div>
  )
}

function SettingsSection(props: {
  id: string
  title: string
  description?: string
  icon?: ReactNode
  actions?: ReactNode
  defaultOpen?: boolean
  children: ReactNode
}) {
  const storageKey = `recorder-settings:section:${props.id}:open`

  const [open, setOpen] = useState(() => {
    if (typeof window === 'undefined') return props.defaultOpen ?? true

    try {
      const raw = window.localStorage.getItem(storageKey)
      if (raw === '1') return true
      if (raw === '0') return false
    } catch {
      // ignore
    }

    return props.defaultOpen ?? true
  })

  function toggleOpen() {
    setOpen((cur) => {
      const next = !cur

      try {
        window.localStorage.setItem(storageKey, next ? '1' : '0')
      } catch {
        // ignore
      }

      return next
    })
  }

  return (
    <section className="overflow-hidden rounded-2xl bg-white shadow-md shadow-gray-200/70 ring-1 ring-gray-300/80 transition-shadow hover:shadow-lg dark:bg-gray-950/70 dark:shadow-black/30 dark:ring-white/15">
      <button
        type="button"
        onClick={toggleOpen}
        className={[
          'flex w-full items-start justify-between gap-4 p-4 text-left transition',
          'focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500',
          'dark:focus-visible:ring-indigo-400',
          open
            ? 'rounded-t-2xl rounded-b-none bg-gray-50 hover:bg-gray-100/80 dark:bg-white/5 dark:hover:bg-white/10'
            : 'rounded-2xl bg-white hover:bg-gray-50 dark:bg-gray-950/70 dark:hover:bg-white/5',
        ].join(' ')}
        aria-expanded={open}
      >
        <div className="min-w-0">
          <div className="flex items-start gap-3">
            {props.icon ? (
              <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-gray-100 text-lg dark:bg-white/10">
                {props.icon}
              </div>
            ) : null}

            <div className="min-w-0">
              <div className="text-sm font-semibold text-gray-900 dark:text-white">
                {props.title}
              </div>

              {props.description ? (
                <div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
                  {props.description}
                </div>
              ) : null}
            </div>
          </div>
        </div>

        <div className="flex shrink-0 items-center gap-2">
          {props.actions}

          <ChevronDownIcon
            className={[
              'h-5 w-5 text-gray-400 transition-transform duration-200 dark:text-gray-500',
              open ? 'rotate-180' : '',
            ].join(' ')}
          />
        </div>
      </button>

      {open ? (
        <div className="border-t border-gray-200 p-4 pt-3 dark:border-white/10">
          {props.children}
        </div>
      ) : null}
    </section>
  )
}

const DEFAULTS: RecorderSettings = {
  databaseUrl: '',
  hasDbPassword: false,
  recordDir: 'records',
  doneDir: 'records/done',
  ffmpegPath: '',
  autoAddToDownloadList: true,
  autoStartAddedDownloads: true,
  enableConcurrentDownloadsLimit: false,
  maxConcurrentDownloads: 20,
  useProviderApis: false,
  useChaturbateApi: false,
  useMyFreeCamsApi: false,
  autoDeleteSmallDownloads: true,
  autoDeleteSmallDownloadsBelowMB: 200,
  autoDeleteSmallDownloadsKeepFavorites: true,
  autoDeleteLowRatedDownloads: false,
  autoDeleteLowRatedDownloadsMaxStars: 3,
  blurPreviews: false,
  teaserPlayback: 'hover',
  teaserAudio: false,
  lowDiskPauseBelowGB: 5,
  enableNotifications: true,

  generateAssetsMeta: true,
  generateAssetsThumb: true,
  generateAssetsTeaser: true,
  generateAssetsSprites: true,
  generateAssetsAnalyze: true,
  enrichPostworkEnabled: true,

  trainingRecognitionEnabled: true,
  trainingDetectorEpochs: 60,
}

type Props = {
  onAssetsGenerated?: () => void
}

function regeneratePhaseOrder(queue?: string, phase?: string) {
  const q = String(queue || '').trim().toLowerCase()
  const p = String(phase || '').trim().toLowerCase()

  const key = `${q}:${p}`

  switch (key) {
    case 'postwork:meta':
      return 0
    case 'postwork:thumb':
      return 1
    case 'postwork:teaser':
      return 2
    case 'postwork:sprites':
      return 3
    case 'enrich:analyze':
      return 4
    default:
      return 999
  }
}

function shortTaskFilename(name?: string, max = 52) {
  const s = String(name ?? '').trim()
  if (!s) return ''
  if (s.length <= max) return s
  return '…' + s.slice(-(max - 1))
}

function regenerateTaskId(file: string) {
  return `regenerate-assets:${file}`
}

function regenerateTaskTitle(_file: string) {
  return 'Assets neu generieren'
}

function assetsPhaseLabel(label?: string, phase?: string) {
  const direct = String(label ?? '').trim()
  if (direct) return direct

  switch (String(phase ?? '').trim().toLowerCase()) {
    case 'meta':
      return 'Meta'
    case 'thumb':
      return 'Vorschaubild'
    case 'teaser':
      return 'Teaser'
    case 'sprites':
      return 'Sprites'
    case 'analyze':
      return 'Analyse'
    default:
      return ''
  }
}

function assetsTaskText(file?: string, label?: string, phase?: string) {
  const fn = shortTaskFilename(file, 38)
  const phaseText = assetsPhaseLabel(label, phase)

  if (fn && phaseText) return `${fn} · ${phaseText}`
  if (fn) return fn
  return phaseText
}

function parseTaskTimeMs(v: unknown) {
  if (typeof v === 'number' && Number.isFinite(v)) return v

  const s = String(v ?? '').trim()
  if (!s) return 0

  const ms = Date.parse(s)
  return Number.isFinite(ms) ? ms : 0
}

function assetTaskDomId(id: string) {
  return `generate-assets:${id}`
}

function checkVideosTaskDomId(id: string) {
  return `check-videos:${id}`
}

function cleanupTaskDomId(id: string) {
  return `cleanup:${id}`
}

function isCancelledError(err?: string) {
  const s = String(err ?? '').trim().toLowerCase()
  return s === 'abgebrochen' || s === 'abbruch' || s === 'cancelled' || s === 'canceled'
}

function upsertTask(
  setter: Dispatch<SetStateAction<TaskItem[]>>,
  next: TaskItem
) {
  setter((prev) => {
    const idx = prev.findIndex((t) => t.id === next.id)
    if (idx === -1) return [...prev, next]
    const copy = [...prev]
    copy[idx] = { ...copy[idx], ...next }
    return copy
  })
}

function mapAssetJobToTask(job: any): TaskItem {
  const err = String(job?.error || '').trim()
  let status: TaskItem['status'] = 'idle'

  if (job?.queued) status = 'queued'
  else if (job?.running) status = 'running'
  else if (err) status = isCancelledError(err) ? 'cancelled' : 'error'
  else if (job?.finishedAt) status = 'done'

  return {
    id: assetTaskDomId(String(job?.id || '')),
    status,
    title: 'Assets generieren',
    text:
      status === 'running'
        ? assetsTaskText(
            String(job?.currentFile || '').trim(),
            String(job?.currentLabel || '').trim(),
            String(job?.currentPhase || '').trim()
          )
        : String(job?.text || ''),
    done: Number(job?.done ?? 0),
    total: Number(job?.total ?? 0),
    err: status === 'error' ? err : undefined,
    cancellable: status === 'queued' || status === 'running',
    fading: false,
    queuedAtMs: parseTaskTimeMs(job?.queuedAt),
    startedAtMs: parseTaskTimeMs(job?.startedAt),
  }
}

function mapCheckVideosJobToTask(job: any): TaskItem {
  const err = String(job?.error || '').trim()
  let status: TaskItem['status'] = 'idle'

  if (job?.queued) status = 'queued'
  else if (job?.running) status = 'running'
  else if (err) status = isCancelledError(err) ? 'cancelled' : 'error'
  else if (job?.finishedAt) status = 'done'

  return {
    id: checkVideosTaskDomId(String(job?.id || '')),
    status,
    title: 'Downloads prüfen',
    text:
      status === 'running'
        ? (String(job?.currentFile || '').trim()
            ? shortTaskFilename(String(job?.currentFile || '').trim(), 52)
            : 'Prüfe Videos…')
        : String(job?.text || ''),
    done: Number(job?.done ?? 0),
    total: Number(job?.total ?? 0),
    err: status === 'error' ? err : undefined,
    cancellable: status === 'queued' || status === 'running',
    fading: false,
    queuedAtMs: parseTaskTimeMs(job?.queuedAt),
    startedAtMs: parseTaskTimeMs(job?.startedAt),
  }
}

function mapCleanupJobToTask(job: any): TaskItem {
  const err = String(job?.error || '').trim()
  let status: TaskItem['status'] = 'idle'

  if (job?.queued) status = 'queued'
  else if (job?.running) status = 'running'
  else if (err) status = isCancelledError(err) ? 'cancelled' : 'error'
  else if (job?.finishedAt) status = 'done'

  return {
    id: cleanupTaskDomId(String(job?.id || '')),
    status,
    title: 'Aufräumen',
    text: String(job?.text || job?.currentFile || ''),
    done: Number(job?.done ?? 0),
    total: Number(job?.total ?? 0),
    err: status === 'error' ? err : undefined,
    cancellable: status === 'queued' || status === 'running',
    fading: false,
    queuedAtMs: parseTaskTimeMs(job?.queuedAt),
    startedAtMs: parseTaskTimeMs(job?.startedAt),
  }
}

const SETTINGS_RATING_STAR_PATH =
  'M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.176 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 9.72c-.783-.57-.38-1.81.588-1.81H7.03a1 1 0 00.951-.69l1.07-3.292z'

function settingsRatingTone(stars: number) {
  switch (stars) {
    case 1:
      return 'text-yellow-300 opacity-55'
    case 2:
      return 'text-yellow-300 opacity-65'
    case 3:
      return 'text-yellow-300 opacity-80'
    case 4:
      return 'text-yellow-300 opacity-95'
    default:
      return 'text-yellow-200 opacity-100'
  }
}

function RatingThresholdStar({
  stars,
  filled,
}: {
  stars: number
  filled: boolean
}) {
  return (
    <span className="relative inline-grid h-7 w-7 shrink-0 place-items-center leading-none">
      <svg
        viewBox="0 0 20 20"
        aria-hidden="true"
        className={[
          'h-7 w-7 shrink-0 transition duration-150',
          filled
            ? `fill-current ${settingsRatingTone(stars)}`
            : 'fill-transparent text-gray-300 dark:text-white/25',
        ].join(' ')}
        style={{
          filter: filled
            ? 'drop-shadow(0 1px 2px rgba(0,0,0,0.75)) drop-shadow(0 0 3px rgba(0,0,0,0.35))'
            : 'drop-shadow(0 1px 1px rgba(0,0,0,0.22))',
        }}
      >
        <path
          d={SETTINGS_RATING_STAR_PATH}
          stroke={filled ? 'rgba(0,0,0,0.75)' : 'currentColor'}
          strokeWidth="1.1"
          paintOrder="stroke fill"
        />

        <text
          x="10"
          y="11.35"
          textAnchor="middle"
          dominantBaseline="middle"
          fontSize="6.4"
          fontWeight="900"
          className={filled ? 'fill-white' : 'fill-gray-500 dark:fill-white/55'}
          style={{
            paintOrder: 'stroke fill',
            stroke: filled ? 'rgba(0,0,0,0.9)' : 'transparent',
            strokeWidth: filled ? 0.75 : 0,
          }}
        >
          {stars}
        </text>
      </svg>
    </span>
  )
}

function RatingThresholdStars({
  value,
  disabled,
  onChange,
}: {
  value: number | undefined
  disabled?: boolean
  onChange: (stars: number) => void
}) {
  const selected = Math.max(1, Math.min(5, Math.round(Number(value ?? 3))))

  return (
    <div className="text-right">
      <div
        role="radiogroup"
        aria-label="Sterne-Grenze"
        className={[
          'flex flex-wrap items-center justify-end gap-1',
          disabled ? 'pointer-events-none opacity-50' : '',
        ].join(' ')}
      >
        {[1, 2, 3, 4, 5].map((stars) => {
          const filled = stars <= selected
          const checked = stars === selected

          return (
            <button
              key={stars}
              type="button"
              role="radio"
              aria-checked={checked}
              disabled={disabled}
              title={`Bis einschließlich ${stars} Stern${stars === 1 ? '' : 'e'} löschen`}
              onClick={() => onChange(stars)}
              className={[
                'rounded-md p-0.5 transition',
                'focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-400 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-gray-950',
                disabled
                  ? 'cursor-not-allowed'
                  : 'hover:scale-110 active:scale-95',
              ].join(' ')}
            >
              <RatingThresholdStar stars={stars} filled={filled} />
            </button>
          )
        })}
      </div>

      <div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
        Löscht bis einschließlich {selected} Stern{selected === 1 ? '' : 'e'}.
      </div>
    </div>
  )
}

export default function RecorderSettings({ onAssetsGenerated }: Props) {
  const [value, setValue] = useState<RecorderSettings>(DEFAULTS)
  const [saving, setSaving] = useState(false)
  const [saveSuccessUntilMs, setSaveSuccessUntilMs] = useState<number>(0)
  const saveSuccessTimerRef = useRef<number | null>(null)
  const appLogPreRef = useRef<HTMLPreElement | null>(null)
  const [browsing, setBrowsing] = useState<'record' | 'done' | 'ffmpeg' | null>(null)
  const [msg, setMsg] = useState<string | null>(null)
  const [err, setErr] = useState<string | null>(null)
  const [diskStatus, setDiskStatus] = useState<DiskStatus | null>(null)
  const [aiServerStatus, setAiServerStatus] = useState<AIServerStatus | null>(null)
  const [appLog, setAppLog] = useState<AppLog | null>(null)
  const [appLogLoading, setAppLogLoading] = useState(false)
  const [appLogOpen, setAppLogOpen] = useState(false)
  const [dbModalOpen, setDbModalOpen] = useState(false)
  const [loadedDatabaseUrl, setLoadedDatabaseUrl] = useState('')
  const [pendingDbPassword, setPendingDbPassword] = useState('') // wird nur beim Speichern gesendet
  const now = Date.now()
  const saveSucceeded = saveSuccessUntilMs > now
  const saveFailed = Boolean(err) && !saving && !saveSucceeded

  const [authStatus, setAuthStatus] = useState<AuthSecurityStatus | null>(null)
  const [securityBusy, setSecurityBusy] = useState(false)

  const [currentPassword, setCurrentPassword] = useState('')
  const [newPassword, setNewPassword] = useState('')
  const [newPasswordRepeat, setNewPasswordRepeat] = useState('')

  const [disable2FAPassword, setDisable2FAPassword] = useState('')
  const [disable2FACode, setDisable2FACode] = useState('')

  const [disable2FAFieldError, setDisable2FAFieldError] = useState<{
    password?: string
    code?: string
  } | null>(null)

  const [passkeySupported, setPasskeySupported] = useState(false)
  const [passkeyFeedback, setPasskeyFeedback] = useState<{
    kind: 'loading' | 'success' | 'error'
    text: string
  } | null>(null)

  type SaveUiState = 'idle' | 'saving' | 'success' | 'error'
  const saveUiState: SaveUiState = saving ? 'saving' : saveSucceeded ? 'success' : saveFailed ? 'error' : 'idle'

  function scrollAppLogToBottom() {
    window.requestAnimationFrame(() => {
      const el = appLogPreRef.current
      if (!el) return

      el.scrollTop = el.scrollHeight
    })
  }

  const saveButton = (() => {
    if (saveUiState === 'saving') {
      return {
        text: 'Speichern…',
        color: 'blue' as const,
        icon: null,            // Spinner kommt über isLoading
        isLoading: true,
      }
    }
    if (saveUiState === 'success') {
      return {
        text: 'Gespeichert',
        color: 'emerald' as const,
        icon: <CheckIcon className="size-4" />,
        isLoading: false,
      }
    }
    if (saveUiState === 'error') {
      return {
        text: 'Fehler',
        color: 'red' as const,
        icon: <XMarkIcon className="size-4" />,
        isLoading: false,
      }
    }
    return {
      text: 'Speichern',
      color: 'indigo' as const,
      icon: null,
      isLoading: false,
    }
  })()

  const [assetTasks, setAssetTasks] = useState<TaskItem[]>([])
  const [checkVideoTasks, setCheckVideoTasks] = useState<TaskItem[]>([])
  const [cleanupTasks, setCleanupTasks] = useState<TaskItem[]>([])
  const [regenerateAssetsTasks, setRegenerateAssetsTasks] = useState<TaskItem[]>([])
  const reportedFinishedAssetTaskIdsRef = useRef<Set<string>>(new Set())

  const pauseGB = Number(value.lowDiskPauseBelowGB ?? DEFAULTS.lowDiskPauseBelowGB ?? 5)
  const uiPauseGB = diskStatus?.pauseGB ?? pauseGB
  const uiResumeGB = diskStatus?.resumeGB ?? (pauseGB + 3)

  async function startAssetsTaskFromCurrentSwitches() {
    setErr(null)

    const payload = {
      meta: !!value.generateAssetsMeta,
      thumb: !!value.generateAssetsThumb,
      teaser: !!value.generateAssetsTeaser,
      sprites: !!value.generateAssetsSprites,
      analyze: !!value.generateAssetsAnalyze,
    }

    if (!Object.values(payload).some(Boolean)) {
      setErr('Mindestens eine Asset-Phase aktivieren.')
      return null
    }

    const res = await fetch('/api/tasks/generate-assets', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      cache: 'no-store',
      body: JSON.stringify(payload),
    })

    if (!res.ok) {
      const t = await res.text().catch(() => '')
      throw new Error(t || `HTTP ${res.status}`)
    }

    return await res.json().catch(() => null)
  }

  useEffect(() => {
    return () => {
      if (saveSuccessTimerRef.current != null) {
        window.clearTimeout(saveSuccessTimerRef.current)
        saveSuccessTimerRef.current = null
      }
    }
  }, [])

  useEffect(() => {
    setPasskeySupported(
      typeof window !== 'undefined' &&
        typeof window.PublicKeyCredential !== 'undefined'
    )
  }, [])

  useEffect(() => {
    void loadAuthStatus()
  }, [])

  useEffect(() => {
    let alive = true
    fetch('/api/settings', { cache: 'no-store' })
      .then(async (r) => {
        if (!r.ok) throw new Error(await r.text())
        return r.json()
      })
      .then((data: RecorderSettings) => {
        if (!alive) return
        setValue({
          databaseUrl: String((data as any).databaseUrl ?? ''),
          hasDbPassword: Boolean((data as any).hasDbPassword ?? false),
          recordDir: (data.recordDir || DEFAULTS.recordDir).toString(),
          doneDir: (data.doneDir || DEFAULTS.doneDir).toString(),
          ffmpegPath: String(data.ffmpegPath ?? DEFAULTS.ffmpegPath ?? ''),


          // ✅ falls backend die Felder noch nicht hat -> defaults nutzen
          autoAddToDownloadList: data.autoAddToDownloadList ?? DEFAULTS.autoAddToDownloadList,
          autoStartAddedDownloads:
            (data as any).autoStartAddedDownloads ?? DEFAULTS.autoStartAddedDownloads,
          enableConcurrentDownloadsLimit:
            (data as any).enableConcurrentDownloadsLimit ?? DEFAULTS.enableConcurrentDownloadsLimit,
          maxConcurrentDownloads:
            (data as any).maxConcurrentDownloads ?? DEFAULTS.maxConcurrentDownloads,

          useProviderApis:
            (data as any).useProviderApis ??
            Boolean((data as any).useChaturbateApi || (data as any).useMyFreeCamsApi),

          useChaturbateApi:
            (data as any).useChaturbateApi ?? DEFAULTS.useChaturbateApi,

          useMyFreeCamsApi:
            (data as any).useMyFreeCamsApi ?? DEFAULTS.useMyFreeCamsApi,
          autoDeleteSmallDownloads: data.autoDeleteSmallDownloads ?? DEFAULTS.autoDeleteSmallDownloads,
          autoDeleteSmallDownloadsBelowMB: data.autoDeleteSmallDownloadsBelowMB ?? DEFAULTS.autoDeleteSmallDownloadsBelowMB,
          autoDeleteSmallDownloadsKeepFavorites:
            (data as any).autoDeleteSmallDownloadsKeepFavorites ?? DEFAULTS.autoDeleteSmallDownloadsKeepFavorites,
          autoDeleteLowRatedDownloads:
            (data as any).autoDeleteLowRatedDownloads ?? DEFAULTS.autoDeleteLowRatedDownloads,
          autoDeleteLowRatedDownloadsMaxStars:
            (data as any).autoDeleteLowRatedDownloadsMaxStars ?? DEFAULTS.autoDeleteLowRatedDownloadsMaxStars,
          blurPreviews: data.blurPreviews ?? DEFAULTS.blurPreviews,
          teaserPlayback: (data as any).teaserPlayback ?? DEFAULTS.teaserPlayback,
          teaserAudio: (data as any).teaserAudio ?? DEFAULTS.teaserAudio,
          lowDiskPauseBelowGB: (data as any).lowDiskPauseBelowGB ?? DEFAULTS.lowDiskPauseBelowGB,
          enableNotifications: (data as any).enableNotifications ?? DEFAULTS.enableNotifications,
          generateAssetsMeta: (data as any).generateAssetsMeta ?? DEFAULTS.generateAssetsMeta,
          generateAssetsThumb: (data as any).generateAssetsThumb ?? DEFAULTS.generateAssetsThumb,
          generateAssetsTeaser: (data as any).generateAssetsTeaser ?? DEFAULTS.generateAssetsTeaser,
          generateAssetsSprites: (data as any).generateAssetsSprites ?? DEFAULTS.generateAssetsSprites,
          generateAssetsAnalyze: (data as any).generateAssetsAnalyze ?? DEFAULTS.generateAssetsAnalyze,
          enrichPostworkEnabled: (data as any).enrichPostworkEnabled ?? DEFAULTS.enrichPostworkEnabled,

          trainingRecognitionEnabled:
            (data as any).trainingRecognitionEnabled ?? DEFAULTS.trainingRecognitionEnabled,
          trainingDetectorEpochs:
            (data as any).trainingDetectorEpochs ?? DEFAULTS.trainingDetectorEpochs,
        })
        setLoadedDatabaseUrl(String((data as any).databaseUrl ?? '').trim())
      })
      .catch(() => {
        // backend evtl. noch alt -> defaults lassen
      })
    return () => {
      alive = false
    }
  }, [])

  useEffect(() => {
    let alive = true

    const loadTasksStatus = async () => {
      try {
        const res = await fetch('/api/tasks/status', { cache: 'no-store' })
        if (!res.ok) return

        const data = await res.json().catch(() => null)
        if (!alive || !data) return
        
        const assetJobs = Array.isArray(data?.generateAssetsJobs) ? data.generateAssetsJobs : []
        setAssetTasks(
          assetJobs
            .map(mapAssetJobToTask)
            .filter((t: TaskItem) => t.status !== 'idle')
        )

        const regenJobs = Array.isArray(data?.regenerateAssetsJobs)
          ? data.regenerateAssetsJobs
          : []

        setRegenerateAssetsTasks((prev) => {
          const prevById = new Map(prev.map((t: TaskItem) => [t.id, t] as const))

          const nextTasks: TaskItem[] = regenJobs
            .map((job: any): TaskItem | null => {
              const file = String(job?.file || '').trim()
              if (!file) return null

              const id = regenerateTaskId(file)
              const prevTask = prevById.get(id)

              const state = String(job?.state || '').trim().toLowerCase()
              const error = String(job?.error || '').trim()

              const cancelled =
                state === 'cancelled' ||
                error.toLowerCase() === 'abgebrochen' ||
                error.toLowerCase() === 'abbruch' ||
                error.toLowerCase() === 'cancelled' ||
                error.toLowerCase() === 'canceled'

              let status: TaskItem['status'] = 'idle'

              if (state === 'queued') status = 'queued'
              else if (state === 'running') status = 'running'
              else if (state === 'done') status = 'done'
              else if (cancelled) status = 'cancelled'
              else if (state === 'error') status = 'error'

              const phases = Array.isArray(job?.phases) ? job.phases : []

              const relevantPhases = phases
                .filter((p: any) => {
                  const q = String(p?.queue || '').toLowerCase()
                  const ph = String(p?.phase || '').toLowerCase()
                  return (
                    (q === 'postwork' && ['meta', 'thumb', 'teaser', 'sprites'].includes(ph)) ||
                    (q === 'enrich' && ph === 'analyze')
                  )
                })
                .sort((a: any, b: any) => {
                  return regeneratePhaseOrder(a?.queue, a?.phase) - regeneratePhaseOrder(b?.queue, b?.phase)
                })

              const runningPhase = relevantPhases.find(
                (p: any) => String(p?.state || '').toLowerCase() === 'running'
              )

              const queuedPhase = relevantPhases.find(
                (p: any) => String(p?.state || '').toLowerCase() === 'queued'
              )

              const activePhase = runningPhase ?? queuedPhase

              const phaseLabel = assetsPhaseLabel(
                String(activePhase?.label || '').trim(),
                String(activePhase?.phase || '').trim()
              )

              const total = relevantPhases.length || 5
              const done = relevantPhases.filter(
                (p: any) => String(p?.state || '').toLowerCase() === 'done'
              ).length

              const stableQueuedAtMs =
                prevTask?.queuedAtMs ||
                Number(job?.enqueuedAt ?? 0) ||
                Number(job?.startedAt ?? 0) ||
                Date.now()

              const stableStartedAtMs =
                Number(job?.startedAt ?? 0) ||
                prevTask?.startedAtMs ||
                0

              const text =
                status === 'running' || status === 'queued'
                  ? phaseLabel
                    ? `${shortTaskFilename(file, 38)} · ${phaseLabel}`
                    : shortTaskFilename(file, 52)
                  : shortTaskFilename(file, 52)

              return {
                id,
                status,
                title: regenerateTaskTitle(file),
                text,
                done: status === 'done' ? total : done,
                total,
                err: status === 'error' ? error || 'Fehler beim Regenerieren.' : undefined,
                cancellable: status === 'queued' || status === 'running',
                fading: prevTask?.fading ?? false,
                queuedAtMs: stableQueuedAtMs,
                startedAtMs: stableStartedAtMs,
              }
            })
            .filter(Boolean) as TaskItem[]

          const nextById = new Map(nextTasks.map((t: TaskItem) => [t.id, t] as const))
          const merged: TaskItem[] = []

          for (const next of nextTasks) {
            const prevTask = prevById.get(next.id)
            merged.push(prevTask?.fading ? { ...next, fading: true } : next)
          }

          for (const prevTask of prev) {
            if (nextById.has(prevTask.id)) continue
            if (prevTask.status === 'running' || prevTask.status === 'queued') continue

            if (prevTask.fading) {
              merged.push(prevTask)
              continue
            }

            merged.push({ ...prevTask, fading: true })

            window.setTimeout(() => {
              setRegenerateAssetsTasks((cur: TaskItem[]) =>
                cur.filter((t: TaskItem) => t.id !== prevTask.id)
              )
            }, 500)
          }

          return merged
        })

        const checkVideoJobs = Array.isArray(data?.checkVideosJobs) ? data.checkVideosJobs : []
        setCheckVideoTasks(
          checkVideoJobs
            .map(mapCheckVideosJobToTask)
            .filter((t: TaskItem) => t.status !== 'idle')
        )

        const cleanupJobs = Array.isArray(data?.cleanupJobs) ? data.cleanupJobs : []
        setCleanupTasks(
          cleanupJobs
            .map(mapCleanupJobToTask)
            .filter((t: TaskItem) => t.status !== 'idle')
        )
      } catch {
        // ignore
      }
    }

    void loadTasksStatus()
    const timer = window.setInterval(loadTasksStatus, 1500)

    return () => {
      alive = false
      window.clearInterval(timer)
    }
  }, [])

  useEffect(() => {
    for (const task of assetTasks) {
      if (task.status === 'done') {
        if (!reportedFinishedAssetTaskIdsRef.current.has(task.id)) {
          reportedFinishedAssetTaskIdsRef.current.add(task.id)
          onAssetsGenerated?.()
        }
      } else if (task.status === 'queued' || task.status === 'running') {
        reportedFinishedAssetTaskIdsRef.current.delete(task.id)
      }
    }
  }, [assetTasks, onAssetsGenerated])

  useEffect(() => {
    let alive = true
    const load = async () => {
      try {
        const r = await fetch('/api/status/disk', { cache: 'no-store' })
        if (!r.ok) return
        const data = (await r.json()) as DiskStatus
        if (alive) setDiskStatus(data)
      } catch {
        // ignorieren
      }
    }

    load()
    const t = window.setInterval(load, 1000) // jede Sekunde aktualisieren
    return () => {
      alive = false
      window.clearInterval(t)
    }
  }, [])

  useEffect(() => {
    let alive = true

    const load = async () => {
      try {
        const res = await fetch('/api/status/ai-server', { cache: 'no-store' })
        const data = await res.json().catch(() => null)

        if (!alive) return

        if (!res.ok || !data) {
          setAiServerStatus({
            ok: false,
            running: false,
            error: `HTTP ${res.status}`,
          })
          return
        }

        setAiServerStatus({
          ok: Boolean(data.ok),
          running: Boolean(data.running),
          url: String(data.url || ''),
          statusCode: Number(data.statusCode || 0),
          error: String(data.error || ''),
        })
      } catch (e) {
        if (!alive) return

        setAiServerStatus({
          ok: false,
          running: false,
          error: e instanceof Error ? e.message : String(e),
        })
      }
    }

    void load()
    const timer = window.setInterval(load, 5000)

    return () => {
      alive = false
      window.clearInterval(timer)
    }
  }, [])

  async function loadAppLog() {
    setAppLogLoading(true)

    try {
      const res = await fetch('/api/status/log?maxBytes=131072', {
        cache: 'no-store',
      })

      const data = await res.json().catch(() => null)

      if (!res.ok || !data) {
        setAppLog({
          ok: false,
          log: '',
          error: `HTTP ${res.status}`,
        })
        return
      }

      setAppLog({
        ok: Boolean(data.ok),
        exists: Boolean(data.exists),
        path: String(data.path || ''),
        sizeBytes: Number(data.sizeBytes || 0),
        log: String(data.log || ''),
        error: String(data.error || ''),
      })
    } catch (e) {
      setAppLog({
        ok: false,
        log: '',
        error: e instanceof Error ? e.message : String(e),
      })
    } finally {
      setAppLogLoading(false)
      scrollAppLogToBottom()
    }
  }

  useEffect(() => {
    if (!appLogOpen) return
    void loadAppLog()
  }, [appLogOpen])

  useEffect(() => {
    if (!appLogOpen) return

    scrollAppLogToBottom()
  }, [appLogOpen, appLog?.log, appLogLoading])

  async function browse(target: 'record' | 'done' | 'ffmpeg') {
    setErr(null)
    setMsg(null)
    setBrowsing(target)

    try {
      window.focus()
      const res = await fetch(`/api/settings/browse?target=${target}`, { cache: 'no-store' })
      if (res.status === 204) return // user cancelled
      if (!res.ok) {
        const t = await res.text().catch(() => '')
        throw new Error(t || `HTTP ${res.status}`)
      }

      const data = (await res.json()) as { path?: string }
      const p = (data.path ?? '').trim()
      if (!p) return

      setValue((v) => {
        if (target === 'record') return { ...v, recordDir: p }
        if (target === 'done') return { ...v, doneDir: p }
        return { ...v, ffmpegPath: p }
      })
    } catch (e: any) {
      setErr(e?.message ?? String(e))
    } finally {
      setBrowsing(null)
    }
  }

  async function save() {
    setErr(null)
    setMsg(null)

    const recordDir = value.recordDir.trim()
    const doneDir = value.doneDir.trim()
    const ffmpegPath = (value.ffmpegPath ?? '').trim()
    const databaseUrl = String((value as any).databaseUrl ?? '').trim()
    const hadPendingDbPassword = Boolean((pendingDbPassword || '').trim())

    if (!recordDir || !doneDir) {
      setErr('Bitte Aufnahme-Ordner und Ziel-Ordner angeben.')
      return
    }

    // ✅ Switch-Logik: Autostart nur sinnvoll, wenn Auto-Add aktiv ist
    const autoAddToDownloadList = !!value.autoAddToDownloadList
    const autoStartAddedDownloads = !!value.autoStartAddedDownloads
    const enableConcurrentDownloadsLimit = !!(value as any).enableConcurrentDownloadsLimit
    const maxConcurrentDownloads = Math.max(
      1,
      Math.min(100, Math.floor(Number((value as any).maxConcurrentDownloads ?? DEFAULTS.maxConcurrentDownloads ?? 20)))
    )

    const useProviderApis = !!value.useProviderApis
    const useChaturbateApi = useProviderApis && !!value.useChaturbateApi
    const useMyFreeCamsApi = useProviderApis && !!value.useMyFreeCamsApi
    const autoDeleteSmallDownloads = !!value.autoDeleteSmallDownloads
    const autoDeleteSmallDownloadsBelowMB = Math.max(
      0,
      Math.min(100_000, Math.floor(Number(value.autoDeleteSmallDownloadsBelowMB ?? DEFAULTS.autoDeleteSmallDownloadsBelowMB)))
    )
    const autoDeleteSmallDownloadsKeepFavorites = !!value.autoDeleteSmallDownloadsKeepFavorites
    const autoDeleteLowRatedDownloads = !!value.autoDeleteLowRatedDownloads
    const autoDeleteLowRatedDownloadsMaxStars = Math.max(
      1,
      Math.min(5, Math.floor(Number(value.autoDeleteLowRatedDownloadsMaxStars ?? DEFAULTS.autoDeleteLowRatedDownloadsMaxStars ?? 3)))
    )
    const blurPreviews = !!value.blurPreviews
    const teaserPlayback =
      value.teaserPlayback === 'still' || value.teaserPlayback === 'all' || value.teaserPlayback === 'hover'
        ? value.teaserPlayback
        : DEFAULTS.teaserPlayback
    const teaserAudio = !!value.teaserAudio
    const lowDiskPauseBelowGB = Math.max(1, Math.floor(Number(value.lowDiskPauseBelowGB ?? DEFAULTS.lowDiskPauseBelowGB)))
    const enableNotifications = !!value.enableNotifications
    const generateAssetsMeta = !!value.generateAssetsMeta
    const generateAssetsThumb = !!value.generateAssetsThumb
    const generateAssetsTeaser = !!value.generateAssetsTeaser
    const generateAssetsSprites = !!value.generateAssetsSprites
    const generateAssetsAnalyze = !!value.generateAssetsAnalyze
    const enrichPostworkEnabled = !!value.enrichPostworkEnabled

    const trainingRecognitionEnabled = !!value.trainingRecognitionEnabled
    const trainingDetectorEpochs = Math.max(
      1,
      Math.min(300, Math.floor(Number(value.trainingDetectorEpochs ?? DEFAULTS.trainingDetectorEpochs ?? 60)))
    )

    setSaving(true)
    try {
      const res = await fetch('/api/settings', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          databaseUrl,
          dbPassword: pendingDbPassword || undefined,
          recordDir,
          doneDir,
          ffmpegPath,
          autoAddToDownloadList,
          autoStartAddedDownloads,
          enableConcurrentDownloadsLimit,
          maxConcurrentDownloads,
          useProviderApis,
          useChaturbateApi,
          useMyFreeCamsApi,
          autoDeleteSmallDownloads,
          autoDeleteSmallDownloadsBelowMB,
          autoDeleteSmallDownloadsKeepFavorites,
          autoDeleteLowRatedDownloads,
          autoDeleteLowRatedDownloadsMaxStars,
          blurPreviews,
          teaserPlayback,
          teaserAudio,
          lowDiskPauseBelowGB,
          enableNotifications,
          generateAssetsMeta,
          generateAssetsThumb,
          generateAssetsTeaser,
          generateAssetsSprites,
          generateAssetsAnalyze,
          enrichPostworkEnabled,
          trainingRecognitionEnabled,
          trainingDetectorEpochs,
        }),
      })
      if (!res.ok) {
        const t = await res.text().catch(() => '')
        throw new Error(t || `HTTP ${res.status}`)
      }
      // Button-Label: "Gespeichert" für 2.5s
      const until = Date.now() + 2500
      setSaveSuccessUntilMs(until)

      if (saveSuccessTimerRef.current != null) {
        window.clearTimeout(saveSuccessTimerRef.current)
      }
      saveSuccessTimerRef.current = window.setTimeout(() => {
        setSaveSuccessUntilMs(0)
        saveSuccessTimerRef.current = null
      }, 2500)
      window.dispatchEvent(new CustomEvent('recorder-settings-updated'))

      const databaseUrlChanged =
        databaseUrl !== loadedDatabaseUrl || hadPendingDbPassword

      if (databaseUrlChanged) {
        window.dispatchEvent(
          new CustomEvent('models-db-changed', {
            detail: { databaseUrl },
          })
        )
        setLoadedDatabaseUrl(databaseUrl)
        setPendingDbPassword('')
      }
    } catch (e: any) {
      setSaveSuccessUntilMs(0)
      if (saveSuccessTimerRef.current != null) {
        window.clearTimeout(saveSuccessTimerRef.current)
        saveSuccessTimerRef.current = null
      }
      setErr(e?.message ?? String(e))
    } finally {
      setSaving(false)
    }
  }

  function fadeOutRegenerateTask(file: string, delayMs = 3000, fadeMs = 500) {
    const id = regenerateTaskId(file)

    window.setTimeout(() => {
      setRegenerateAssetsTasks((prev) =>
        prev.map((t) => (t.id === id ? { ...t, fading: true } : t))
      )

      window.setTimeout(() => {
        setRegenerateAssetsTasks((prev) => prev.filter((t) => t.id !== id))
      }, fadeMs)
    }, delayMs)
  }

  async function cleanupSmallDone() {
    setErr(null)

    const mb = Number(value.autoDeleteSmallDownloadsBelowMB ?? DEFAULTS.autoDeleteSmallDownloadsBelowMB ?? 0)
    const doneDir = (value.doneDir || DEFAULTS.doneDir).trim()
    const deleteLowRated = !!value.autoDeleteLowRatedDownloads
    const maxStars = Math.max(
      1,
      Math.min(5, Math.floor(Number(value.autoDeleteLowRatedDownloadsMaxStars ?? DEFAULTS.autoDeleteLowRatedDownloadsMaxStars ?? 3)))
    )

    if (!doneDir) {
      setErr('doneDir ist leer.')
      return null
    }
    if ((!mb || mb <= 0) && !deleteLowRated) {
      setErr('Mindestgröße ist 0 und Rating-Löschen ist deaktiviert – es würde nichts gelöscht.')
      return null
    }

    const ratingLine = deleteLowRated
      ? `• Rating ≤ ${maxStars} Stern${maxStars === 1 ? '' : 'e'}\n`
      : ''

    const sizeLine = mb > 0
      ? `• Dateien < ${mb} MB in "${doneDir}"\n`
      : ''

    const ok = window.confirm(
      `Aufräumen:\n` +
        sizeLine +
        ratingLine +
        `• Entfernt defekte Video-Dateien\n` +
        `• Löscht Record-Reste und verwaiste Assets\n\n` +
        `Fortfahren?`
    )
    if (!ok) return null

    const res = await fetch('/api/settings/cleanup', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      cache: 'no-store',
    })

    if (!res.ok) {
      const t = await res.text().catch(() => '')
      throw new Error(t || `HTTP ${res.status}`)
    }

    return await res.json().catch(() => null)
  }

  async function cancelAssetsTask(jobId: string) {
    try {
      await fetch(`/api/tasks/generate-assets?id=${encodeURIComponent(jobId)}`, {
        method: 'DELETE',
        cache: 'no-store' as any,
      })
    } catch {
      // ignore
    }
  }

  async function cancelRegenerateAssetsTask(file: string) {
    const cleanFile = String(file || '').trim()
    if (!cleanFile) return

    setRegenerateAssetsTasks((prev) =>
      prev.map((t) =>
        t.id === regenerateTaskId(cleanFile)
          ? {
              ...t,
              status: 'cancelled',
              title: regenerateTaskTitle(cleanFile),
              text: shortTaskFilename(cleanFile, 52),
              err: undefined,
              cancellable: false,
              fading: false,
            }
          : t
      )
    )

    try {
      await fetch('/api/record/regenerate-assets', {
        method: 'DELETE',
        headers: { 'Content-Type': 'application/json' },
        cache: 'no-store' as any,
        body: JSON.stringify({ file: cleanFile }),
      })
    } catch {
      // ignore
    }

    fadeOutRegenerateTask(cleanFile)
  }

  async function cancelCheckVideosTask(jobId: string) {
    try {
      await fetch(`/api/tasks/check-videos?id=${encodeURIComponent(jobId)}`, {
        method: 'DELETE',
        cache: 'no-store' as any,
      })
    } catch {
      // ignore
    }
  }

  async function cancelCleanupTask(jobId: string) {
    try {
      await fetch(`/api/settings/cleanup?id=${encodeURIComponent(jobId)}`, {
        method: 'DELETE',
        cache: 'no-store' as any,
      })
    } catch {
      // ignore
    }
  }

  const visibleTasks: TaskItem[] = [
    ...assetTasks,
    ...regenerateAssetsTasks,
    ...checkVideoTasks,
    ...cleanupTasks,
  ].filter((t: TaskItem) => t.status !== 'idle')

  async function loadAuthStatus() {
    try {
      const res = await fetch('/api/auth/me', { cache: 'no-store' })
      if (!res.ok) return

      const data = await res.json().catch(() => null)
      if (!data) return

      setAuthStatus({
        authenticated: Boolean(data.authenticated),
        totpEnabled: Boolean(data.totpEnabled),
        totpConfigured: Boolean(data.totpConfigured),
        passkeyConfigured: Boolean(data.passkeyConfigured),
        passkeyCount: Number(data.passkeyCount ?? 0),
        totpDevice: data.totpDevice ?? null,
        passkeys: Array.isArray(data.passkeys) ? data.passkeys : [],
      })
    } catch {
      // ignore
    }
  }

  async function changePassword() {
    setErr(null)
    setMsg(null)

    if (!currentPassword.trim()) {
      setErr('Bitte aktuelles Passwort eingeben.')
      return
    }

    if (newPassword.length < 8) {
      setErr('Das neue Passwort muss mindestens 8 Zeichen lang sein.')
      return
    }

    if (newPassword !== newPasswordRepeat) {
      setErr('Die neuen Passwörter stimmen nicht überein.')
      return
    }

    setSecurityBusy(true)

    try {
      const res = await fetch('/api/auth/password/change', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        cache: 'no-store',
        body: JSON.stringify({
          currentPassword,
          newPassword,
        }),
      })

      if (!res.ok) {
        const text = await res.text().catch(() => '')
        throw new Error(text || `HTTP ${res.status}`)
      }

      setCurrentPassword('')
      setNewPassword('')
      setNewPasswordRepeat('')
      setMsg('Passwort wurde geändert.')
    } catch (e: any) {
      setErr(e?.message ?? String(e))
    } finally {
      setSecurityBusy(false)
    }
  }

  async function disable2FA() {
    setErr(null)
    setMsg(null)
    setDisable2FAFieldError(null)

    const password = disable2FAPassword.trim()
    const code = disable2FACode.trim()

    const nextFieldError: {
      password?: string
      code?: string
    } = {}

    if (!password) {
      nextFieldError.password = 'Gib dein aktuelles Passwort ein, um 2FA zu deaktivieren.'
    }

    if (authStatus?.totpEnabled && !/^\d{6}$/.test(code)) {
      nextFieldError.code = 'Gib den 6-stelligen Code aus deiner Authenticator-App ein.'
    }

    if (nextFieldError.password || nextFieldError.code) {
      setDisable2FAFieldError(nextFieldError)
      return
    }

    const ok = window.confirm('2FA wirklich deaktivieren? Dein Login ist danach weniger geschützt.')
    if (!ok) return

    setSecurityBusy(true)

    try {
      const res = await fetch('/api/auth/2fa/disable', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        cache: 'no-store',
        body: JSON.stringify({
          currentPassword: password,
          code,
        }),
      })

      if (!res.ok) {
        const text = await res.text().catch(() => '')
        throw new Error(text || `HTTP ${res.status}`)
      }

      setDisable2FAPassword('')
      setDisable2FACode('')
      setDisable2FAFieldError(null)
      setMsg('2FA wurde deaktiviert.')
      await loadAuthStatus()
    } catch (e: any) {
      setErr(e?.message ?? String(e))
    } finally {
      setSecurityBusy(false)
    }
  }

  function passkeyErrorMessage(e: any) {
    const raw = String(e?.message ?? e ?? '').trim()

    if (!raw) return 'Passkey konnte nicht hinzugefügt werden.'

    if (raw.includes('passkey registration verify failed')) {
      return (
        raw +
        '\n\nPrüfe AUTH_RP_ORIGIN und AUTH_RP_ID. Die Werte müssen exakt zur Browser-URL passen.'
      )
    }

    if (
      raw.includes('NotAllowedError') ||
      raw.toLowerCase().includes('not allowed') ||
      raw.toLowerCase().includes('aborted')
    ) {
      return 'Passkey-Einrichtung wurde abgebrochen oder vom Browser/Authenticator nicht erlaubt.'
    }

    if (
      raw.includes('SecurityError') ||
      raw.toLowerCase().includes('relying party') ||
      raw.toLowerCase().includes('rp id') ||
      raw.toLowerCase().includes('origin')
    ) {
      return (
        raw +
        '\n\nDas sieht nach einem Origin/RP-ID Problem aus. Prüfe AUTH_RP_ORIGIN und AUTH_RP_ID.'
      )
    }

    if (raw.includes('challenge expired')) {
      return 'Die Passkey-Challenge ist abgelaufen. Bitte erneut versuchen.'
    }

    return raw
  }

  async function registerSettingsPasskey() {
    if (securityBusy) return

    setErr(null)
    setMsg(null)
    setPasskeyFeedback({
      kind: 'loading',
      text: 'Starte Passkey-Einrichtung…',
    })
    setSecurityBusy(true)

    try {
      setPasskeyFeedback({
        kind: 'loading',
        text: 'Hole WebAuthn-Challenge vom Backend…',
      })

      const optionsRes = await fetch('/api/auth/passkey/register/options', {
        method: 'POST',
        cache: 'no-store',
      })

      if (!optionsRes.ok) {
        const text = await optionsRes.text().catch(() => '')
        throw new Error(text || `HTTP ${optionsRes.status}`)
      }

      const options = await optionsRes.json()

      setPasskeyFeedback({
        kind: 'loading',
        text: 'Browser-Passkey-Dialog geöffnet. Bitte Face ID, Fingerabdruck, Windows Hello oder Sicherheitsschlüssel bestätigen.',
      })

      const credential = await startRegistration({
        optionsJSON: unwrapWebAuthnOptions(options),
      })

      setPasskeyFeedback({
        kind: 'loading',
        text: 'Passkey wurde vom Browser erstellt. Verifiziere beim Backend…',
      })

      const verifyRes = await fetch('/api/auth/passkey/register/verify', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        cache: 'no-store',
        body: JSON.stringify(credential),
      })

      if (!verifyRes.ok) {
        const text = await verifyRes.text().catch(() => '')
        throw new Error(text || `HTTP ${verifyRes.status}`)
      }

      setMsg('Passkey wurde hinzugefügt.')
      setPasskeyFeedback({
        kind: 'success',
        text: 'Passkey wurde erfolgreich hinzugefügt.',
      })
      await loadAuthStatus()
    } catch (e: any) {
      const message = passkeyErrorMessage(e)

      setErr(message)
      setPasskeyFeedback({
        kind: 'error',
        text: message,
      })
    } finally {
      setSecurityBusy(false)
    }
  }

  async function deletePasskey(passkey: AuthDevice) {
    setErr(null)
    setMsg(null)

    const credentialId = String(passkey?.id ?? '').trim()
    if (!credentialId) {
      setErr('Passkey kann nicht gelöscht werden: Credential-ID fehlt.')
      return
    }

    const label = String(passkey?.label || 'diesen Passkey').trim()

    const ok = window.confirm(
      `"${label}" wirklich löschen? Anmeldung mit diesem Passkey ist danach nicht mehr möglich.`
    )
    if (!ok) return

    setSecurityBusy(true)

    try {
      const res = await fetch('/api/auth/passkey/delete', {
        method: 'DELETE',
        headers: { 'Content-Type': 'application/json' },
        cache: 'no-store',
        body: JSON.stringify({
          credentialId,
        }),
      })

      if (!res.ok) {
        const text = await res.text().catch(() => '')
        throw new Error(text || `HTTP ${res.status}`)
      }

      setMsg('Passkey wurde gelöscht.')
      await loadAuthStatus()
    } catch (e: any) {
      setErr(e?.message ?? String(e))
    } finally {
      setSecurityBusy(false)
    }
  }

  return (
    <Card
      header={
        <div className="flex items-start justify-between gap-4">
          <div className="min-w-0">
            <div className="flex flex-wrap items-center gap-2">
              <div className="text-base font-semibold text-gray-900 dark:text-white">
                Einstellungen
              </div>

              <span
                className={[
                  'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-bold ring-1',
                  aiServerStatus === null
                    ? 'bg-gray-100 text-gray-700 ring-gray-200 dark:bg-white/10 dark:text-gray-200 dark:ring-white/10'
                    : aiServerStatus.running
                      ? 'bg-emerald-50 text-emerald-800 ring-emerald-200 dark:bg-emerald-500/15 dark:text-emerald-100 dark:ring-emerald-400/30'
                      : 'bg-red-50 text-red-800 ring-red-200 dark:bg-red-500/15 dark:text-red-100 dark:ring-red-400/30',
                ].join(' ')}
                title={
                  aiServerStatus === null
                    ? 'AI Server Status wird geladen…'
                    : aiServerStatus.running
                      ? `AI Server läuft${aiServerStatus.url ? `: ${aiServerStatus.url}` : ''}`
                      : `AI Server offline${aiServerStatus.error ? `: ${aiServerStatus.error}` : ''}`
                }
              >
                <span
                  className={[
                    'h-1.5 w-1.5 rounded-full',
                    aiServerStatus === null
                      ? 'bg-gray-400'
                      : aiServerStatus.running
                        ? 'bg-emerald-500'
                        : 'bg-red-500',
                  ].join(' ')}
                  aria-hidden="true"
                />

                {aiServerStatus === null
                  ? 'AI prüft…'
                  : aiServerStatus.running
                    ? 'AI online'
                    : 'AI offline'}
              </span>
            </div>
            <div className="mt-0.5 text-xs text-gray-600 dark:text-gray-300">
              Recorder-Konfiguration, Automatisierung und Tasks.
            </div>
          </div>

          {/* Rechts: Alerts + Button */}
          <div className="flex items-start gap-2">
            <Button
              variant="primary"
              color={saveButton.color}
              onClick={save}
              disabled={saving}
              className="shrink-0"
              size="md"
              isLoading={saveButton.isLoading}
              leadingIcon={saveButton.icon}
            >
              {saveButton.text}
            </Button>
          </div>
        </div>
      }
      grayBody
    >
      <div className="space-y-4">
        <TaskList
          tasks={visibleTasks}
          onCancel={(id: string) => {
            if (id.startsWith('generate-assets:')) {
              const jobId = id.slice('generate-assets:'.length)
              void cancelAssetsTask(jobId)
              return
            }

            if (id.startsWith('regenerate-assets:')) {
              const file = id.slice('regenerate-assets:'.length)
              void cancelRegenerateAssetsTask(file)
              return
            }

            if (id.startsWith('check-videos:')) {
              const jobId = id.slice('check-videos:'.length)
              void cancelCheckVideosTask(jobId)
              return
            }

            if (id.startsWith('cleanup:')) {
              const jobId = id.slice('cleanup:'.length)
              void cancelCleanupTask(jobId)
              return
            }
          }}
        />

        {/* Alerts (mobile) */}
        {saveUiState !== 'success' ? (
          <div className="sm:hidden">
            {err && (
              <div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-500/30 dark:bg-red-500/10 dark:text-red-200">
                {err}
              </div>
            )}
            {msg && (
              <div className="rounded-lg border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700 dark:border-green-500/30 dark:bg-green-500/10 dark:text-green-200">
                {msg}
              </div>
            )}
          </div>
        ) : null}

        {/* Aufgaben */}
        <SettingsSection
          id="tasks"
          title="Aufgaben"
          description="Hintergrundaufgaben wie z.B. Asset/Preview-Generierung."
          icon="⚙️"
          defaultOpen={true}
        >
          <div className="space-y-3">
            <Task
              title="Assets-Generator"
              description="Erzeugt nur fehlende oder unvollständige Assets der aktivierten Phasen. Nicht ausgewählte Phasen werden komplett übersprungen."
              startLabel="Start"
              startingLabel="Starte…"
              onTrigger={startAssetsTaskFromCurrentSwitches}
              onCreated={(job) => {
                if (!job?.id) return
                upsertTask(setAssetTasks, mapAssetJobToTask(job))
              }}
              onError={(message) => {
                setErr(message)
              }}
            />

            <div className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-white/10 dark:bg-white/5">
              <div className="space-y-2">
                <LabeledSwitch
                  checked={!!value.generateAssetsMeta}
                  onChange={(checked) => setValue((v) => ({ ...v, generateAssetsMeta: checked }))}
                  label="Meta"
                  description="Erzeugt oder ergänzt die meta.json mit Dauer, Auflösung, FPS und weiteren Videoinformationen."
                />

                <LabeledSwitch
                  checked={!!value.generateAssetsThumb}
                  onChange={(checked) => setValue((v) => ({ ...v, generateAssetsThumb: checked }))}
                  label="Vorschaubild"
                  description="Erzeugt das statische Vorschaubild preview.jpg für Listen und Kartenansicht."
                />

                <LabeledSwitch
                  checked={!!value.generateAssetsTeaser}
                  onChange={(checked) => setValue((v) => ({ ...v, generateAssetsTeaser: checked }))}
                  label="Teaser"
                  description="Erzeugt die kurze Video-Vorschau preview.mp4 für Hover- und Preview-Wiedergabe."
                />

                <LabeledSwitch
                  checked={!!value.generateAssetsSprites}
                  onChange={(checked) => setValue((v) => ({ ...v, generateAssetsSprites: checked }))}
                  label="Sprites"
                  description="Erzeugt das Sprite-Bild preview-sprite.jpg für die Scrubber- und Timeline-Vorschau."
                />

                <LabeledSwitch
                  checked={!!value.generateAssetsAnalyze}
                  onChange={(checked) => setValue((v) => ({ ...v, generateAssetsAnalyze: checked }))}
                  label="Analyse"
                  description="Führt die Analyse aus und ergänzt Treffer, Segmente und weitere Auswertungsdaten."
                />
              </div>
            </div>

            <Task
              title="Downloads prüfen"
              description="Prüft fertige Videos anhand der preview-sprite.jpg auf schwarze Endzellen und schreibt das Ergebnis in die meta.json."
              startLabel="Prüfen"
              startingLabel="Starte…"
              startUrl="/api/tasks/check-videos"
              onCreated={(job) => {
                if (!job?.id) return
                upsertTask(setCheckVideoTasks, mapCheckVideosJobToTask(job))
              }}
              onError={(message) => {
                setErr(message)
              }}
            />

            <Task
              title="Aufräumen"
              description='Löscht Dateien im doneDir kleiner als die Mindestgröße, optional Downloads bis zur gewählten Sterne-Bewertung, und entfernt verwaiste Assets.'
              startLabel="Aufräumen"
              startingLabel="Läuft…"
              onTrigger={cleanupSmallDone}
              disabled={saving}
              onCreated={(job) => {
                if (!job?.id) return
                upsertTask(setCleanupTasks, mapCleanupJobToTask(job))
              }}
              onError={(message) => {
                setErr(message)
              }}
            />
          </div>
        </SettingsSection>

        {/* Paths */}
        <SettingsSection
          id="paths"
          title="Pfad-Einstellungen"
          description="Aufnahme- und Zielverzeichnisse sowie optionaler ffmpeg-Pfad."
          icon="📁"
          defaultOpen={false}
        >
          <div className="mb-3">
            <div className="text-sm font-semibold text-gray-900 dark:text-white">Pfad-Einstellungen</div>
            <div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
              Aufnahme- und Zielverzeichnisse sowie optionaler ffmpeg-Pfad.
            </div>
          </div>
          
          {/* Datenbank */}
          <div className="mb-3 rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-950/40">
            <div className="mb-3">
              <div className="text-sm font-semibold text-gray-900 dark:text-white">Datenbank</div>
              <div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
                Postgres Verbindung. Passwort wird verschlüsselt gespeichert.
              </div>
            </div>

            <div className="space-y-3">
              <div className="grid grid-cols-1 gap-2 sm:grid-cols-12 sm:items-center">
                <label className="text-sm font-medium text-gray-900 dark:text-gray-200 sm:col-span-3">
                  Database URL
                </label>

                <div className="sm:col-span-9 flex gap-2">
                  <input
                    value={String((value as any).databaseUrl ?? '')}
                    onChange={(e) => setValue((v) => ({ ...v, databaseUrl: e.target.value }))}
                    placeholder="postgres://user@host:5432/db?sslmode=disable"
                    className="min-w-0 flex-1 rounded-lg px-3 py-2 text-sm bg-white text-gray-900 ring-1 ring-gray-200
                              focus:outline-none focus:ring-2 focus:ring-indigo-500
                              dark:bg-white/10 dark:text-white dark:ring-white/10"
                  />

                  <Button variant="secondary" onClick={() => setDbModalOpen(true)} disabled={saving}>
                    URL erstellen…
                  </Button>
                </div>
              </div>
            </div>
          </div>

          <PostgresUrlModal
            open={dbModalOpen}
            onClose={() => setDbModalOpen(false)}
            initialUrl={String((value as any).databaseUrl ?? '')}
            initialHasPassword={Boolean((value as any).hasDbPassword)}
            onApply={({ databaseUrl, dbPassword }) => {
              setValue((v) => ({ ...v, databaseUrl }))
              setPendingDbPassword(dbPassword || '')
            }}
          />

          <div className="space-y-3">
            {/* Aufnahme-Ordner */}
            <div className="grid grid-cols-1 gap-2 sm:grid-cols-12 sm:items-center">
              <label className="text-sm font-medium text-gray-900 dark:text-gray-200 sm:col-span-3">
                Aufnahme-Ordner
              </label>
              <div className="sm:col-span-9 flex gap-2">
                <input
                  value={value.recordDir}
                  onChange={(e) => setValue((v) => ({ ...v, recordDir: e.target.value }))}
                  placeholder="records  (oder absolut: C:\records / /mnt/data/records)"
                  className="min-w-0 flex-1 rounded-lg px-3 py-2 text-sm bg-white text-gray-900 ring-1 ring-gray-200
                            focus:outline-none focus:ring-2 focus:ring-indigo-500
                            dark:bg-white/10 dark:text-white dark:ring-white/10"
                />
                <Button variant="secondary" onClick={() => browse('record')} disabled={saving || browsing !== null}>
                  Durchsuchen...
                </Button>
              </div>
            </div>

            {/* Fertige Downloads */}
            <div className="grid grid-cols-1 gap-2 sm:grid-cols-12 sm:items-center">
              <label className="text-sm font-medium text-gray-900 dark:text-gray-200 sm:col-span-3">
                Fertige Downloads nach
              </label>
              <div className="sm:col-span-9 flex gap-2">
                <input
                  value={value.doneDir}
                  onChange={(e) => setValue((v) => ({ ...v, doneDir: e.target.value }))}
                  placeholder="records/done"
                  className="min-w-0 flex-1 rounded-lg px-3 py-2 text-sm bg-white text-gray-900 ring-1 ring-gray-200
                            focus:outline-none focus:ring-2 focus:ring-indigo-500
                            dark:bg-white/10 dark:text-white dark:ring-white/10"
                />
                <Button variant="secondary" onClick={() => browse('done')} disabled={saving || browsing !== null}>
                  Durchsuchen...
                </Button>
              </div>
            </div>

            {/* ffmpeg.exe */}
            <div className="grid grid-cols-1 gap-2 sm:grid-cols-12 sm:items-center">
              <label className="text-sm font-medium text-gray-900 dark:text-gray-200 sm:col-span-3">
                ffmpeg.exe
              </label>
              <div className="sm:col-span-9 flex gap-2">
                <input
                  value={value.ffmpegPath ?? ''}
                  onChange={(e) => setValue((v) => ({ ...v, ffmpegPath: e.target.value }))}
                  placeholder="Leer = automatisch (FFMPEG_PATH / ffmpeg im PATH)"
                  className="min-w-0 flex-1 rounded-lg px-3 py-2 text-sm bg-white text-gray-900 ring-1 ring-gray-200
                            focus:outline-none focus:ring-2 focus:ring-indigo-500
                            dark:bg-white/10 dark:text-white dark:ring-white/10"
                />
                <Button variant="secondary" onClick={() => browse('ffmpeg')} disabled={saving || browsing !== null}>
                  Durchsuchen...
                </Button>
              </div>
            </div>
          </div>
        </SettingsSection>

        {/* Automatisierung */}
        <SettingsSection
          id="automation"
          title="Automatisierung & Anzeige"
          description="Verhalten beim Hinzufügen/Starten sowie Anzeigeoptionen."
          icon="🤖"
          defaultOpen={false}
        >
          <div className="mb-3">
            <div className="text-sm font-semibold text-gray-900 dark:text-white">Automatisierung & Anzeige</div>
            <div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
              Verhalten beim Hinzufügen/Starten sowie Anzeigeoptionen.
            </div>
          </div>

          <div className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-white/10 dark:bg-white/5 mb-2">
            <LabeledSwitch
              checked={!!value.enableConcurrentDownloadsLimit}
              onChange={(checked) =>
                setValue((v) => ({
                  ...v,
                  enableConcurrentDownloadsLimit: checked,
                  maxConcurrentDownloads: v.maxConcurrentDownloads ?? 20,
                }))
              }
              label="Gleichzeitige Downloads begrenzen"
              description="Begrenzt die Anzahl parallel laufender Downloads. Weitere Starts werden vom Backend blockiert."
            />

            <div
              className={
                'mt-2 grid grid-cols-1 gap-2 sm:grid-cols-12 sm:items-center ' +
                (!value.enableConcurrentDownloadsLimit ? 'opacity-50 pointer-events-none' : '')
              }
            >
              <div className="sm:col-span-4">
                <div className="text-sm font-medium text-gray-900 dark:text-gray-200">Max. parallele Downloads</div>
                <div className="text-xs text-gray-600 dark:text-gray-300">
                  Mehr gleichzeitige Starts werden nicht zugelassen.
                </div>
              </div>

              <div className="sm:col-span-8">
                <div className="flex items-center justify-end gap-2">
                  <input
                    type="number"
                    min={1}
                    step={1}
                    value={value.maxConcurrentDownloads ?? 20}
                    onChange={(e) =>
                      setValue((v) => ({
                        ...v,
                        maxConcurrentDownloads: Number(e.target.value || 1),
                      }))
                    }
                    className="h-9 w-32 rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-900 shadow-sm
                              dark:border-white/10 dark:bg-gray-900 dark:text-gray-100"
                  />
                </div>
              </div>
            </div>
          </div>

          <div className="space-y-3">
            <LabeledSwitch
              checked={!!value.enrichPostworkEnabled}
              onChange={(checked) =>
                setValue((v) => ({
                  ...v,
                  enrichPostworkEnabled: checked,
                }))
              }
              label="EnrichQ-Postwork im Hintergrund"
              description="Wenn aktiv, werden fehlende Analyse-/Enrich-Assets automatisch im Hintergrund nachgeholt. Wenn deaktiviert, werden keine neuen EnrichQ-Hintergrundjobs gestartet; manuelle Asset-Generierung bleibt möglich."
            />
            
            <LabeledSwitch
              checked={!!value.autoAddToDownloadList}
              onChange={(checked) =>
                setValue((v) => ({
                  ...v,
                  autoAddToDownloadList: checked,
                  autoStartAddedDownloads: checked ? v.autoStartAddedDownloads : false,
                }))
              }
              label="Clipboard-Links in Start-URL übernehmen"
              description="Übernimmt erkannte Chaturbate- und MyFreeCams-Links aus der Zwischenablage automatisch in das Start-URL-Feld. Wenn deaktiviert, werden Clipboard-Links ignoriert."
            />
            
            <LabeledSwitch
              checked={!!value.autoStartAddedDownloads}
              onChange={(checked) =>
                setValue((v) => ({
                  ...v,
                  autoStartAddedDownloads: checked,
                  autoAddToDownloadList: checked ? true : v.autoAddToDownloadList,
                }))
              }
              label="Übernommene Links automatisch starten"
              description="Startet automatisch, sobald ein erkannter Clipboard-Link übernommen wurde oder ein aktivierter Provider ein watched Model als öffentlich online erkennt. Aktiviert automatisch auch die Clipboard-Übernahme."
            />

            <div className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-white/10 dark:bg-white/5">
              <LabeledSwitch
                checked={!!value.useProviderApis}
                onChange={(checked) =>
                  setValue((v) => ({
                    ...v,
                    useProviderApis: checked,
                    useChaturbateApi: checked ? v.useChaturbateApi : false,
                    useMyFreeCamsApi: checked ? v.useMyFreeCamsApi : false,
                  }))
                }
                label="Provider-APIs verwenden"
                description="Master-Schalter für externe Provider-APIs. Wenn deaktiviert, werden keine automatischen API-Abfragen für Online-Status, Profilbilder oder Model-Daten ausgeführt."
              />

              <div
                className={
                  'mt-3 space-y-3 border-t border-gray-200 pt-3 dark:border-white/10 ' +
                  (!value.useProviderApis ? 'opacity-50 pointer-events-none' : '')
                }
              >
                <LabeledSwitch
                  checked={!!value.useChaturbateApi}
                  onChange={(checked) =>
                    setValue((v) => ({
                      ...v,
                      useProviderApis: checked ? true : v.useProviderApis,
                      useChaturbateApi: checked,
                    }))
                  }
                  label="Chaturbate API"
                  description="Aktualisiert Online-Status, Vorschaubilder und Model-Daten über die Chaturbate-API. Wenn Automatisch starten aktiv ist, werden öffentliche watched Models gestartet."
                />

                <LabeledSwitch
                  checked={!!value.useMyFreeCamsApi}
                  onChange={(checked) =>
                    setValue((v) => ({
                      ...v,
                      useProviderApis: checked ? true : v.useProviderApis,
                      useMyFreeCamsApi: checked,
                    }))
                  }
                  label="MyFreeCams API"
                  description="Aktualisiert Online-Status, Vorschaubilder und Model-Daten über usernameLookup. Wenn Automatisch starten aktiv ist, werden öffentliche watched Models gestartet. Bei 'servers busy' pausiert die App MyFreeCams-Anfragen automatisch."
                />
              </div>
            </div>

            {/* ✅ NEU: Auto-Delete kleine Downloads */}
            <div className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-white/10 dark:bg-white/5">
              <LabeledSwitch
                checked={!!value.autoDeleteSmallDownloads}
                onChange={(checked) =>
                  setValue((v) => ({
                    ...v,
                    autoDeleteSmallDownloads: checked,
                    autoDeleteSmallDownloadsBelowMB:
                      v.autoDeleteSmallDownloadsBelowMB ?? 50,
                  }))
                }
                label="Kleine Downloads automatisch löschen"
                description="Löscht fertige Downloads automatisch, wenn die Datei kleiner als die eingestellte Mindestgröße ist."
              />

              <div
                className={
                  'mt-2 grid grid-cols-1 gap-2 sm:grid-cols-12 sm:items-center ' +
                  (!value.autoDeleteSmallDownloads ? 'opacity-50 pointer-events-none' : '')
                }
              >
                <div className="sm:col-span-4">
                  <div className="text-sm font-medium text-gray-900 dark:text-gray-200">Mindestgröße</div>
                  <div className="text-xs text-gray-600 dark:text-gray-300">Alles darunter wird gelöscht.</div>
                </div>

                <div className="sm:col-span-8">
                  <div className="flex items-center justify-end gap-2">
                    <input
                      type="number"
                      min={0}
                      step={1}
                      value={value.autoDeleteSmallDownloadsBelowMB ?? 50}
                      onChange={(e) =>
                        setValue((v) => ({
                          ...v,
                          autoDeleteSmallDownloadsBelowMB: Number(e.target.value || 0),
                        }))
                      }
                      className="h-9 w-32 rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-900 shadow-sm
                                dark:border-white/10 dark:bg-gray-900 dark:text-gray-100"
                    />

                    <span className="shrink-0 text-xs text-gray-600 dark:text-gray-300">MB</span>
                  </div>
                </div>
              </div>

              <div
                className={
                  'mt-3 border-t border-gray-200 pt-3 dark:border-white/10 ' +
                  (!value.autoDeleteSmallDownloads ? 'opacity-50 pointer-events-none' : '')
                }
              >
                <LabeledSwitch
                  checked={!!value.autoDeleteSmallDownloadsKeepFavorites}
                  onChange={(checked) =>
                    setValue((v) => ({
                      ...v,
                      autoDeleteSmallDownloadsKeepFavorites: checked,
                    }))
                  }
                  label="Favoriten nicht automatisch löschen"
                  description="Schützt Downloads von favorisierten Models vor dem automatischen Löschen, auch wenn die Datei kleiner als die Mindestgröße ist."
                />
              </div>
            </div>

            <div className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-white/10 dark:bg-white/5">
              <LabeledSwitch
                checked={!!value.autoDeleteLowRatedDownloads}
                onChange={(checked) =>
                  setValue((v) => ({
                    ...v,
                    autoDeleteLowRatedDownloads: checked,
                    autoDeleteLowRatedDownloadsMaxStars:
                      v.autoDeleteLowRatedDownloadsMaxStars ?? 3,
                  }))
                }
                label="Downloads nach Analyse nach Rating löschen"
                description="Löscht fertige Downloads automatisch nach der Analyse, wenn das Rating höchstens der gewählten Sternezahl entspricht."
              />

              <div
                className={
                  'mt-2 grid grid-cols-1 gap-2 sm:grid-cols-12 sm:items-center ' +
                  (!value.autoDeleteLowRatedDownloads ? 'opacity-50 pointer-events-none' : '')
                }
              >
                <div className="sm:col-span-5">
                  <div className="text-sm font-medium text-gray-900 dark:text-gray-200">
                    Sterne-Grenze
                  </div>
                  <div className="text-xs text-gray-600 dark:text-gray-300">
                    Bis einschließlich dieser Bewertung wird gelöscht.
                  </div>
                </div>

                <div className="sm:col-span-7">
                  <RatingThresholdStars
                    value={value.autoDeleteLowRatedDownloadsMaxStars}
                    disabled={!value.autoDeleteLowRatedDownloads}
                    onChange={(stars) =>
                      setValue((v) => ({
                        ...v,
                        autoDeleteLowRatedDownloadsMaxStars: stars,
                      }))
                    }
                  />
                </div>
              </div>
            </div>

            <div className="mt-5 rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-white/10 dark:bg-white/5">
              <LabeledSwitch
                checked={!!value.trainingRecognitionEnabled}
                onChange={(checked) =>
                  setValue((v) => ({
                    ...v,
                    trainingRecognitionEnabled: checked,
                  }))
                }
                label="Training-Erkennung aktivieren"
                description="Wenn aktiv, werden neue Trainingsbilder automatisch mit YOLO und Scene-KNN analysiert. Wenn deaktiviert, kannst du weiterhin manuell korrigieren und Boxen zeichnen."
              />

              <div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-12 sm:items-center">
                <div className="sm:col-span-9">
                  <div className="text-sm font-medium text-gray-900 dark:text-gray-200">
                    Object Detection Epochs
                  </div>
                  <div className="max-w-4xl text-xs text-gray-600 dark:text-gray-300">
                    Anzahl der Trainingsdurchläufe für die Box-Erkennung. Mehr Epochs können die Erkennung verbessern, dauern aber länger. 60 ist ein guter Standard.
                  </div>
                </div>

                <div className="sm:col-span-3">
                  <div className="flex items-center justify-end gap-2">
                    <input
                      type="number"
                      min={1}
                      max={300}
                      step={1}
                      value={value.trainingDetectorEpochs ?? 60}
                      onChange={(e) =>
                        setValue((v) => ({
                          ...v,
                          trainingDetectorEpochs: Number(e.target.value || 60),
                        }))
                      }
                      className="h-9 w-32 rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-900 shadow-sm
                                dark:border-white/10 dark:bg-gray-900 dark:text-gray-100"
                    />

                    <span className="shrink-0 text-xs text-gray-600 dark:text-gray-300">
                      Epochs
                    </span>
                  </div>
                </div>
              </div>
            </div>

            <LabeledSwitch
              checked={!!value.blurPreviews}
              onChange={(checked) => setValue((v) => ({ ...v, blurPreviews: checked }))}
              label="Vorschaubilder blurren"
              description="Weichzeichnet Vorschaubilder/Teaser (praktisch auf mobilen Geräten oder im öffentlichen Umfeld)."
            />

            <div className="grid grid-cols-1 gap-2 sm:grid-cols-12 sm:items-center">
              <div className="sm:col-span-8">
                <div className="text-sm font-medium text-gray-900 dark:text-gray-200">
                  Teaser abspielen
                </div>
                <div className="text-xs text-gray-600 dark:text-gray-300">
                  Standbild spart Leistung. „Bei Hover (Standard)“: Desktop spielt bei Hover ab, Mobile im Viewport. „Alle“ kann viel CPU ziehen.
                </div>
              </div>

              <div className="sm:col-span-4 sm:flex sm:justify-end">
                <label className="sr-only" htmlFor="teaserPlayback">
                  Teaser abspielen
                </label>
                <select
                  id="teaserPlayback"
                  value={value.teaserPlayback ?? 'hover'}
                  onChange={(e) =>
                    setValue((v) => ({ ...v, teaserPlayback: e.target.value as any }))
                  }
                  className="h-9 w-full rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-900 shadow-sm
                            focus:outline-none focus:ring-2 focus:ring-indigo-500
                            dark:border-white/10 dark:bg-gray-900 dark:text-gray-100 dark:[color-scheme:dark]
                            sm:max-w-56"
                >
                  <option value="still">Standbild</option>
                  <option value="hover">Bei Hover (Standard)</option>
                  <option value="all">Alle</option>
                </select>
              </div>
            </div>

            <LabeledSwitch
              checked={!!value.teaserAudio}
              onChange={(checked) => setValue((v) => ({ ...v, teaserAudio: checked }))}
              label="Teaser mit Ton"
              description="Wenn aktiv, werden Vorschau/Teaser nicht stumm geschaltet."
            />

            <LabeledSwitch
              checked={!!value.enableNotifications}
              onChange={(checked) => setValue((v) => ({ ...v, enableNotifications: checked }))}
              label="Benachrichtigungen"
              description="Wenn aktiv, zeigt das Frontend Toasts (z.B. wenn watched Models online/live gehen oder wenn ein queued Model wieder public wird)."
            />

            <div className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-white/10 dark:bg-white/5">
              <div className="flex items-start justify-between gap-3">
                <div>
                  <div className="text-sm font-semibold text-gray-900 dark:text-white">Speicherplatz-Notbremse</div>
                  <div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
                    Aktiviert automatisch Stop + Autostart-Block bei wenig freiem Speicher (Resume bei +3 GB).
                  </div>
                </div>

                <span
                  className={
                    'inline-flex items-center rounded-full px-2 py-1 text-[11px] font-medium ' +
                    (diskStatus?.emergency
                      ? 'bg-red-100 text-red-800 dark:bg-red-500/20 dark:text-red-200'
                      : 'bg-green-100 text-green-800 dark:bg-green-500/20 dark:text-green-200')
                  }
                  title={diskStatus?.emergency ? 'Notfallbremse greift gerade' : 'OK'}
                >
                  {diskStatus?.emergency ? 'AKTIV' : 'OK'}
                </span>
              </div>

              <div className="mt-3 text-sm text-gray-900 dark:text-gray-200">
                <div>
                  <span className="font-medium">Schwelle:</span>{' '}
                  Pause unter <span className="tabular-nums">{uiPauseGB}</span> GB
                  {' · '}Resume ab{' '}
                  <span className="tabular-nums">
                    {uiResumeGB}
                  </span>{' '}
                  GB
                </div>

                <div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
                  {diskStatus
                    ? `Frei: ${diskStatus.freeBytesHuman}${diskStatus.recordPath ? ` (Pfad: ${diskStatus.recordPath})` : ''}`
                    : 'Status wird geladen…'}
                </div>

                {diskStatus?.emergency && (
                  <div className="mt-2 text-xs text-red-700 dark:text-red-200">
                    Notfallbremse greift: laufende Downloads werden gestoppt und Autostart bleibt gesperrt, bis wieder genug frei ist.
                  </div>
                )}
              </div>
            </div>

            <div className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-white/10 dark:bg-white/5">
              <button
                type="button"
                onClick={() => setAppLogOpen((v) => !v)}
                className="flex w-full items-start justify-between gap-3 text-left"
              >
                <div className="min-w-0">
                  <div className="flex flex-wrap items-center gap-2">
                    <div className="text-sm font-semibold text-gray-900 dark:text-white">
                      Log
                    </div>

                    {appLog?.sizeBytes ? (
                      <span className="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-700 ring-1 ring-gray-200 dark:bg-white/10 dark:text-gray-200 dark:ring-white/10">
                        {Math.ceil(appLog.sizeBytes / 1024)} KB
                      </span>
                    ) : null}
                  </div>

                  <div className="mt-1 truncate text-xs text-gray-600 dark:text-gray-300">
                    Zeigt die aktuelle recorder.log aus dem App-Verzeichnis.
                  </div>
                </div>

                <span
                  className={[
                    'shrink-0 text-sm text-gray-500 transition-transform dark:text-gray-400',
                    appLogOpen ? 'rotate-180' : '',
                  ].join(' ')}
                  aria-hidden="true"
                >
                  ▾
                </span>
              </button>

              {appLogOpen ? (
                <div className="mt-3 border-t border-gray-200 pt-3 dark:border-white/10">
                  <div className="mb-3 flex flex-wrap items-center justify-between gap-2">
                    <div className="min-w-0 text-xs text-gray-600 dark:text-gray-300">
                      {appLog?.path ? (
                        <span className="break-all">
                          Logdatei: {appLog.path}
                        </span>
                      ) : appLogLoading ? (
                        'Logdatei wird geladen…'
                      ) : (
                        'Logdatei noch nicht geladen.'
                      )}
                    </div>

                    <Button
                      variant="secondary"
                      size="sm"
                      disabled={appLogLoading}
                      onClick={() => void loadAppLog()}
                    >
                      {appLogLoading ? 'Lade…' : 'Aktualisieren'}
                    </Button>
                  </div>

                  {appLog?.error ? (
                    <div className="mb-3 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800 dark:border-amber-400/30 dark:bg-amber-500/10 dark:text-amber-100">
                      {appLog.error}
                    </div>
                  ) : null}

                  <pre
                    ref={appLogPreRef}
                    className="max-h-96 overflow-auto whitespace-pre-wrap rounded-xl bg-gray-950 p-3 font-mono text-[11px] leading-relaxed text-gray-100 ring-1 ring-black/10 dark:ring-white/10"
                  >
                    {appLogLoading
                      ? 'Log wird geladen…'
                      : appLog?.log?.trim()
                        ? appLog.log
                        : 'Noch kein Log vorhanden.'}
                  </pre>
                </div>
              ) : null}
            </div>
          </div>
        </SettingsSection>

        {/* Sicherheit */}
        <SettingsSection
          id="security"
          title="Konto-Sicherheit"
          description="Passwort, Zwei-Faktor-Authentifizierung und Passkeys getrennt von den Recorder-Einstellungen verwalten."
          icon="🛡️"
          defaultOpen={false}
          actions={
            <>
              {statusPill(!!authStatus?.totpEnabled, '2FA aktiv', '2FA aus')}
              {statusPill(!!authStatus?.passkeyConfigured, 'Passkey aktiv', 'Kein Passkey')}
            </>
          }
        >
          <div className="space-y-4">
            <div className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-white/10 dark:bg-white/5">
              <div className="flex items-start justify-between gap-3">
                <div>
                  <div className="text-sm font-semibold text-gray-900 dark:text-white">
                    Passwort ändern
                  </div>
                  <div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
                    Ändert das Login-Passwort. Andere Sessions werden abgemeldet.
                  </div>
                </div>
              </div>

              <div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-3">
                <input
                  type="password"
                  value={currentPassword}
                  onChange={(e) => setCurrentPassword(e.target.value)}
                  placeholder="Aktuelles Passwort"
                  autoComplete="current-password"
                  disabled={securityBusy}
                  className="rounded-lg bg-white px-3 py-2 text-sm text-gray-900 ring-1 ring-gray-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-white/10 dark:text-white dark:ring-white/10"
                />

                <input
                  type="password"
                  value={newPassword}
                  onChange={(e) => setNewPassword(e.target.value)}
                  placeholder="Neues Passwort"
                  autoComplete="new-password"
                  disabled={securityBusy}
                  className="rounded-lg bg-white px-3 py-2 text-sm text-gray-900 ring-1 ring-gray-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-white/10 dark:text-white dark:ring-white/10"
                />

                <input
                  type="password"
                  value={newPasswordRepeat}
                  onChange={(e) => setNewPasswordRepeat(e.target.value)}
                  placeholder="Neues Passwort wiederholen"
                  autoComplete="new-password"
                  disabled={securityBusy}
                  className="rounded-lg bg-white px-3 py-2 text-sm text-gray-900 ring-1 ring-gray-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-white/10 dark:text-white dark:ring-white/10"
                />
              </div>

              <div className="mt-3 flex justify-end">
                <Button
                  variant="primary"
                  disabled={securityBusy}
                  onClick={() => void changePassword()}
                >
                  Passwort speichern
                </Button>
              </div>
            </div>

            <div className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-white/10 dark:bg-white/5">
              <div className="flex items-start justify-between gap-3">
                <div>
                  <div className="text-sm font-semibold text-gray-900 dark:text-white">
                    Zwei-Faktor-Authentifizierung
                  </div>
                  <div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
                    Authenticator-App als zweiter Faktor. Einzelne Geräte können bei TOTP nicht getrennt erkannt werden.
                  </div>
                </div>

                {statusPill(!!authStatus?.totpEnabled, 'Aktiv', 'Aus')}
              </div>

              <div className="mt-3 space-y-2">
                {authStatus?.totpDevice ? (
                  <SecurityDeviceRow
                    icon="🔐"
                    title={authStatus.totpDevice.label || 'Authenticator-App'}
                    subtitle={
                      authStatus?.totpEnabled
                        ? '2FA ist aktiv und wird beim Passwort-Login abgefragt.'
                        : '2FA ist eingerichtet, aber aktuell nicht aktiv.'
                    }
                    meta={`Eingerichtet: ${formatSecurityDate(authStatus.totpDevice.configuredAt)}`}
                    badge={authStatus?.totpEnabled ? 'TOTP' : 'Inaktiv'}
                  />
                ) : (
                  <div className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200">
                    2FA ist noch nicht eingerichtet. Die Einrichtung erfolgt aktuell beim nächsten Login-Setup.
                  </div>
                )}
              </div>

              {authStatus?.totpConfigured || authStatus?.totpEnabled ? (
                <>
                  <div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
                    <input
                      type="password"
                      value={disable2FAPassword}
                      onChange={(e) => {
                        setDisable2FAPassword(e.target.value)
                        setDisable2FAFieldError((cur) => ({ ...cur, password: undefined }))
                      }}
                      placeholder="Aktuelles Passwort"
                      autoComplete="current-password"
                      disabled={securityBusy}
                      className={[
                        'rounded-lg bg-white px-3 py-2 text-sm text-gray-900 ring-1 focus:outline-none focus:ring-2 dark:bg-white/10 dark:text-white',
                        disable2FAFieldError?.password
                          ? 'ring-red-300 focus:ring-red-500 dark:ring-red-400/50'
                          : 'ring-gray-200 focus:ring-indigo-500 dark:ring-white/10',
                      ].join(' ')}
                    />

                    <input
                      value={disable2FACode}
                      onChange={(e) => {
                        setDisable2FACode(e.target.value.replace(/\D/g, '').slice(0, 6))
                        setDisable2FAFieldError((cur) => ({ ...cur, code: undefined }))
                      }}
                      placeholder="2FA-Code"
                      inputMode="numeric"
                      autoComplete="one-time-code"
                      disabled={securityBusy}
                      className={[
                        'rounded-lg bg-white px-3 py-2 text-sm text-gray-900 ring-1 focus:outline-none focus:ring-2 dark:bg-white/10 dark:text-white',
                        disable2FAFieldError?.code
                          ? 'ring-red-300 focus:ring-red-500 dark:ring-red-400/50'
                          : 'ring-gray-200 focus:ring-indigo-500 dark:ring-white/10',
                      ].join(' ')}
                    />
                  </div>

                  {disable2FAFieldError?.password || disable2FAFieldError?.code ? (
                    <div className="mt-2 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700 dark:border-red-500/30 dark:bg-red-500/10 dark:text-red-200">
                      {disable2FAFieldError.password || disable2FAFieldError.code}
                    </div>
                  ) : (
                    <div className="mt-2 text-xs text-gray-500 dark:text-gray-400">
                      Zum Deaktivieren brauchst du dein aktuelles Passwort
                      {authStatus?.totpEnabled ? ' und einen gültigen 2FA-Code.' : '.'}
                    </div>
                  )}

                  <div className="mt-3 flex justify-end">
                    <Button
                      variant="primary"
                      color="red"
                      disabled={securityBusy}
                      onClick={() => void disable2FA()}
                    >
                      2FA deaktivieren
                    </Button>
                  </div>
                </>
              ) : null}
            </div>

            <div className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-white/10 dark:bg-white/5">
              <div className="flex items-start justify-between gap-3">
                <div>
                  <div className="text-sm font-semibold text-gray-900 dark:text-white">
                    Passkeys
                  </div>
                  <div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
                    Anmeldung ohne Passwort über Face ID, Fingerabdruck, Windows Hello oder Sicherheitsschlüssel.
                  </div>
                </div>

                {statusPill(!!authStatus?.passkeyConfigured, 'Aktiv', 'Keine')}
              </div>

              <div className="mt-3 space-y-2">
                {(authStatus?.passkeys ?? []).length > 0 ? (
                  (authStatus?.passkeys ?? []).map((pk, index) => (
                    <SecurityDeviceRow
                      key={pk.id || index}
                      icon="🔑"
                      title={pk.label || `Passkey ${index + 1}`}
                      subtitle={
                        pk.backupEligible
                          ? pk.backupState
                            ? 'Synchronisierter Passkey'
                            : 'Passkey kann synchronisiert werden'
                          : 'Gerätegebundener Passkey oder Sicherheitsschlüssel'
                      }
                      meta={[
                        `Erstellt: ${formatSecurityDate(pk.createdAt)}`,
                        `Zuletzt genutzt: ${formatSecurityDate(pk.lastUsedAt)}`,
                        typeof pk.signCount === 'number' ? `SignCount: ${pk.signCount}` : '',
                      ].filter(Boolean).join(' · ')}
                      badge="Passkey"
                      actions={
                        <Button
                          variant="secondary"
                          color="red"
                          size="sm"
                          disabled={securityBusy}
                          onClick={() => void deletePasskey(pk)}
                        >
                          Löschen
                        </Button>
                      }
                    />
                  ))
                ) : (
                  <div className="rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs text-gray-600 dark:border-white/10 dark:bg-gray-950/40 dark:text-gray-300">
                    Noch kein Passkey gespeichert.
                  </div>
                )}
              </div>

              {!passkeySupported ? (
                <div className="mt-3 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200">
                  Dein Browser oder diese Verbindung unterstützt Passkeys aktuell nicht.
                </div>
              ) : (
                <div className="mt-3 space-y-2">
                  {passkeyFeedback ? (
                    <div
                      className={[
                        'whitespace-pre-wrap rounded-lg border px-3 py-2 text-xs',
                        passkeyFeedback.kind === 'success'
                          ? 'border-green-200 bg-green-50 text-green-800 dark:border-green-500/30 dark:bg-green-500/10 dark:text-green-200'
                          : passkeyFeedback.kind === 'error'
                            ? 'border-red-200 bg-red-50 text-red-700 dark:border-red-500/30 dark:bg-red-500/10 dark:text-red-200'
                            : 'border-blue-200 bg-blue-50 text-blue-800 dark:border-blue-500/30 dark:bg-blue-500/10 dark:text-blue-200',
                      ].join(' ')}
                    >
                      {passkeyFeedback.kind === 'loading' ? '⏳ ' : null}
                      {passkeyFeedback.kind === 'success' ? '✅ ' : null}
                      {passkeyFeedback.kind === 'error' ? '⚠️ ' : null}
                      {passkeyFeedback.text}
                    </div>
                  ) : null}

                  <div className="flex flex-col gap-2 sm:flex-row sm:justify-end">
                    <Button
                      variant="secondary"
                      disabled={securityBusy}
                      onClick={() => void registerSettingsPasskey()}
                    >
                      {securityBusy ? 'Passkey wird eingerichtet…' : 'Passkey hinzufügen'}
                    </Button>
                  </div>
                </div>
              )}
            </div>
          </div>
        </SettingsSection>
      </div>
    </Card>
  )
}
