(() => {
  'use strict';
  const root = document.getElementById('backupper-app');
  if (!root) return;
  const content = document.getElementById('bu-content');
  const health = document.getElementById('bu-health');
  const toast = document.getElementById('bu-toast');
  const tooltip = document.getElementById('bu-tooltip');
  const sourceRoots = ['/mnt/user','/mnt/disks','/mnt/remotes','/boot'].map(path=>({name:path,path}));
  const repositoryRoots = ['/mnt/user','/mnt/disks','/mnt/remotes'].map(path=>({name:path,path}));
  const state = { config: null, runs: [], view: 'dashboard', snapshots: [], selectedSnapshot: null, snapshotRepo: null, files: [], browserPath: '/', restoreIncludes: [], backupSelections: [], workloadSelections: [], workloads: [], workloadAvailable: true, workloadError: '', sourceBrowserPath: '/', sourceDirectories: sourceRoots, sourceBrowserError: '', repositoryBrowserPath: '/', repositoryDirectories: repositoryRoots, repositoryBrowserError: '' };

  const fieldHelp = {
    name: 'Frei wählbarer Anzeigename. Beispiel: Appdata täglich oder VM Home Assistant.',
    type: 'Art der zu sichernden Daten. Beispiel: appdata für Containerdaten, vm für virtuelle Maschinen oder flash für den Unraid-USB-Stick.',
    repositoryId: 'Speicherziel für diesen Vorgang. Jeder Backup-Job verwendet ein eigenes Restic-Repository.',
    scheduleMode: 'Legt fest, an welchen Tagen das Backup startet. Beispiel: täglich, Montag bis Freitag oder ausgewählte Wochentage.',
    scheduleTime: 'Uhrzeit für den automatischen Start. Beispiel: 02:30 startet das Backup nachts um halb drei.',
    scheduleDays: 'Wähle mindestens einen Wochentag. Beispiel: Montag, Mittwoch und Sonntag jeweils zur eingestellten Uhrzeit.',
    consistency: 'Live sichert ohne Unterbrechung, kann aber nur crash-konsistent sein. Controlled stop beendet Container oder VM vor dem Backup und startet sie danach wieder.',
    compression: 'Restic-Kompression. auto ist empfohlen; off spart CPU; max spart eher Speicher, benötigt aber mehr CPU-Zeit.',
    sources: 'Optionale manuelle Quellen, eine pro Zeile. Ordner wählst du einfacher über die Checkbox-Liste. Docker- oder VM-ID mit @ voranstellen, z. B. @HomeAssistant.',
    excludes: 'Muster für Dateien, die nicht gesichert werden. Ein Muster pro Zeile. Beispiele: *.tmp, cache/** oder /mnt/user/appdata/plex/Cache/**.',
    shutdownSecs: 'Maximale Wartezeit für einen kontrollierten Stopp. Beispiel: 120 wartet bis zu zwei Minuten; danach wird das Backup ohne Force-Stop abgebrochen.',
    keepWithinDays: 'Behält alle Snapshots, die innerhalb dieser Anzahl Tage erstellt wurden. Beispiel: 30 entfernt ältere Snapshots beim nächsten erfolgreichen Backup.',
    keepDaily: 'Zusätzliche tägliche Stände außerhalb der Altersfrist. 0 deaktiviert diese Zusatzregel.',
    keepWeekly: 'Zusätzliche wöchentliche Stände außerhalb der Altersfrist. 0 deaktiviert diese Zusatzregel.',
    keepMonthly: 'Zusätzliche monatliche Stände außerhalb der Altersfrist. 0 deaktiviert diese Zusatzregel.',
    enabled: 'Aktiviert automatische geplante Läufe. Deaktivierte Jobs können weiterhin gespeichert, aber nicht automatisch ausgeführt werden.',
    location: 'Restic-Ziel. Lokal: /mnt/user/backups/job1. SFTP: user@server:/backup/job1. Bei SMB/NFS mit Mount wird hier der Mount-Pfad verwendet.',
    passwordRef: 'Interne ID des verschlüsselt gespeicherten Restic-Passworts. Beispiel: repo-password-appdata. Keine Leerzeichen verwenden.',
    password: 'Passwort zur Restic-Verschlüsselung. Beispiel: eine lange zufällige Passphrase. Leer lassen, wenn ein vorhandenes Passwort unverändert bleiben soll.',
    managed: 'Wenn aktiv, mountet URBM SMB/NFS vor dem Job selbst. Wenn aus, muss der angegebene Pfad bereits durch Unraid oder Unassigned Devices gemountet sein.',
    remote: 'Adresse der Netzwerkfreigabe. SMB-Beispiel: //192.168.1.20/backup. NFS-Beispiel: 192.168.1.20:/volume1/backup.',
    mountPoint: 'Lokaler Einhängepunkt auf Unraid. Beispiel: /mnt/remotes/backup-nas. Erlaubt sind Pfade unter /mnt/remotes oder /mnt/disks.',
    credentialRef: 'Interne ID für SMB-Zugangsdaten oder einen SFTP-Schlüssel. Beispiel: nas-login oder sftp-backup-key.',
    credential: 'SMB-Beispiel: username=backup und password=geheim auf getrennten Zeilen. Bei SFTP hier den kompletten privaten SSH-Schlüssel einfügen.',
    knownHostsPath: 'Datei mit dem geprüften SSH-Hostschlüssel. Beispiel: /root/.ssh/known_hosts. StrictHostKeyChecking ist immer aktiv.',
    snapshotId: 'Eindeutige ID des Sicherungsstands. Beispiel: 7f3a91cd. Sie kann im Bereich Snapshots über Browse ausgewählt werden.',
    includes: 'Nur diese Pfade werden wiederhergestellt, einer pro Zeile. Beispiel: /mnt/user/Documents/Rechnung.pdf oder /mnt/user/Documents.',
    target: 'Zielverzeichnis der Wiederherstellung. Sicheres Beispiel: /mnt/user/backupper-restores/restore-123. Standardmäßig wird nicht direkt über Originaldaten geschrieben.',
    inPlace: 'Erlaubt Wiederherstellung direkt an einen produktiven Zielpfad. Nur aktivieren, wenn vorhandene Dateien bewusst ersetzt werden sollen.',
    confirmed: 'Zusätzliche Bestätigung für einen In-place-Restore. Ohne diese Bestätigung wird ein direkter Restore blockiert.',
    resticPath: 'Pfad zur vom Plugin bereitgestellten Restic-Datei. Standard: /usr/local/libexec/backupper/restic. Normalerweise nicht ändern.',
    restoreRoot: 'Basisverzeichnis für sichere Staging-Restores. Beispiel: /mnt/user/backupper-restores.',
    persistentLogDir: 'Optionales Verzeichnis für dauerhafte Detailprotokolle. Beispiel: /mnt/user/system/backupper-logs. Leer lassen, um Flash-Schreibzugriffe zu vermeiden.',
    catchUp: 'Wie viele Stunden nach einem verpassten Termin ein Job beim Boot nachgeholt werden darf. Beispiel: 24 holt einen innerhalb des letzten Tages verpassten Lauf nach.',
    checkCron: 'Zeitplan für vollständige Repository-Prüfungen. Beispiel: 0 3 1 * * prüft am ersten Tag jedes Monats um 03:00 Uhr.',
    pruneCron: 'Zeitplan zum Freigeben nicht mehr benötigter Restic-Daten. Beispiel: 0 4 * * 0 startet sonntags um 04:00 Uhr.',
    url: 'Basisadresse deines Gotify-Servers. Beispiel: https://gotify.example.de oder http://192.168.1.20:8080.',
    tokenRef: 'Interne ID für das verschlüsselt gespeicherte Gotify-App-Token. Beispiel: gotify-token-main.',
    token: 'App-Token aus Gotify unter Apps. Das Token wird verschlüsselt gespeichert und nicht erneut angezeigt.',
    events: 'Legt fest, bei welchen Ergebnissen dieser Kanal informiert. Mindestens ein Ereignis muss aktiv sein.',
  };

  const actionHelp = {
    'new-job': 'Öffnet den Assistenten für einen neuen Backup-Job. Beispiel: tägliches Appdata-Backup.',
    'submit-job': 'Prüft und speichert diesen Backup-Job.',
    'new-repo': 'Legt ein neues, isoliertes Restic-Speicherziel an.',
    'submit-repo': 'Speichert Ziel und Zugangsdaten. Das Repository wird dadurch noch nicht automatisch initialisiert.',
    'init-repo': 'Initialisiert am konfigurierten Ziel ein neues, leeres Restic-Repository. Beispiel: nach dem erstmaligen Speichern eines neuen Backup-Ziels. Nicht für ein bereits vorhandenes Repository verwenden.',
    'load-snapshots': 'Lädt alle Sicherungsstände des ausgewählten Repositorys.',
    'restore-selected': 'Übernimmt die markierten Dateien oder Ordner in den Restore-Assistenten.',
    'submit-restore': 'Stellt den Restore in die globale Warteschlange; Backups und Restores laufen nacheinander.',
    'submit-settings': 'Speichert die globalen URBM-Einstellungen.',
    'submit-unraid-notify': 'Speichert native Unraid-Benachrichtigungen. Die Zustellung erfolgt über die in Unraid eingerichteten Notification Agents.',
    'submit-gotify-notify': 'Speichert den Gotify-Kanal und das App-Token verschlüsselt.',
    'refresh': 'Lädt Laufstatus und Historie erneut vom URBM-Daemon.',
    'cancel-form': 'Verwirft ungespeicherte Eingaben und kehrt zur Übersicht zurück.',
    'browser-up': 'Öffnet im Snapshot-Browser das übergeordnete Verzeichnis.',
    'source-up': 'Öffnet im Ordner-Browser das übergeordnete Verzeichnis.',
    'select-current-source': 'Wählt den aktuell geöffneten Ordner vollständig als Backup-Quelle aus.',
    'repository-up': 'Öffnet im lokalen Repository-Browser das übergeordnete Verzeichnis.',
    'select-repository-folder': 'Übernimmt den aktuell geöffneten Ordner als lokalen Repository-Pfad.',
    'select-current-restore': 'Wählt den aktuell geöffneten Snapshot-Ordner vollständig für die Wiederherstellung aus.',
  };

  const esc = (value) => String(value ?? '').replace(/[&<>'"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c]));
  const api = async (path, options = {}) => {
    const response = await fetch(`${root.dataset.api}?path=${encodeURIComponent(path)}`, {
      ...options,
      headers: {'Content-Type':'application/json', 'X-CSRF-Token':window.BACKUPPER_CSRF || window.csrf_token || '', ...(options.headers || {})}
    });
    const text = await response.text();
    let body = null;
    try { body = text ? JSON.parse(text) : null; } catch (_) { body = {error:text}; }
    if (!response.ok) throw new Error(body?.error || `Request failed (${response.status})`);
    return body;
  };
  const notify = (message, error = false) => {
    toast.textContent = message; toast.className = `show${error ? ' error' : ''}`;
    clearTimeout(notify.timer); notify.timer = setTimeout(() => toast.className = '', 4500);
  };
  const id = (prefix) => `${prefix}-${crypto.randomUUID()}`;
  const repoName = (repoId) => state.config.repositories.find(r => r.id === repoId)?.name || repoId;
  const volatileLocation = (repo) => repo.type === 'local' && (/^\/tmp(?:\/|$)/.test(repo.location) || /^\/run(?:\/|$)/.test(repo.location));
  const tipAttr = (text) => text ? ` data-tooltip="${esc(text)}"` : '';
  const actionTooltip = (action, label) => {
    const [name] = action.split(':');
    if (actionHelp[name]) return actionHelp[name];
    const dynamic = {
      'run-job': 'Startet diesen Backup-Job sofort. Beispiel: manuelles Backup vor einem Unraid-Update.',
      'edit-job': 'Öffnet die Einstellungen dieses Backup-Jobs zum Bearbeiten.',
      'duplicate-job': 'Erstellt eine deaktivierte Kopie als Vorlage. Danach Name, Repository und Quellen anpassen.',
      'delete-job': 'Löscht die Job-Konfiguration. Vorhandene Restic-Snapshots im Repository bleiben erhalten.',
      'test-repo': 'Prüft Erreichbarkeit, Passwort und Restic-Zugriff, ohne ein Backup zu starten.',
      'init-repo': 'Erstellt die Restic-Struktur am Speicherziel. Diese Aktion ist einmalig für ein neues, leeres Ziel erforderlich.',
      'repo-snapshots': 'Wechselt zu den Sicherungsständen dieses Repositorys.',
      'maint': action.includes(':check:') ? 'Prüft Struktur und Integrität des Restic-Repositorys.' : 'Entfernt nach Retention nicht mehr benötigte Datenblöcke und gibt Speicher frei.',
      'edit-repo': 'Öffnet Ziel-, Mount- und Zugangseinstellungen dieses Repositorys.',
      'delete-repo': 'Löscht nur die URBM-Konfiguration. Die Backupdaten am Speicherziel werden nicht gelöscht.',
      'browse': 'Öffnet den Dateibaum dieses Snapshots, um einzelne Dateien oder Ordner auszuwählen.',
      'open-dir': 'Öffnet dieses Verzeichnis innerhalb des schreibgeschützten Snapshots.',
      'open-source-dir': 'Öffnet diesen Ordner, um darin weitere Unterordner auszuwählen.',
      'open-repository-dir': 'Öffnet diesen lokalen Ordner, um ihn oder einen Unterordner als Repository-Ziel auszuwählen.',
      'cancel-run': 'Bricht die laufende oder wartende Aufgabe ab. Cleanup und Neustart gestoppter Workloads werden trotzdem versucht.',
    };
    return dynamic[name] || `${label} ausführen.`;
  };
  const status = (value) => `<span class="bu-status ${esc(value)}" tabindex="0"${tipAttr(`Status dieses Vorgangs. Beispiel: success = erfolgreich, warning = mit Warnung, failed = fehlgeschlagen, running = läuft.`)}>${esc(value)}</span>`;
  const button = (label, action, kind = '') => `<button class="bu-button ${kind}" type="button" data-action="${esc(action)}"${tipAttr(actionTooltip(action, label))}>${esc(label)}</button>`;
  const disabledButton = (label, help) => `<button class="bu-button" type="button" disabled${tipAttr(help)}>${esc(label)}</button>`;
  const actionGroup = (buttons) => `<div class="bu-actions bu-table-actions">${buttons}</div>`;
  const formatBytes = (value) => {
    const units = ['B','KiB','MiB','GiB','TiB']; let size = Number(value)||0; let unit = 0;
    while (size >= 1024 && unit < units.length-1) { size /= 1024; unit++; }
    return `${size >= 10 || unit === 0 ? size.toFixed(0) : size.toFixed(1)} ${units[unit]}`;
  };
  const formatEta = value => {
    let seconds=Math.max(0,Math.round(Number(value)||0));
    const hours=Math.floor(seconds/3600); seconds%=3600;
    const minutes=Math.floor(seconds/60); seconds%=60;
    return hours?`${hours}h ${minutes}m`:minutes?`${minutes}m ${seconds}s`:`${seconds}s`;
  };
  const weekDays = [{value:'1',short:'Mo',name:'Montag'},{value:'2',short:'Di',name:'Dienstag'},{value:'3',short:'Mi',name:'Mittwoch'},{value:'4',short:'Do',name:'Donnerstag'},{value:'5',short:'Fr',name:'Freitag'},{value:'6',short:'Sa',name:'Samstag'},{value:'0',short:'So',name:'Sonntag'}];
  function parseSchedule(cron = '0 2 * * *') {
    const match = String(cron).trim().match(/^(\d{1,2})\s+(\d{1,2})\s+\*\s+\*\s+(.+)$/);
    if (!match || Number(match[1]) > 59 || Number(match[2]) > 23) return {mode:'custom',time:'02:00',days:[],cron};
    const time = `${String(match[2]).padStart(2,'0')}:${String(match[1]).padStart(2,'0')}`;
    if (match[3] === '*') return {mode:'daily',time,days:[],cron};
    if (match[3] === '1-5') return {mode:'weekdays',time,days:[],cron};
    const days = match[3].split(',');
    if (days.length && days.every(day=>weekDays.some(item=>item.value===day))) return {mode:'selected',time,days:[...new Set(days)],cron};
    return {mode:'custom',time,days:[],cron};
  }
  function scheduleCron(form) {
    const mode = form.get('scheduleMode');
    if (mode === 'custom') return form.get('customCron');
    const [hour, minute] = String(form.get('scheduleTime') || '02:00').split(':');
    const dayField = mode === 'daily' ? '*' : mode === 'weekdays' ? '1-5' : form.getAll('scheduleDays').join(',');
    if (mode === 'selected' && !dayField) throw new Error('Select at least one weekday for the backup schedule');
    return `${Number(minute)} ${Number(hour)} * * ${dayField}`;
  }
  function scheduleLabel(cron) {
    const schedule = parseSchedule(cron);
    if (schedule.mode === 'custom') return cron || 'Manual';
    if (schedule.mode === 'daily') return `Täglich um ${schedule.time} Uhr`;
    if (schedule.mode === 'weekdays') return `Mo–Fr um ${schedule.time} Uhr`;
    const labels = weekDays.filter(day=>schedule.days.includes(day.value)).map(day=>day.short).join(', ');
    return `${labels} um ${schedule.time} Uhr`;
  }
  function scheduleFields(job = {}) {
    const schedule = parseSchedule(job.schedule?.cron || '0 2 * * *');
    const modes = [['daily','Täglich'],['weekdays','Montag bis Freitag'],['selected','Ausgewählte Wochentage']];
    if (schedule.mode === 'custom') modes.push(['custom','Bestehenden Cron-Zeitplan beibehalten']);
    return `<fieldset class="wide bu-fieldset bu-schedule"><legend>Backup schedule</legend><div class="bu-schedule-grid"><label>Days<select name="scheduleMode">${modes.map(([value,label])=>`<option value="${value}" ${value===schedule.mode?'selected':''}>${label}</option>`).join('')}</select></label><label>Time<input name="scheduleTime" type="time" required value="${esc(schedule.time)}"></label></div><div class="bu-weekdays" ${schedule.mode==='selected'?'':'hidden'}><span>Weekdays</span>${weekDays.map(day=>`<label><input type="checkbox" name="scheduleDays" value="${day.value}" ${schedule.days.includes(day.value)?'checked':''}><span>${day.short}</span></label>`).join('')}</div>${schedule.mode==='custom'?`<label class="bu-custom-cron">Existing schedule<input name="customCron" readonly value="${esc(schedule.cron)}"></label>`:''}</fieldset>`;
  }

  function lineChart(runs, field, formatter, emptyText) {
    const points = runs.map(run=>({run,value:Number(run[field] || (field==='bytesProcessed'?run.bytesAdded:run.filesNew) || 0)}));
    if (!points.length || points.every(point=>point.value===0)) return `<div class="bu-chart-empty">${esc(emptyText)}</div>`;
    const width=720, height=220, left=54, right=18, top=18, bottom=38, innerW=width-left-right, innerH=height-top-bottom;
    const max=Math.max(...points.map(point=>point.value),1);
    const coords=points.map((point,index)=>({ ...point, x:left+(points.length===1?innerW/2:index*innerW/(points.length-1)), y:top+innerH-(point.value/max)*innerH }));
    const polyline=coords.map(point=>`${point.x.toFixed(1)},${point.y.toFixed(1)}`).join(' ');
    const area=`${left},${top+innerH} ${polyline} ${left+innerW},${top+innerH}`;
    const grid=[0,.25,.5,.75,1].map(ratio=>{const y=top+innerH-ratio*innerH; return `<line x1="${left}" y1="${y}" x2="${left+innerW}" y2="${y}"/><text x="${left-8}" y="${y+4}" text-anchor="end">${esc(formatter(max*ratio))}</text>`;}).join('');
    const dots=coords.map(point=>{const job=state.config.jobs.find(item=>item.id===point.run.jobId)?.name||point.run.jobId; const date=new Date(point.run.finishedAt||point.run.createdAt).toLocaleString(); return `<circle cx="${point.x}" cy="${point.y}" r="4"><title>${esc(`${date} · ${job} · ${formatter(point.value)}`)}</title></circle>`;}).join('');
    const first=new Date(points[0].run.finishedAt||points[0].run.createdAt).toLocaleDateString();
    const last=new Date(points.at(-1).run.finishedAt||points.at(-1).run.createdAt).toLocaleDateString();
    return `<svg class="bu-chart" viewBox="0 0 ${width} ${height}" role="img"><g class="bu-chart-grid">${grid}</g><polygon class="bu-chart-area" points="${area}"/><polyline class="bu-chart-line" points="${polyline}"/><g class="bu-chart-dots">${dots}</g><text class="bu-chart-date" x="${left}" y="${height-8}">${esc(first)}</text><text class="bu-chart-date" x="${left+innerW}" y="${height-8}" text-anchor="end">${esc(last)}</text></svg>`;
  }

  function setTooltip(element, text) {
    if (!element || !text || element.dataset.tooltip) return;
    element.dataset.tooltip = text;
    if (!['INPUT','SELECT','TEXTAREA','BUTTON'].includes(element.tagName) && !element.hasAttribute('tabindex')) element.tabIndex = 0;
  }

  function enhanceTooltips(container = root) {
    container.querySelectorAll('.bu-form [name]').forEach(control => {
      const help = fieldHelp[control.name];
      if (help) {
        setTooltip(control, help);
        setTooltip(control.closest('label'), help);
      }
    });
    const dashboardHelp = {
      'Enabled jobs': 'Anzahl aktivierter Backup-Jobs, die nach Zeitplan laufen dürfen. Beispiel: 3 bedeutet drei aktive Sicherungspläne.',
      'Repositories': 'Anzahl konfigurierter Restic-Speicherziele. Im MVP gehört jedes Repository genau zu einem Job.',
      'Queued / running': 'Aufgaben in der Warteschlange oder in Ausführung. URBM führt immer nur eine Aufgabe gleichzeitig aus.',
      'Recent failures': 'Fehlgeschlagene Läufe in der jüngsten Historie. Details stehen unter Activity.',
    };
    container.querySelectorAll('.bu-card .bu-muted').forEach(element => setTooltip(element.closest('.bu-card') || element, dashboardHelp[element.textContent.trim()]));
    const headerHelp = {
      Job: 'Name und interne ID des Backup-Jobs.', Type: 'Art der gesicherten Daten.', Repository: 'Verwendetes Restic-Speicherziel.', Schedule: 'Wochentage und Uhrzeit des automatischen Backups.', Enabled: 'Zeigt, ob der Job automatisch laufen darf.', Actions: 'Verfügbare Aktionen für diesen Eintrag.', Name: 'Anzeigename und interne ID.', Location: 'Pfad oder Adresse des Restic-Ziels.', Mount: 'Zeigt, ob URBM das Netzwerkziel selbst mountet.', Snapshot: 'Eindeutiger Sicherungsstand.', Time: 'Erstellungszeitpunkt des Snapshots.', Host: 'Hostname des Systems, das den Snapshot erstellt hat.', Paths: 'Im Snapshot enthaltene Quellpfade.', Task: 'Art und Ziel der Aufgabe.', Status: 'Aktueller oder endgültiger Laufstatus.', Created: 'Zeitpunkt, an dem die Aufgabe eingereiht wurde.', Message: 'Ergebnis oder Fehlerbeschreibung.', Size: 'Gespeicherte Dateigröße im Snapshot.'
    };
    container.querySelectorAll('.bu-table th').forEach(th => setTooltip(th, headerHelp[th.textContent.trim()] || 'Informationen zu dieser Tabellenspalte.'));
    const snapshotSelect = container.querySelector('#bu-snapshot-repo');
    setTooltip(snapshotSelect, 'Repository auswählen, dessen Snapshots geladen werden. Beispiel: Appdata Backup NAS.');
    container.querySelectorAll('.bu-file-select').forEach(box => setTooltip(box, `Für den Restore auswählen. Beispielpfad: ${box.value}`));
    const sectionHelp = {
      'Backup Jobs': 'Sicherungspläne verbinden Quellen, Zeitplan und genau ein Restic-Repository.',
      'Repositories': 'Speicherziele und verschlüsselte Zugangsdaten für Restic.',
      'Snapshots': 'Unveränderliche Sicherungsstände, die durchsucht und wiederhergestellt werden können.',
      'Restore': 'Wiederherstellung ausgewählter Dateien oder Ordner. Staging ist die sichere Standardmethode.',
      'Activity': 'Globale Warteschlange und Historie aller URBM-Aufgaben.',
      'Settings': 'Globale Pfade sowie Zeitpläne für Check, Prune und Nachholläufe.',
      'Unraid notifications': 'Native Meldungen im Unraid-Webinterface und über alle unter Einstellungen > Benachrichtigungen aktivierten Agenten.',
      'Gotify notification': 'Optionaler direkter Push-Kanal zu deinem eigenen Gotify-Server.',
      'Recent activity': 'Die zuletzt gestarteten Backup-, Restore- und Wartungsvorgänge.',
    };
    container.querySelectorAll('h2, h3').forEach(heading => setTooltip(heading, sectionHelp[heading.textContent.trim()]));
  }

  let tooltipTimer = null;
  let tooltipTarget = null;
  function hideTooltip() {
    clearTimeout(tooltipTimer); tooltipTimer = null; tooltipTarget = null;
    tooltip.classList.remove('visible', 'below'); tooltip.setAttribute('aria-hidden', 'true');
  }
  function placeTooltip(target) {
    const rect = target.getBoundingClientRect();
    tooltip.classList.add('visible');
    tooltip.style.left = '0px'; tooltip.style.top = '0px';
    const box = tooltip.getBoundingClientRect();
    const gap = 10;
    let left = Math.max(10, Math.min(rect.left + rect.width / 2 - box.width / 2, window.innerWidth - box.width - 10));
    let top = rect.top - box.height - gap;
    const below = top < 8;
    if (below) top = Math.min(window.innerHeight - box.height - 8, rect.bottom + gap);
    tooltip.classList.toggle('below', below);
    tooltip.style.setProperty('--bu-tooltip-arrow', `${Math.max(14, Math.min(box.width - 20, rect.left + rect.width / 2 - left - 6))}px`);
    tooltip.style.left = `${left}px`; tooltip.style.top = `${top}px`;
  }
  function showTooltip(target, delay = 1200) {
    const text = target?.dataset?.tooltip;
    if (!text) return;
    clearTimeout(tooltipTimer); tooltipTarget = target;
    tooltipTimer = setTimeout(() => {
      if (tooltipTarget !== target || !document.contains(target)) return;
      tooltip.textContent = text; tooltip.setAttribute('aria-hidden', 'false'); placeTooltip(target);
    }, delay);
  }

  async function load() {
    try {
      const [config, runs, daemon] = await Promise.all([api('/v1/config'), api('/v1/runs'), api('/v1/health')]);
      state.config = config; state.runs = runs;
      health.textContent = `Daemon online (${daemon.version || 'unknown'})`; health.className = 'bu-health ok'; render();
    } catch (error) {
      health.textContent = 'Daemon unavailable'; health.className = 'bu-health bad';
      content.innerHTML = `<div class="bu-card"><h2>Connection failed</h2><p>${esc(error.message)}</p></div>`;
    }
  }

  function render() {
    const views = {dashboard:renderDashboard, jobs:renderJobs, repositories:renderRepositories, snapshots:renderSnapshots, restore:renderRestore, activity:renderActivity, settings:renderSettings};
    content.innerHTML = views[state.view]();
    enhanceTooltips(content);
  }

  function renderDashboard() {
    const active = state.runs.filter(r => ['queued','running'].includes(r.status)).length;
    const failures = state.runs.filter(r => r.status === 'failed').slice(-5);
    const last = [...state.runs].reverse().slice(0, 8);
    const backups = state.runs.filter(r=>r.taskType==='backup' && ['success','warning'].includes(r.status)).slice(-20);
    return `<div class="bu-grid">
      <section class="bu-card"><div class="bu-muted">Enabled jobs</div><div class="bu-stat">${state.config.jobs.filter(j => j.enabled).length}</div></section>
      <section class="bu-card"><div class="bu-muted">Repositories</div><div class="bu-stat">${state.config.repositories.length}</div></section>
      <section class="bu-card"><div class="bu-muted">Queued / running</div><div class="bu-stat">${active}</div></section>
      <section class="bu-card"><div class="bu-muted">Recent failures</div><div class="bu-stat">${failures.length}</div></section>
    </div><div class="bu-chart-grid-layout">
      <section class="bu-card"><h2>Backup size</h2><div class="bu-muted">Total source data processed per backup</div>${lineChart(backups,'bytesProcessed',formatBytes,'Run a backup to collect size history.')}</section>
      <section class="bu-card"><h2>Files backed up</h2><div class="bu-muted">Total files processed per backup</div>${lineChart(backups,'filesProcessed',value=>Math.round(value).toLocaleString(),'Run a backup to collect file history.')}</section>
    </div><section class="bu-card" style="margin-top:16px"><h2>Recent activity</h2>${runsTable(last)}</section>`;
  }

  function renderJobs() {
    const rows = state.config.jobs.map(j => `<tr><td><strong>${esc(j.name)}</strong><br><span class="bu-muted">${esc(j.id)}</span></td><td>${esc(j.type)}</td><td>${esc(repoName(j.repositoryId))}</td><td>${esc(scheduleLabel(j.schedule?.cron))}</td><td>${j.enabled ? status('success') : status('cancelled')}</td><td>${actionGroup(button('Run',`run-job:${j.id}`,'primary')+button('Edit',`edit-job:${j.id}`)+button('Duplicate',`duplicate-job:${j.id}`)+button('Delete',`delete-job:${j.id}`,'danger'))}</td></tr>`).join('');
    return `<div class="bu-toolbar"><div><h2>Backup Jobs</h2><div class="bu-muted">One isolated Restic repository per job.</div></div>${button('New job','new-job','primary')}</div>
      <section class="bu-card">${rows ? `<table class="bu-table"><thead><tr><th>Job</th><th>Type</th><th>Repository</th><th>Schedule</th><th>Enabled</th><th>Actions</th></tr></thead><tbody>${rows}</tbody></table>` : '<div class="bu-empty">No backup jobs configured.</div>'}</section>`;
  }

  function jobForm(job = {}) {
    state.backupSelections = (job.sources || []).filter(s=>s.path).map(s=>s.path);
    state.workloadSelections = (job.sources || []).filter(s=>s.workloadId).map(s=>s.workloadId);
    state.workloads = []; state.workloadAvailable = true; state.workloadError = '';
    state.sourceBrowserPath = '/'; state.sourceDirectories = sourceRoots; state.sourceBrowserError = '';
    const options = state.config.repositories.map(r => `<option value="${esc(r.id)}" ${r.id===job.repositoryId?'selected':''}>${esc(r.name)}</option>`).join('');
    content.innerHTML = `<section class="bu-card"><h2>${job.id ? 'Edit' : 'New'} backup job</h2><form id="bu-job-form" class="bu-form">
      <input type="hidden" name="id" value="${esc(job.id || '')}"><label>Name<input name="name" required value="${esc(job.name || '')}"></label>
      <label>Type<select name="type">${['share','appdata','docker','vm','flash'].map(v=>`<option ${v===job.type?'selected':''}>${v}</option>`).join('')}</select></label>
      <label>Repository<select name="repositoryId" required>${options}</select></label><label>Compression<select name="compression">${['auto','off','max'].map(v=>`<option ${v===(job.compression||'auto')?'selected':''}>${v}</option>`).join('')}</select></label>
      ${scheduleFields(job)}
      <div id="bu-job-specific" class="wide"></div>
      <label class="wide">Exclude patterns, one per line<textarea name="excludes">${esc((job.excludes||[]).join('\n'))}</textarea></label>
      <fieldset class="wide bu-fieldset"><legend>Backup retention</legend><p class="bu-muted">Snapshots matching any enabled rule are kept. Set daily, weekly and monthly to 0 for a strict age limit.</p><div class="bu-retention-grid"><label>Keep all backups for days<input name="keepWithinDays" type="number" min="0" value="${job.retention?.keepWithinDays ?? 30}"></label><label>Additional daily backups<input name="keepDaily" type="number" min="0" value="${job.retention?.daily ?? 0}"></label><label>Additional weekly backups<input name="keepWeekly" type="number" min="0" value="${job.retention?.weekly ?? 0}"></label><label>Additional monthly backups<input name="keepMonthly" type="number" min="0" value="${job.retention?.monthly ?? 0}"></label></div></fieldset>
      <label><span>Enabled</span><input name="enabled" type="checkbox" ${job.enabled !== false?'checked':''}></label>
      <div class="wide bu-actions">${button('Save','submit-job','primary')}${button('Cancel','cancel-form')}</div></form></section>`;
    enhanceTooltips(content);
    renderJobSpecific(job.type || 'share', job);
  }

  function renderJobSpecific(type, job = {}) {
    const host = document.getElementById('bu-job-specific');
    if (!host) return;
    if (type === 'share' || type === 'appdata') {
      host.innerHTML = `<h3>${type==='appdata'?'Appdata folders':'Share folders'}</h3><div class="bu-muted">Select one or more folders. Existing file exclude patterns remain available below.</div><div id="bu-source-browser"></div><label class="bu-manual-source">Additional manual paths<textarea name="sources"></textarea></label>`;
      state.sourceBrowserPath = type === 'appdata' ? '/mnt/user/appdata' : '/mnt/user';
      state.sourceDirectories = []; state.sourceBrowserError = '';
      loadSourceDirectories(state.sourceBrowserPath);
    } else if (type === 'docker' || type === 'vm') {
      host.innerHTML = `<h3>${type==='docker'?'Docker containers':'Virtual machines'}</h3><div class="bu-muted">Select the workloads to back up. Metadata and discovered bind mounts or virtual disks are included automatically.</div><div id="bu-workload-browser" class="bu-browser-card"><div class="bu-empty">Loading...</div></div><div class="bu-workload-options"><label>Consistency<select name="consistency"><option value="live">Live</option><option value="stop" ${job.consistency?.mode==='stop'?'selected':''}>Controlled stop</option></select></label><label>Shutdown timeout (seconds)<input name="shutdownSecs" type="number" min="1" value="${job.shutdownTimeoutSeconds || 120}"></label></div>`;
      loadWorkloads(type);
    } else {
      state.backupSelections = ['/boot'];
      host.innerHTML = `<div class="bu-info-card"><h3>Unraid boot backup</h3><p>The complete <strong>/boot</strong> configuration drive is backed up. No additional source selection is required.</p></div>`;
    }
    enhanceTooltips(host);
  }

  function renderWorkloadBrowser(type) {
    const host = document.getElementById('bu-workload-browser');
    if (!host) return;
    const selected = new Set(state.workloadSelections);
    const rows = state.workloads.map(item=>`<tr><td><input type="checkbox" class="bu-workload-select" value="${esc(item.id)}" ${selected.has(item.id)?'checked':''}></td><td><strong>${esc(item.name)}</strong><br><span class="bu-muted">${esc(item.id)}</span></td><td>${esc(item.status||'unknown')}</td></tr>`).join('');
    const notice = state.workloadError ? `<div class="${state.workloadAvailable===false?'bu-inline-info':'bu-inline-error'}">${esc(state.workloadError)}</div>` : '';
    host.innerHTML = `<div class="bu-toolbar"><h3>${type==='docker'?'Available containers':'Available virtual machines'}</h3><span class="bu-count">Selected: ${state.workloadSelections.length}</span></div>${notice}<div class="bu-browser-scroll"><table class="bu-table"><thead><tr><th>Select</th><th>Name</th><th>Status</th></tr></thead><tbody>${rows||(!state.workloadError?'<tr><td colspan="3" class="bu-empty">No workloads found.</td></tr>':'')}</tbody></table></div>`;
  }

  async function loadWorkloads(type) {
    try {
      const result = await api(`/v1/workloads/${type}`); state.workloads = result.items || []; state.workloadAvailable = result.available; state.workloadError = result.message || '';
    } catch (error) {
      state.workloads = []; state.workloadAvailable = true; state.workloadError = `Workloads could not be loaded: ${error.message}`;
    }
    renderWorkloadBrowser(type);
  }

  function renderSourceBrowser() {
    const host = document.getElementById('bu-source-browser');
    if (!host) return;
    const selected = new Set(state.backupSelections);
    const rows = state.sourceDirectories.map(item => `<tr><td><input type="checkbox" class="bu-source-select" value="${esc(item.path)}" ${selected.has(item.path)?'checked':''}></td><td>dir</td><td>${esc(item.name)}</td><td>${esc(item.path)}</td><td>${button('Open',`open-source-dir:${encodeURIComponent(item.path)}`)}</td></tr>`).join('');
    const chosen = state.backupSelections.length ? state.backupSelections.map(path=>`<span class="bu-selection">${esc(path)}</span>`).join('') : '<span class="bu-muted">No folders selected.</span>';
    const error = state.sourceBrowserError ? `<div class="bu-inline-error">${esc(state.sourceBrowserError)}</div>` : '';
    host.innerHTML = `<section class="bu-browser-card"><div class="bu-toolbar"><div><h3>Backup source browser</h3><span class="bu-muted">${esc(state.sourceBrowserPath)}</span></div><div class="bu-actions">${state.sourceBrowserPath!=='/'?button('Select current folder','select-current-source'):''}${state.sourceBrowserPath!=='/'?button('Up','source-up'):''}<span class="bu-count">Selected: ${state.backupSelections.length}</span></div></div>${error}<div class="bu-selection-list">${chosen}</div><div class="bu-browser-scroll"><table class="bu-table"><thead><tr><th>Select</th><th>Type</th><th>Name</th><th>Path</th><th></th></tr></thead><tbody>${rows||'<tr><td colspan="5" class="bu-empty">No subfolders found.</td></tr>'}</tbody></table></div></section>`;
    enhanceTooltips(host);
  }

  async function loadSourceDirectories(path) {
    if (path === '/') {
      state.sourceDirectories = sourceRoots; state.sourceBrowserPath = '/'; state.sourceBrowserError = ''; renderSourceBrowser(); return;
    }
    try {
      state.sourceDirectories = await api(`/v1/filesystem/directories?path=${encodeURIComponent(path)}`);
      state.sourceBrowserPath = path;
      state.sourceBrowserError = '';
      renderSourceBrowser();
    } catch (error) {
      state.sourceBrowserError = `Folder could not be loaded: ${error.message}`;
      renderSourceBrowser();
    }
  }

  function renderRepositories() {
    const rows = state.config.repositories.map(r => {
      const initialize = volatileLocation(r) ? disabledButton('Initialize','Dieses Ziel liegt im flüchtigen RAM. Zuerst über Edit einen dauerhaften Pfad wie /mnt/user/Backups/HomeServer eintragen.') : button('Initialize',`init-repo:${r.id}`,'primary');
      const actions = initialize+button('Test',`test-repo:${r.id}`)+button('Snapshots',`repo-snapshots:${r.id}`)+button('Check',`maint:check:${r.id}`)+button('Prune',`maint:prune:${r.id}`)+button('Edit',`edit-repo:${r.id}`)+button('Delete',`delete-repo:${r.id}`,'danger');
      return `<tr><td><strong>${esc(r.name)}</strong><br><span class="bu-muted">${esc(r.id)}</span></td><td>${esc(r.type)}</td><td>${esc(r.location)}${volatileLocation(r)?'<span class="bu-warning">Volatile path: data is lost after reboot. Use /mnt/user/...</span>':''}</td><td>${r.mount?.managed?'Managed':'Direct / external'}</td><td>${actionGroup(actions)}</td></tr>`;
    }).join('');
    return `<div class="bu-toolbar"><div><h2>Repositories</h2><div class="bu-muted">Local, SMB, NFS and SFTP destinations.</div></div>${button('New repository','new-repo','primary')}</div><section class="bu-card">${rows ? `<table class="bu-table"><thead><tr><th>Name</th><th>Type</th><th>Location</th><th>Mount</th><th>Actions</th></tr></thead><tbody>${rows}</tbody></table>` : '<div class="bu-empty">No repositories configured.</div>'}</section>`;
  }

  function repositoryForm(repo = {}) {
    state.repositoryBrowserPath = '/'; state.repositoryDirectories = repositoryRoots; state.repositoryBrowserError = '';
    content.innerHTML = `<section class="bu-card"><h2>${repo.id?'Edit':'New'} repository</h2><form id="bu-repo-form" class="bu-form"><input type="hidden" name="id" value="${esc(repo.id||'')}">
      <label>Name<input name="name" required value="${esc(repo.name||'')}"></label><label>Type<select name="type">${['local','smb','nfs','sftp'].map(v=>`<option ${v===repo.type?'selected':''}>${v}</option>`).join('')}</select></label>
      <div id="bu-repository-specific" class="wide"></div>
      <fieldset class="wide bu-fieldset"><legend>Restic encryption</legend><div class="bu-form-grid"><label>Password secret ID<input name="passwordRef" required value="${esc(repo.passwordRef||id('repo-password'))}"></label><label>Repository password<input name="password" type="password" placeholder="Leave blank to keep existing"></label></div></fieldset>
      <div class="wide bu-actions">${button('Save','submit-repo','primary')}${button('Cancel','cancel-form')}</div></form></section>`;
    enhanceTooltips(content);
    renderRepositorySpecific(repo.type || 'local', repo);
  }

  function repositoryLocationField(label, value, placeholder) {
    return `<label class="wide">${label}<input name="location" required placeholder="${esc(placeholder)}" value="${esc(value||'')}"></label>`;
  }

  function renderRepositorySpecific(type, repo = {}) {
    const host = document.getElementById('bu-repository-specific');
    if (!host) return;
    if (type === 'local') {
      host.innerHTML = `${repositoryLocationField('Local repository folder',repo.location,'/mnt/user/backups/urbm')}<div id="bu-repository-browser"></div>`;
      renderRepositoryBrowser();
    } else if (type === 'smb' || type === 'nfs') {
      const managed = repo.mount?.managed === true;
      host.innerHTML = `<fieldset class="bu-fieldset"><legend>${type.toUpperCase()} destination</legend><label class="bu-inline-check"><input name="managed" type="checkbox" ${managed?'checked':''}><span>URBM should mount this share</span></label><div class="bu-external-mount" ${managed?'hidden':''}>${repositoryLocationField('Existing mounted folder',repo.location,'/mnt/remotes/backup-nas')}</div><div class="bu-managed-mount" ${managed?'':'hidden'}><div class="bu-form-grid"><label>Remote share<input name="remote" placeholder="${type==='smb'?'//192.168.1.20/backup':'192.168.1.20:/volume1/backup'}" value="${esc(repo.mount?.remote||'')}"></label><label>Mount point<input name="mountPoint" placeholder="/mnt/remotes/urbm-backup" value="${esc(repo.mount?.mountPoint||repo.location||'')}"></label>${type==='smb'?`<label>Credential secret ID<input name="credentialRef" value="${esc(repo.credentialRef||'smb-credentials')}"></label><label class="wide">SMB credentials<textarea name="credential" placeholder="username=backup&#10;password=secret"></textarea></label>`:''}</div></div></fieldset>`;
    } else {
      host.innerHTML = `<fieldset class="bu-fieldset"><legend>SFTP destination</legend><div class="bu-form-grid">${repositoryLocationField('SFTP repository location',repo.location,'backup@192.168.1.20:/srv/backups/urbm')}<label>SSH key secret ID<input name="credentialRef" value="${esc(repo.credentialRef||'')}"></label><label>SFTP known_hosts path<input name="knownHostsPath" value="${esc(repo.options?.knownHostsPath||'/root/.ssh/known_hosts')}"></label><label class="wide">Private SSH key (optional)<textarea name="credential" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"></textarea></label></div></fieldset>`;
    }
    enhanceTooltips(host);
  }

  function renderRepositoryBrowser() {
    const host = document.getElementById('bu-repository-browser');
    if (!host) return;
    const rows = state.repositoryDirectories.map(item=>`<tr><td>${esc(item.name)}</td><td>${esc(item.path)}</td><td>${button('Open',`open-repository-dir:${encodeURIComponent(item.path)}`)}</td></tr>`).join('');
    const error = state.repositoryBrowserError ? `<div class="bu-inline-error">${esc(state.repositoryBrowserError)}</div>` : '';
    host.innerHTML = `<section class="bu-browser-card"><div class="bu-toolbar"><div><h3>Local folder browser</h3><span class="bu-muted">${esc(state.repositoryBrowserPath)}</span></div><div class="bu-actions">${state.repositoryBrowserPath!=='/'?button('Use this folder','select-repository-folder','primary'):''}${state.repositoryBrowserPath!=='/'?button('Up','repository-up'):''}</div></div>${error}<div class="bu-browser-scroll"><table class="bu-table"><thead><tr><th>Name</th><th>Path</th><th></th></tr></thead><tbody>${rows||'<tr><td colspan="3" class="bu-empty">No subfolders found.</td></tr>'}</tbody></table></div></section>`;
    enhanceTooltips(host);
  }

  async function loadRepositoryDirectories(path) {
    if (path === '/') {
      state.repositoryBrowserPath='/'; state.repositoryDirectories=repositoryRoots; state.repositoryBrowserError=''; renderRepositoryBrowser(); return;
    }
    try {
      state.repositoryDirectories=await api(`/v1/filesystem/directories?path=${encodeURIComponent(path)}`);
      state.repositoryBrowserPath=path; state.repositoryBrowserError='';
    } catch (error) {
      state.repositoryBrowserError=`Folder could not be loaded: ${error.message}`;
    }
    renderRepositoryBrowser();
  }

  function renderSnapshots() {
    const options = state.config.repositories.map(r=>`<option value="${esc(r.id)}">${esc(r.name)}</option>`).join('');
    const rows = state.snapshots.map(s=>`<tr><td>${esc(s.short_id||s.id)}</td><td>${esc(new Date(s.time).toLocaleString())}</td><td>${esc(s.hostname)}</td><td>${esc((s.paths||[]).join(', '))}</td><td>${button('Browse',`browse:${s.id}`)}</td></tr>`).join('');
    return `<div class="bu-toolbar"><h2>Snapshots</h2><div class="bu-actions"><select id="bu-snapshot-repo">${options}</select>${button('Load','load-snapshots','primary')}</div></div><section class="bu-card">${rows?`<table class="bu-table"><thead><tr><th>Snapshot</th><th>Time</th><th>Host</th><th>Paths</th><th></th></tr></thead><tbody>${rows}</tbody></table>`:'<div class="bu-empty">Select a repository and load snapshots.</div>'}</section>${state.selectedSnapshot && state.files.length ? renderFileBrowser() : ''}`;
  }

  function renderFileBrowser() {
    const selected = new Set(state.restoreIncludes);
    const rows = state.files.map(item => {
      const path = item.path || `${state.browserPath.replace(/\/$/,'')}/${item.name||''}`;
      const open = item.type === 'dir' ? button('Open',`open-dir:${encodeURIComponent(path)}`) : '';
      return `<tr><td><input type="checkbox" class="bu-file-select" value="${esc(path)}" ${selected.has(path)?'checked':''}></td><td>${esc(item.type||'')}</td><td>${esc(item.name||path)}</td><td>${Number(item.size||0).toLocaleString()}</td><td>${open}</td></tr>`;
    }).join('');
    const chosen = state.restoreIncludes.length ? state.restoreIncludes.map(path=>`<span class="bu-selection">${esc(path)}</span>`).join('') : '<span class="bu-muted">Nothing selected.</span>';
    return `<section class="bu-card" style="margin-top:16px"><div class="bu-toolbar"><div><h3>Snapshot ${esc(state.selectedSnapshot.slice(0,12))}</h3><span class="bu-muted">${esc(state.browserPath)}</span></div><div class="bu-actions">${button('Select current folder','select-current-restore')}${state.browserPath!=='/'?button('Up','browser-up'):''}${button(`Restore selected (${state.restoreIncludes.length})`,'restore-selected','primary')}</div></div><div class="bu-selection-list">${chosen}</div><table class="bu-table"><thead><tr><th>Select</th><th>Type</th><th>Name</th><th>Size</th><th></th></tr></thead><tbody>${rows}</tbody></table></section>`;
  }

  function renderRestore() {
    const options = state.config.repositories.map(r=>`<option value="${esc(r.id)}">${esc(r.name)}</option>`).join('');
    return `<section class="bu-card"><h2>Restore</h2><form id="bu-restore-form" class="bu-form"><label>Repository<select name="repositoryId">${options}</select></label><label>Snapshot ID<input name="snapshotId" required value="${esc(state.selectedSnapshot||'')}"></label><label class="wide">Include paths, one per line<textarea name="includes">${esc((state.restoreIncludes||[]).join('\n'))}</textarea></label><label class="wide">Target<input name="target" required value="${esc(state.config.settings.restoreRoot)}/${esc(id('restore'))}"></label><label>In-place restore<input name="inPlace" type="checkbox"></label><label>Confirm overwrite risk<input name="confirmed" type="checkbox"></label><div class="wide bu-actions">${button('Queue restore','submit-restore','primary')}</div></form></section>`;
  }

  function renderActivity() { return `<div class="bu-toolbar"><div><h2>Activity</h2><div class="bu-muted">Running tasks update automatically every two seconds.</div></div>${button('Refresh','refresh','primary')}</div><section class="bu-card">${runsTable([...state.runs].reverse())}</section>`; }
  function progressView(run) {
    if(run.status==='queued') return '<span class="bu-muted">Waiting in queue</span>';
    if(run.taskType!=='backup'&&!run.progressPercent) return '<span class="bu-muted">No percentage available</span>';
    const percent=Math.max(0,Math.min(100,Number(run.progressPercent)||0));
    const scanning=run.status==='running'&&percent===0;
    const details=[];
    if(run.progressTotal) details.push(`${formatBytes(run.progressBytes)} / ${formatBytes(run.progressTotal)}`);
    if(run.progressFileTotal) details.push(`${Number(run.progressFiles||0).toLocaleString()} / ${Number(run.progressFileTotal).toLocaleString()} files`);
    if(run.bytesPerSecond) details.push(`${formatBytes(run.bytesPerSecond)}/s`);
    if(run.secondsRemaining>0) details.push(`ETA ${formatEta(run.secondsRemaining)}`);
    return `<div class="bu-progress-wrap"><div class="bu-progress-label"><strong>${scanning?'Scanning…':`${percent.toFixed(1)}%`}</strong><span>${esc(details.join(' · '))}</span></div><div class="bu-progress ${scanning?'indeterminate':''}"><span style="width:${scanning?30:percent}%"></span></div>${run.currentFile?`<div class="bu-current-file" title="${esc(run.currentFile)}">${esc(run.currentFile)}</div>`:''}</div>`;
  }
  function liveLogView(run) {
    if(!(run.liveLog||[]).length) return '';
    return `<details class="bu-live-log" ${run.status==='running'?'open':''}><summary>${run.status==='running'?'Live log':'Run log'} (${run.liveLog.length})</summary><pre>${esc(run.liveLog.join('\n'))}</pre></details>`;
  }
  function runsTable(runs) {
    if(!runs.length) return '<div class="bu-empty">No activity recorded.</div>';
    return `<table class="bu-table bu-runs-table"><thead><tr><th>Task</th><th>Status</th><th>Created</th><th>Progress</th><th>Message</th><th></th></tr></thead><tbody>${runs.map(r=>`<tr><td>${esc(r.taskType)} ${esc(r.jobId||'')}</td><td>${status(r.status)}</td><td>${esc(new Date(r.createdAt).toLocaleString())}</td><td>${progressView(r)}</td><td>${esc(r.message||'')}</td><td>${['queued','running'].includes(r.status)?button('Cancel',`cancel-run:${r.id}`,'danger'):''}</td></tr>${(r.liveLog||[]).length?`<tr class="bu-log-row"><td colspan="6">${liveLogView(r)}</td></tr>`:''}`).join('')}</tbody></table>`;
  }

  function renderSettings() {
    const s = state.config.settings;
    const unraid = state.config.notifications.find(n=>n.type==='unraid') || {id:'unraid',enabled:true,events:['success','warning','failed']};
    const gotify = state.config.notifications.find(n=>n.type==='gotify') || {id:'gotify',enabled:false,events:['success','warning','failed']};
    const eventBoxes = target => [['success','Success'],['warning','Warning'],['failed','Failed']].map(([value,label])=>`<label><input name="events" type="checkbox" value="${value}" ${(target.events||[]).includes(value)?'checked':''}><span>${label}</span></label>`).join('');
    return `<section class="bu-card"><h2>Settings</h2><form id="bu-settings-form" class="bu-form"><label class="wide">Restic binary<input name="resticPath" value="${esc(s.resticPath)}"></label><label class="wide">Restore staging root<input name="restoreRoot" value="${esc(s.restoreRoot)}"></label><label class="wide">Persistent log directory<input name="persistentLogDir" value="${esc(s.persistentLogDir||'')}"></label><label>Catch-up window (hours)<input name="catchUp" type="number" value="${s.catchUpWindowHours||24}"></label><label>Monthly check cron<input name="checkCron" value="${esc(s.checkCron)}"></label><label>Prune cron<input name="pruneCron" value="${esc(s.pruneCron)}"></label><div class="wide bu-actions">${button('Save settings','submit-settings','primary')}</div></form></section>
      <section class="bu-card" style="margin-top:16px"><h2>Unraid notifications</h2><p class="bu-muted">Uses Unraid's notification system and all agents configured under Settings &gt; Notification Settings.</p><form id="bu-unraid-notify-form" class="bu-form"><input type="hidden" name="id" value="${esc(unraid.id)}"><label><span>Enabled</span><input name="enabled" type="checkbox" ${unraid.enabled?'checked':''}></label><fieldset class="wide bu-fieldset"><legend>Notify on</legend><div class="bu-event-options">${eventBoxes(unraid)}</div></fieldset><div class="wide bu-actions">${button('Save Unraid notifications','submit-unraid-notify','primary')}</div></form></section>
      <section class="bu-card" style="margin-top:16px"><h2>Gotify notification</h2><p class="bu-muted">Create an application in Gotify and enter its application token here.</p><form id="bu-gotify-notify-form" class="bu-form"><input type="hidden" name="id" value="${esc(gotify.id||'gotify')}"><label class="wide">Server URL<input name="url" type="url" placeholder="https://gotify.example.de" value="${esc(gotify.url||'')}"></label><label>Token secret ID<input name="tokenRef" value="${esc(gotify.tokenRef||'gotify-token')}"></label><label>Application token<input name="token" type="password" autocomplete="new-password"></label><label><span>Enabled</span><input name="enabled" type="checkbox" ${gotify.enabled?'checked':''}></label><fieldset class="wide bu-fieldset"><legend>Notify on</legend><div class="bu-event-options">${eventBoxes(gotify)}</div></fieldset><div class="wide bu-actions">${button('Save Gotify notification','submit-gotify-notify','primary')}</div></form></section>`;
  }

  async function saveConfig() {
    state.config = await api('/v1/config',{method:'PUT',body:JSON.stringify(state.config)});
    notify('Configuration saved');
    return state.config;
  }
  const lines = (value) => value.split('\n').map(v=>v.trim()).filter(Boolean);

  async function handle(action, element) {
    const [name, a, b] = action.split(':');
    try {
      if (name === 'new-job') return jobForm();
      if (name === 'edit-job') return jobForm(state.config.jobs.find(j=>j.id===a));
      if (name === 'new-repo') return repositoryForm();
      if (name === 'edit-repo') return repositoryForm(state.config.repositories.find(r=>r.id===a));
      if (name === 'cancel-form') return render();
      if (name === 'refresh') return load();
      if (name === 'run-job') { await api(`/v1/jobs/${a}/run`,{method:'POST'}); notify('Backup queued'); return load(); }
      if (name === 'cancel-run') { await api(`/v1/runs/${a}/cancel`,{method:'POST'}); return load(); }
      if (name === 'test-repo') { await api(`/v1/repositories/${a}/test`,{method:'POST'}); return notify('Repository connection succeeded'); }
      if (name === 'init-repo') { if(confirm('Initialize a new Restic repository at this destination? Use this only for a new, empty backup destination.')) { await api(`/v1/repositories/${a}/init`,{method:'POST'}); notify('Repository initialized successfully'); } return; }
      if (name === 'maint') { await api(`/v1/repositories/${b}/${a}`,{method:'POST'}); notify(`${a} queued`); return load(); }
      if (name === 'repo-snapshots') { state.view='snapshots'; document.querySelector('[data-view="snapshots"]').click(); render(); setTimeout(()=>{document.getElementById('bu-snapshot-repo').value=a; handle('load-snapshots');},0); return; }
      if (name === 'load-snapshots') { const repo=document.getElementById('bu-snapshot-repo').value; state.snapshots=await api(`/v1/repositories/${repo}/snapshots`); state.snapshotRepo=repo; return render(); }
      if (name === 'browse') { state.selectedSnapshot=a; state.browserPath='/'; state.restoreIncludes=[]; state.files=await api(`/v1/repositories/${state.snapshotRepo}/snapshots/${a}/files?path=${encodeURIComponent('/')}`); return render(); }
      if (name === 'open-source-dir') { return loadSourceDirectories(decodeURIComponent(a)); }
      if (name === 'source-up') { const parent=state.sourceBrowserPath==='/'?'/':state.sourceBrowserPath.split('/').slice(0,-1).join('/')||'/'; return loadSourceDirectories(parent==='/mnt'?'/':parent); }
      if (name === 'select-current-source') { if(!state.backupSelections.includes(state.sourceBrowserPath)) state.backupSelections.push(state.sourceBrowserPath); return renderSourceBrowser(); }
      if (name === 'open-repository-dir') { return loadRepositoryDirectories(decodeURIComponent(a)); }
      if (name === 'repository-up') { const parent=state.repositoryBrowserPath==='/'?'/':state.repositoryBrowserPath.split('/').slice(0,-1).join('/')||'/'; return loadRepositoryDirectories(parent==='/mnt'?'/':parent); }
      if (name === 'select-repository-folder') { const location=document.querySelector('#bu-repo-form [name="location"]'); if(location) location.value=state.repositoryBrowserPath; return; }
      if (name === 'open-dir') { state.browserPath=decodeURIComponent(a); state.files=await api(`/v1/repositories/${state.snapshotRepo}/snapshots/${state.selectedSnapshot}/files?path=${encodeURIComponent(state.browserPath)}`); return render(); }
      if (name === 'browser-up') { state.browserPath=state.browserPath.replace(/\/$/,'').split('/').slice(0,-1).join('/')||'/'; state.files=await api(`/v1/repositories/${state.snapshotRepo}/snapshots/${state.selectedSnapshot}/files?path=${encodeURIComponent(state.browserPath)}`); return render(); }
      if (name === 'select-current-restore') { if(!state.restoreIncludes.includes(state.browserPath)) state.restoreIncludes.push(state.browserPath); return render(); }
      if (name === 'restore-selected') { if(!state.restoreIncludes.length) throw new Error('Select at least one file or directory'); state.view='restore'; document.querySelectorAll('.bu-tabs button').forEach(x=>x.classList.toggle('active',x.dataset.view==='restore')); return render(); }
      if (name === 'delete-job') { if(confirm('Delete this job?')) { state.config.jobs=state.config.jobs.filter(j=>j.id!==a); await saveConfig(); render(); } return; }
      if (name === 'duplicate-job') { const source=state.config.jobs.find(j=>j.id===a); const copy=structuredClone(source); copy.id=id('job'); copy.name += ' copy'; copy.enabled=false; state.config.jobs.push(copy); await saveConfig(); return render(); }
      if (name === 'delete-repo') { if(state.config.jobs.some(j=>j.repositoryId===a)) throw new Error('Repository is still referenced by a job'); if(confirm('Delete this repository?')) { state.config.repositories=state.config.repositories.filter(r=>r.id!==a); await saveConfig(); render(); } return; }
      if (name.startsWith('submit-')) return submit(name);
    } catch (error) { notify(error.message, true); }
  }

  async function submit(kind) {
    if (kind === 'submit-job') {
      const f=new FormData(document.getElementById('bu-job-form')); const existing=state.config.jobs.find(j=>j.id===f.get('id'));
      const type=f.get('type'); let sources=[];
      if(type==='docker'||type==='vm') sources=state.workloadSelections.map(workloadId=>({workloadId}));
      else if(type==='flash') sources=[];
      else sources=[...state.backupSelections.map(path=>({path})),...lines(f.get('sources')||'').map(path=>({path}))].filter((item,index,all)=>all.findIndex(other=>other.path===item.path)===index);
      const retention={keepWithinDays:Number(f.get('keepWithinDays')),daily:Number(f.get('keepDaily')),weekly:Number(f.get('keepWeekly')),monthly:Number(f.get('keepMonthly'))};
      if(!retention.keepWithinDays&&!retention.daily&&!retention.weekly&&!retention.monthly) throw new Error('Retention must keep at least one age or calendar policy');
      const job={schemaVersion:1,id:f.get('id')||id('job'),name:f.get('name'),type,enabled:f.get('enabled')==='on',repositoryId:f.get('repositoryId'),sources,schedule:{cron:scheduleCron(f),timezone:Intl.DateTimeFormat().resolvedOptions().timeZone},consistency:{mode:f.get('consistency')||'live'},compression:f.get('compression'),excludes:lines(f.get('excludes')),tags:existing?.tags||[],retention,notifyOn:existing?.notifyOn||['success','warning','failed'],shutdownTimeoutSeconds:Number(f.get('shutdownSecs')||120)};
      state.config.jobs=state.config.jobs.filter(j=>j.id!==job.id).concat(job); await saveConfig(); render();
    }
    if (kind === 'submit-repo') {
      const f=new FormData(document.getElementById('bu-repo-form')); const value=name=>String(f.get(name)||'').trim(); const type=value('type'); const managed=(type==='smb'||type==='nfs')&&f.get('managed')==='on';
      let location=managed?value('mountPoint'):value('location');
      if(!location) throw new Error(managed?'Mount point is required':'Repository location is required');
      if(managed&&!value('remote')) throw new Error('Remote share is required for a managed mount');
      let credentialRef=(type==='sftp'||(type==='smb'&&managed))?value('credentialRef'):'';
      if(value('credential')&&!credentialRef) credentialRef=id(type==='sftp'?'sftp-key':'smb-credentials');
      const repo={schemaVersion:1,id:value('id')||id('repo'),name:value('name'),type,location,passwordRef:value('passwordRef'),credentialRef,mount:managed?{managed:true,remote:value('remote'),mountPoint:value('mountPoint')}:null,options:type==='sftp'?{knownHostsPath:value('knownHostsPath')}: {}};
      if (volatileLocation(repo) && !confirm('WARNING: This destination is under /tmp or /run and will be lost after an Unraid reboot. Save it anyway?')) return;
      if(f.get('password')) await api(`/v1/secrets/${encodeURIComponent(repo.passwordRef)}`,{method:'PUT',body:JSON.stringify({type:'restic-password',value:f.get('password')})});
      if(f.get('credential')) await api(`/v1/secrets/${encodeURIComponent(repo.credentialRef)}`,{method:'PUT',body:JSON.stringify({type:type==='sftp'?'sftp-private-key':'smb-credential',value:f.get('credential')})});
      state.config.repositories=state.config.repositories.filter(r=>r.id!==repo.id).concat(repo);
      await saveConfig();
      state.config = await api('/v1/config');
      const saved = state.config.repositories.find(r=>r.id===repo.id);
      if (!saved || saved.location !== repo.location) throw new Error(`Repository location was not saved. Expected: ${repo.location}`);
      render(); notify(`Repository saved: ${saved.location}`);
    }
    if (kind === 'submit-restore') { const f=new FormData(document.getElementById('bu-restore-form')); await api('/v1/restores',{method:'POST',body:JSON.stringify({schemaVersion:1,id:'',repositoryId:f.get('repositoryId'),snapshotId:f.get('snapshotId'),includes:lines(f.get('includes')),target:f.get('target'),inPlace:f.get('inPlace')==='on',confirmed:f.get('confirmed')==='on'})}); notify('Restore queued'); state.view='activity'; render(); }
    if (kind === 'submit-settings') { const f=new FormData(document.getElementById('bu-settings-form')); state.config.settings={resticPath:f.get('resticPath'),restoreRoot:f.get('restoreRoot'),persistentLogDir:f.get('persistentLogDir'),catchUpWindowHours:Number(f.get('catchUp')),checkCron:f.get('checkCron'),pruneCron:f.get('pruneCron')}; await saveConfig(); }
    if (kind === 'submit-unraid-notify') { const f=new FormData(document.getElementById('bu-unraid-notify-form')); const target={schemaVersion:1,id:f.get('id')||'unraid',name:'Unraid',type:'unraid',enabled:f.get('enabled')==='on',events:f.getAll('events')}; if(target.enabled&&!target.events.length) throw new Error('Select at least one event for Unraid notifications'); state.config.notifications=state.config.notifications.filter(n=>n.type!=='unraid'&&n.type!=='ntfy').concat(target); await saveConfig(); render(); }
    if (kind === 'submit-gotify-notify') { const f=new FormData(document.getElementById('bu-gotify-notify-form')); const target={schemaVersion:1,id:f.get('id')||'gotify',name:'Gotify',type:'gotify',enabled:f.get('enabled')==='on',url:String(f.get('url')||'').trim(),tokenRef:String(f.get('tokenRef')||'').trim(),events:f.getAll('events')}; if(target.enabled&&(!target.url||!target.tokenRef)) throw new Error('Gotify server URL and token secret ID are required'); if(target.enabled&&!target.events.length) throw new Error('Select at least one event for Gotify notifications'); if(f.get('token')) await api(`/v1/secrets/${encodeURIComponent(target.tokenRef)}`,{method:'PUT',body:JSON.stringify({type:'gotify-token',value:f.get('token')})}); state.config.notifications=state.config.notifications.filter(n=>n.type!=='gotify'&&n.type!=='ntfy'); if(target.enabled||target.url) state.config.notifications.push(target); await saveConfig(); render(); }
  }

  document.querySelector('.bu-tabs').addEventListener('click', e => { const button=e.target.closest('[data-view]'); if(!button)return; state.view=button.dataset.view; document.querySelectorAll('.bu-tabs button').forEach(x=>x.classList.toggle('active',x===button)); render(); });
  content.addEventListener('click', e => { const target=e.target.closest('[data-action]'); if(!target)return; e.preventDefault(); handle(target.dataset.action,target); });
  content.addEventListener('change', e => {
    if (e.target.name === 'type' && e.target.closest('#bu-repo-form')) {
      renderRepositorySpecific(e.target.value);
    }
    if (e.target.name === 'managed' && e.target.closest('#bu-repo-form')) {
      const fieldset=e.target.closest('.bu-fieldset');
      const managed=fieldset?.querySelector('.bu-managed-mount'); const external=fieldset?.querySelector('.bu-external-mount');
      if(managed) managed.hidden=!e.target.checked;
      if(external) external.hidden=e.target.checked;
    }
    if (e.target.name === 'scheduleMode') {
      const weekdays=e.target.closest('.bu-schedule')?.querySelector('.bu-weekdays');
      if (weekdays) weekdays.hidden=e.target.value!=='selected';
    }
    if (e.target.name === 'type' && e.target.closest('#bu-job-form')) {
      state.backupSelections=[]; state.workloadSelections=[];
      renderJobSpecific(e.target.value);
    }
    if (e.target.classList.contains('bu-source-select')) {
      state.backupSelections = e.target.checked ? [...new Set([...state.backupSelections,e.target.value])] : state.backupSelections.filter(path=>path!==e.target.value);
      renderSourceBrowser();
    }
    if (e.target.classList.contains('bu-file-select')) {
      state.restoreIncludes = e.target.checked ? [...new Set([...state.restoreIncludes,e.target.value])] : state.restoreIncludes.filter(path=>path!==e.target.value);
      render();
    }
    if (e.target.classList.contains('bu-workload-select')) {
      state.workloadSelections = e.target.checked ? [...new Set([...state.workloadSelections,e.target.value])] : state.workloadSelections.filter(id=>id!==e.target.value);
      renderWorkloadBrowser(document.querySelector('#bu-job-form [name="type"]')?.value||'docker');
    }
  });
  content.addEventListener('submit', e => {
    const form = e.target.closest('form');
    if (!form) return;
    e.preventDefault();
    const action = form.id === 'bu-repo-form' ? 'submit-repo' : form.id === 'bu-job-form' ? 'submit-job' : form.id === 'bu-restore-form' ? 'submit-restore' : form.id === 'bu-settings-form' ? 'submit-settings' : form.id === 'bu-unraid-notify-form' ? 'submit-unraid-notify' : form.id === 'bu-gotify-notify-form' ? 'submit-gotify-notify' : '';
    if (action) handle(action);
  });
  root.addEventListener('mouseover', e => {
    const target = e.target.closest('[data-tooltip]');
    if (!target || target.contains(e.relatedTarget)) return;
    showTooltip(target, 1200);
  });
  root.addEventListener('mouseout', e => {
    const target = e.target.closest('[data-tooltip]');
    if (!target || target.contains(e.relatedTarget)) return;
    hideTooltip();
  });
  root.addEventListener('focusin', e => {
    const target = e.target.closest('[data-tooltip]');
    if (target) showTooltip(target, 0);
  });
  root.addEventListener('focusout', hideTooltip);
  root.addEventListener('keydown', e => { if (e.key === 'Escape') hideTooltip(); });
  window.addEventListener('scroll', hideTooltip, true);
  window.addEventListener('resize', hideTooltip);
  enhanceTooltips(root);
  setInterval(async()=>{ if(!state.config)return; try { state.runs=await api('/v1/runs'); if(['dashboard','activity'].includes(state.view))render(); }catch(_){ } },2000);
  load();
})();
