// Drone Task Manager - Frontend
(function() {
  'use strict';

  const _savedApiBase = localStorage.getItem('dtm-api-base');
  const API_BASE = _savedApiBase || window.location.origin;
  const _savedWsHost = localStorage.getItem('dtm-ws-host');
  const _wsHost = _savedWsHost || location.host;
  const WS_URL = (location.protocol === 'https:' ? 'wss://' : 'ws://') + _wsHost + '/ws';

  const state = {
    definitions: {},
    instances: {},
    activeLogId: null,
    ws: null,
    currentDefinitionId: null,
    previewTimer: null,
    historyPage: 1,
    historyPageSize: 5,
    historyFilter: '',
    historyTotal: 0,
    instancePage: 1,
    instancePageSize: 5,
    instanceTotal: 0,
    missionsPage: 1,
    missionsPageSize: 20,
    missionsTotal: 0,
    filesDir: localStorage.getItem('dtm-files-dir') || '~/savemap',
    filesPage: 1,
    filesPageSize: 20,
    filesTotal: 0,
    filesImgFilter: localStorage.getItem('dtm-files-img-filter') || '',
    filesBagFilter: localStorage.getItem('dtm-files-bag-filter') || '',
    filesLogFilter: localStorage.getItem('dtm-files-log-filter') || '',
    activeTab: localStorage.getItem('dtm-active-tab') || 'system',
    prevTab: null,
  };

  // DOM refs
  const els = {
    wsStatus: document.getElementById('ws-status'),
    marketSection: document.getElementById('market-section'),
    taskCards: document.getElementById('task-cards'),
    configSection: document.getElementById('config-section'),
    configTitle: document.getElementById('config-title'),
    configDesc: document.getElementById('config-desc'),
    configForm: document.getElementById('config-form'),
    backBtn: document.getElementById('back-btn'),
    startBtn: document.getElementById('start-btn'),
    commandPreview: document.getElementById('command-preview'),
    instancesList: document.getElementById('instances-list'),
    instancesSection: document.getElementById('instances-section'),
    logPanel: document.getElementById('log-panel'),
    logTitle: document.getElementById('log-title'),
    logContent: document.getElementById('log-content'),
    closeLog: document.getElementById('close-log'),
    fileBrowser: document.getElementById('file-browser'),
    closeFileBrowser: document.getElementById('close-file-browser'),
    fileList: document.getElementById('file-list'),
    systemMonitor: document.getElementById('system-monitor'),
    btnSysReboot: document.getElementById('btn-sys-reboot'),
    btnSysPoweroff: document.getElementById('btn-sys-poweroff'),
    historyList: document.getElementById('history-list'),
    historyFilter: document.getElementById('history-filter'),
    historyPageSize: document.getElementById('history-page-size'),
    historyPaging: document.getElementById('history-paging'),
    historyPagingInfo: document.getElementById('history-paging-info'),
    btnLoadHistory: document.getElementById('btn-load-history'),
    btnCleanupHistory: document.getElementById('btn-cleanup-history'),
    appUploadArea: document.getElementById('app-upload-area'),
    appFileInput: document.getElementById('app-file-input'),
    appsList: document.getElementById('apps-list'),
    btnRefreshApps: document.getElementById('btn-refresh-apps'),
    tabNav: document.getElementById('tab-nav'),
    tabContent: document.getElementById('tab-content'),
    instancePageSize: document.getElementById('instance-page-size'),
    instancePaging: document.getElementById('instance-paging'),
    instancePagingInfo: document.getElementById('instance-paging-info'),
    filesRootSelect: document.getElementById('files-root-select'),
    filesCurrentDir: document.getElementById('files-current-dir'),
    filesList: document.getElementById('files-list'),
    filesPaging: document.getElementById('files-paging'),
    filesPagingInfo: document.getElementById('files-paging-info'),
    filesImgFilter: document.getElementById('files-img-filter'),
    filesBagFilter: document.getElementById('files-bag-filter'),
    filesLogFilter: document.getElementById('files-log-filter'),
    btnFilesUpload: document.getElementById('btn-files-upload'),
    filesUploadInput: document.getElementById('files-upload-input'),
    btnFilesRefresh: document.getElementById('btn-files-refresh'),
    btnFilesDownloadZip: document.getElementById('btn-files-download-zip'),
    btnFilesDownloadTar: document.getElementById('btn-files-download-tar'),
    filesPackProgress: document.getElementById('files-pack-progress'),
    filesPackBar: document.getElementById('files-pack-bar'),
    filesPackPct: document.getElementById('files-pack-pct'),
    btnFilesUp: document.getElementById('btn-files-up'),
    updaterUploadArea: document.getElementById('updater-upload-area'),
    updaterFileInput: document.getElementById('updater-file-input'),
    updaterList: document.getElementById('updater-list'),
    filePreview: document.getElementById('file-preview'),
    filePreviewTitle: document.getElementById('file-preview-title'),
    filePreviewContainer: document.getElementById('file-preview-container'),
    closeFilePreview: document.getElementById('close-file-preview'),
    btnMediaSave: document.getElementById('btn-media-save'),
    btnMediaCancel: document.getElementById('btn-media-cancel'),
    mediaAutoStop: document.getElementById('media-auto-stop'),
    mediaStreamsList: document.getElementById('media-streams-list'),
    mediaStreamForm: document.getElementById('media-stream-form'),
    mediaStreamId: document.getElementById('media-stream-id'),
    mediaStreamName: document.getElementById('media-stream-name'),
    mediaStreamSource: document.getElementById('media-stream-source'),
    mediaStreamCodec: document.getElementById('media-stream-codec'),
    mediaPlayerArea: document.getElementById('media-player-area'),
    mediaPlayerTitle: document.getElementById('media-player-title'),
    mediaVideo: document.getElementById('media-video'),
    mediaPlayerStatus: document.getElementById('media-player-status'),
    btnMediaPlayerClose: document.getElementById('btn-media-player-close'),
    mediaError: document.getElementById('media-error'),
    pushGlobalAuto: document.getElementById('push-global-auto'),
    btnPushStartAll: document.getElementById('btn-push-start-all'),
    btnPushStopAll: document.getElementById('btn-push-stop-all'),
    btnPushAdd: document.getElementById('btn-push-add'),
    btnPushSave: document.getElementById('btn-push-save'),
    btnPushCancel: document.getElementById('btn-push-cancel'),
    pushForm: document.getElementById('push-form'),
    pushConfigList: document.getElementById('push-config-list'),
    pushName: document.getElementById('push-name'),
    pushSourceUrl: document.getElementById('push-source-url'),
    pushServerHost: document.getElementById('push-server-host'),
    pushServerPort: document.getElementById('push-server-port'),
    pushStreamKey: document.getElementById('push-stream-key'),
    pushProtocol: document.getElementById('push-protocol'),
    pushCodec: document.getElementById('push-codec'),
    pushEnabled: document.getElementById('push-enabled'),
    pushAutoStart: document.getElementById('push-auto-start'),
    pushStopOnFlight: document.getElementById('push-stop-on-flight'),
    detectEnabled: document.getElementById('detect-enabled'),
    detectAutoStart: document.getElementById('detect-auto-start'),
    detectStopOnFlight: document.getElementById('detect-stop-on-flight'),
    detectStreamUrl: document.getElementById('detect-stream-url'),
    detectOutputDir: document.getElementById('detect-output-dir'),
    detectConfidence: document.getElementById('detect-confidence'),
    detectInterval: document.getElementById('detect-interval'),
    detectMaxImages: document.getElementById('detect-max-images'),
    detectTargetLabels: document.getElementById('detect-target-labels'),
    detectModel: document.getElementById('detect-model'),
    btnDetectUploadModel: document.getElementById('btn-detect-upload-model'),
    detectModelInput: document.getElementById('detect-model-input'),
    btnDetectStart: document.getElementById('btn-detect-start'),
    btnDetectStop: document.getElementById('btn-detect-stop'),
    diagResults: document.getElementById('diag-results'),
    diagRunAll: document.getElementById('btn-diag-run-all'),
    diagRunSelected: document.getElementById('btn-diag-run-selected'),
    diagSelectAll: document.getElementById('diag-select-all'),
    diagBack: document.getElementById('btn-diag-back'),
    diagProgressBar: document.getElementById('diag-progress-bar'),
    diagProgressFill: document.getElementById('diag-progress-fill'),
    diagProgressText: document.getElementById('diag-progress-text'),
    diagAlert: document.getElementById('diag-alert'),
    diagDownload: document.getElementById('btn-diag-download'),
    btnDetectSave: document.getElementById('btn-detect-save'),
    detectStatus: document.getElementById('detect-status'),
    detectAlert: document.getElementById('detect-alert'),
    selfUpdateVersion: document.getElementById('self-update-version'),
    selfUpdateStatus: document.getElementById('self-update-status'),
    selfUpdateUploadArea: document.getElementById('self-update-upload-area'),
    selfUpdateFileInput: document.getElementById('self-update-file-input'),
    selfUpdatePackages: document.getElementById('self-update-packages'),
    selfUpdateBackups: document.getElementById('self-update-backups'),
    selfUpdateProgress: document.getElementById('self-update-progress'),
    selfUpdateProgressBar: document.getElementById('self-update-progress-bar'),
    selfUpdateProgressPct: document.getElementById('self-update-progress-pct'),
    selfUpdateUploadDone: document.getElementById('self-update-upload-done'),
    settingsApiBase: document.getElementById('settings-api-base'),
    settingsWsHost: document.getElementById('settings-ws-host'),
    btnSettingsSave: document.getElementById('btn-settings-save'),
    btnSettingsReset: document.getElementById('btn-settings-reset'),
    settingsStatus: document.getElementById('settings-status'),
    missionsList: document.getElementById('missions-list'),
    missionsEmpty: document.getElementById('missions-empty'),
    missionsPageSize: document.getElementById('missions-page-size'),
    missionsPaging: document.getElementById('missions-paging'),
    missionsPagingInfo: document.getElementById('missions-paging-info'),
    servicesList: document.getElementById('services-list'),
  };

  // Utils
  async function api(path, opts = {}) {
    const url = `${API_BASE}${path}`;
    const res = await fetch(url, {
      headers: { 'Content-Type': 'application/json', ...(opts.headers || {}) },
      ...opts,
    });
    if (!res.ok) {
      const text = await res.text();
      throw new Error(`${res.status}: ${text}`);
    }
    return res.status === 204 ? null : res.json();
  }

  function debounce(fn, ms) {
    let t;
    return (...args) => {
      clearTimeout(t);
      t = setTimeout(() => fn(...args), ms);
    };
  }

  function formatDuration(start, end) {
    const s = new Date(start), e = end ? new Date(end) : new Date();
    const diff = Math.max(0, Math.floor((e - s) / 1000));
    const m = Math.floor(diff / 60), sec = diff % 60;
    return `${m}m ${sec}s`;
  }

  function formatDateTime(iso) {
    if (!iso) return '-';
    const d = new Date(iso);
    const pad = n => String(n).padStart(2, '0');
    return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
  }

  function getDefName(def) {
    if (!def) return '';
    return def.name || def.id || '';
  }
  function getDefDesc(def) {
    if (!def) return '';
    return def.description || '';
  }

  function getActiveInstanceForDef(defId) {
    return Object.values(state.instances).find(
      i => i.definition_id === defId && ['pending','running','stopping'].includes(i.status)
    );
  }

  // WebSocket
  function connectWS() {
    const ws = new WebSocket(WS_URL);
    state.ws = ws;

    ws.onopen = () => {
      els.wsStatus.textContent = ' ' + t('ws_connected');
      els.wsStatus.className = 'badge ok';
    };
    ws.onclose = () => {
      els.wsStatus.textContent = ' ' + t('ws_disconnected');
      els.wsStatus.className = 'badge err';
      setTimeout(connectWS, 3000);
    };
    ws.onmessage = (ev) => {
      let msg;
      try { msg = JSON.parse(ev.data); } catch { return; }
      handleWsMessage(msg);
    };
  }

  function handleWsMessage(msg) {
    if (msg.type === 'snapshot') {
      state.instances = {};
      (msg.payload.instances || []).forEach(i => state.instances[i.id] = i);
      loadInstances();
      renderMarket();
      return;
    }
    const p = msg.payload || {};
    if (msg.type === 'status_changed') {
      const inst = state.instances[p.instance_id];
      if (inst) {
        inst.status = p.status;
        if (p.returncode !== undefined) inst.returncode = p.returncode;
      }
      loadInstances();
      renderMarket();
    }
    if (msg.type === 'progress_updated') {
      const inst = state.instances[p.instance_id];
      if (inst) inst.progress = p.progress;
      loadInstances();
    }
    if (msg.type === 'log_added') {
      if (state.activeLogId === p.instance_id) {
        appendLog(p.line);
      }
    }
    if (msg.type === 'system_info') {
      renderSystemInfo(p);
    }
  }

  // Market
  function renderMarket() {
    els.taskCards.innerHTML = '';
    Object.values(state.definitions).forEach(def => {
      const active = getActiveInstanceForDef(def.id);
      const card = document.createElement('div');
      card.className = 'task-card' + (active ? (active.status === 'stopping' ? ' stopping' : ' active') : '');
      card.innerHTML = `
        <h3>${escapeHtml(getDefName(def))}</h3>
        <p>${escapeHtml(getDefDesc(def))}</p>
        <div class="card-footer">
          <span style="font-size:12px;color:var(--text-muted)">${def.parameters.length} 个参数 · v${escapeHtml(def.version || '1')}</span>
          <span class="status-dot"></span>
        </div>
      `;
      card.onclick = () => openConfig(def.id);
      els.taskCards.appendChild(card);
    });
  }

  // Tab switching
  function switchTab(tabId) {
    if (!els.tabNav) return;
    state.activeTab = tabId;
    localStorage.setItem('dtm-active-tab', tabId);
    // Update buttons
    els.tabNav.querySelectorAll('.tab-btn').forEach(btn => {
      btn.classList.toggle('active', btn.dataset.tab === tabId);
    });
    // Update panes
    document.querySelectorAll('.tab-pane').forEach(pane => {
      pane.classList.toggle('active', pane.dataset.tab === tabId);
    });
    // Tab hooks
    if (tabId === 'live') loadMediaStreams();
    if (tabId === 'push') loadPushConfigs();
    if (tabId === 'detect') loadDetectionSettings();
    if (tabId === 'system') { loadUpdater(); loadSelfUpdate(); }
    if (tabId === 'diagnostics') loadDiagnostics();
    if (tabId === 'market') loadApps();
    if (tabId === 'services') { loadServices(); startServicesPoll(); } else { stopServicesPoll(); }
  }

  // Config sub-page
  function openConfig(defId) {
    const def = state.definitions[defId];
    if (!def) return;
    const active = getActiveInstanceForDef(defId);
    if (active) {
      alert(t('running_instance_alert', {name: getDefName(def), id: active.id}));
      switchTab('instances');
      scrollToInstance(active.id);
      return;
    }

    state.currentDefinitionId = defId;
    state.prevTab = state.activeTab;
    if (els.tabNav) els.tabNav.classList.add('hidden');
    if (els.tabContent) els.tabContent.classList.add('hidden');
    els.configSection.classList.remove('hidden');
    els.configTitle.textContent = getDefName(def);
    els.configDesc.textContent = getDefDesc(def);
    els.startBtn.disabled = false;
    els.startBtn.textContent = '▶ ' + t('start_task');
    els.commandPreview.textContent = '';

    buildForm(def);
  }

  function buildForm(def) {
    els.configForm.innerHTML = '';
    def.parameters.forEach(p => {
      const group = document.createElement('div');
      group.className = 'form-group';

      const label = document.createElement('label');
      label.textContent = (p.required ? '* ' : '') + p.name + (p.description ? ` (${p.description})` : '');
      group.appendChild(label);

      let input;
      if (p.type === 'enum' && p.options) {
        input = document.createElement('select');
        p.options.forEach(opt => {
          const o = document.createElement('option');
          o.value = opt; o.textContent = opt;
          input.appendChild(o);
        });
      } else if (p.type === 'boolean') {
        const wrap = document.createElement('label');
        wrap.style.display = 'flex'; wrap.style.alignItems = 'center'; wrap.style.gap = '8px';
        input = document.createElement('input');
        input.type = 'checkbox';
        wrap.appendChild(input);
        wrap.appendChild(document.createTextNode(t('enable')));
        group.appendChild(wrap);
        input = wrap; // for event binding later we need the checkbox
      } else {
        input = document.createElement('input');
        if (p.type === 'integer') { input.type = 'number'; input.step = '1'; }
        else if (p.type === 'float') { input.type = 'number'; input.step = 'any'; }
        else { input.type = 'text'; }
      }

      if (input.tagName !== 'LABEL') {
        input.name = p.name;
        if (p.default !== undefined && p.default !== null) {
          if (p.type === 'boolean') input.checked = !!p.default;
          else input.value = String(p.default);
        }
        // path 类型添加浏览按钮
        if (p.type === 'path') {
          const wrap = document.createElement('div');
          wrap.style.display = 'flex'; wrap.style.gap = '8px'; wrap.style.alignItems = 'center';
          input.style.flex = '1';
          wrap.appendChild(input);
          const browseBtn = document.createElement('button');
          browseBtn.type = 'button';
          browseBtn.className = 'btn-small';
          browseBtn.textContent = t('browse');
          browseBtn.onclick = (e) => { e.preventDefault(); openFileBrowser(input, p); };
          wrap.appendChild(browseBtn);
          group.appendChild(wrap);
        } else {
          group.appendChild(input);
        }
      } else {
        // boolean wrapper: extract checkbox
        input = input.querySelector('input');
        input.name = p.name;
        if (p.default !== undefined && p.default !== null) input.checked = !!p.default;
      }

      if (p.constraints && (p.constraints.min !== undefined || p.constraints.max !== undefined)) {
        const hint = document.createElement('span');
        hint.className = 'hint';
        hint.textContent = t('range') + ': ' + (p.constraints.min ?? '-∞') + ' ~ ' + (p.constraints.max ?? '+∞');
        group.appendChild(hint);
      }

      els.configForm.appendChild(group);

      // bind change
      input.addEventListener('input', debounce(() => updatePreview(def.id), 400));
      input.addEventListener('change', () => updatePreview(def.id));
    });

    updatePreview(def.id);
  }

  function collectParams() {
    const params = {};
    els.configForm.querySelectorAll('input, select').forEach(el => {
      if (el.type === 'checkbox') params[el.name] = el.checked;
      else if (el.type === 'number') params[el.name] = el.value === '' ? null : (el.step === '1' ? parseInt(el.value,10) : parseFloat(el.value));
      else params[el.name] = el.value;
    });
    return params;
  }

  async function updatePreview(defId) {
    const params = collectParams();
    try {
      const res = await api('/api/tasks/preview', {
        method: 'POST',
        body: JSON.stringify({ definition_id: defId, params }),
      });
      els.commandPreview.textContent = res.command || '';
    } catch (e) {
      els.commandPreview.textContent = t('param_validation_failed') + ': ' + e.message;
    }
  }

  function closeConfig() {
    els.configSection.classList.add('hidden');
    if (els.tabNav) els.tabNav.classList.remove('hidden');
    if (els.tabContent) els.tabContent.classList.remove('hidden');
    state.currentDefinitionId = null;
    // Restore previous tab if any
    if (state.prevTab) {
      switchTab(state.prevTab);
      state.prevTab = null;
    }
  }

  // Instances
  async function loadInstances() {
    if (!els.instancesList) return;
    els.instancesList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('loading') + '</div>';
    try {
      const params = new URLSearchParams();
      params.set('page', String(state.instancePage));
      params.set('page_size', String(state.instancePageSize));
      const data = await api('/api/tasks/instances?' + params.toString());
      state.instanceTotal = data.total || 0;
      // 更新全量缓存（用于市场卡片状态判断）
      (data.entries || []).forEach(i => state.instances[i.id] = i);
      renderInstances(data.entries || []);
      renderInstancePaging();
    } catch (e) {
      els.instancesList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('load_instances_failed') + ': ' + escapeHtml(e.message) + '</div>';
    }
  }

  function renderInstances(entries) {
    if (entries.length === 0) {
      els.instancesList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('no_instances') + '</div>';
      return;
    }
    els.instancesList.innerHTML = '';
    entries.forEach((inst, idx) => {
      const def = state.definitions[inst.definition_id];
      const row = document.createElement('div');
      row.className = 'instance-row';
      row.dataset.id = inst.id;

      const progress = inst.progress || {};
      const pct = Math.max(0, Math.min(100, progress.percent || 0));

      const statusClass = inst.status;
      const isTerminal = ['stopped','failed','completed'].includes(inst.status);
      const wasInterrupted = inst.progress?.metadata?._was_interrupted === true;

      // 默认全部折叠
      const collapseCls = ' collapsed';

      // 构建结果展示 HTML
      let resultHtml = '';
      const meta = progress.metadata || {};
      if (meta.result !== undefined) {
        const resultText = typeof meta.result === 'object' ? JSON.stringify(meta.result, null, 2) : String(meta.result);
        resultHtml += `<div class="result-block${collapseCls}">
          <div class="result-header" onclick="toggleCollapse(this)">
            <span class="result-label">${t('result')}</span>
            <span class="collapse-icon">▼</span>
          </div>
          <div class="result-body"><pre class="result-pre">${escapeHtml(resultText)}</pre></div>
        </div>`;
      }
      if (meta.files && Array.isArray(meta.files) && meta.files.length > 0) {
        const links = meta.files.map(f => {
          let title, remark, path;
          if (typeof f === 'string') {
            path = f;
            title = f.split('/').pop();
            remark = '';
          } else {
            path = f.path || '';
            title = f.title || f.path.split('/').pop();
            remark = f.remark || '';
          }
          return `<a class="file-link" href="/api/files/download?path=${encodeURIComponent(path)}" target="_blank" download title="${escapeHtml(path)}">
            <span class="file-title">${escapeHtml(title)}</span>
            ${remark ? `<span class="file-remark">${escapeHtml(remark)}</span>` : ''}
          </a>`;
        }).join('');
        resultHtml += `<div class="result-block${collapseCls}">
          <div class="result-header" onclick="toggleCollapse(this)">
            <span class="result-label">${t('files')}</span>
            <span class="collapse-icon">▼</span>
          </div>
          <div class="result-body"><div class="file-links">${links}</div></div>
        </div>`;
      }
      if (wasInterrupted) {
        resultHtml += `<div class="result-block interrupt-block${collapseCls}">
          <div class="result-header" onclick="toggleCollapse(this)">
            <span class="result-label">⚠️ ${t('interrupted')}</span>
            <span class="collapse-icon">▼</span>
          </div>
          <div class="result-body">
            <div style="font-size:12px;color:var(--text-muted);margin-bottom:8px">${t('restart_hint')}</div>
            <button class="btn-primary" data-action="restart" data-id="${inst.id}">🔄 ${t('restart')}</button>
          </div>
        </div>`;
      }

      row.innerHTML = `
        <div class="instance-info">
          <div>
            <span class="id">${inst.id}</span>
            <span class="status-badge ${statusClass}">${inst.status}</span>
            ${def ? `<span style="color:var(--text-muted);font-size:12px">${escapeHtml(getDefName(def))}</span>` : ''}
          </div>
          <div class="cmd">${escapeHtml(inst.command || '')}</div>
          <div style="margin-top:6px;display:flex;align-items:center;gap:12px">
            <div class="progress-bar" title="${progress.message || ''}"><div style="width:${pct}%"></div></div>
            <span style="font-size:11px;color:var(--text-muted)">${pct}% ${progress.message ? '· ' + escapeHtml(progress.message) : ''}</span>
          </div>
          ${resultHtml}
        </div>
        <div class="instance-meta">
          <span style="font-size:11px;color:var(--text-muted)">PID: ${inst.pid ?? '-'}</span>
          <span style="font-size:11px;color:var(--text-muted)">${isTerminal ? t('finished') + ': ' + formatDateTime(inst.finished_at) : t('running') + ': ' + formatDuration(inst.started_at, null)}</span>
          <span style="font-size:11px;color:var(--text-muted)">${t('duration')}: ${formatDuration(inst.created_at, inst.finished_at)}</span>
        </div>
        <div class="instance-actions">
          ${!isTerminal ? `<button class="btn-danger" data-action="stop" data-id="${inst.id}">${t('stop')}</button>` : ''}
          ${isTerminal ? `<button class="btn-small" data-action="delete" data-id="${inst.id}">${t('delete')}</button>` : ''}
          ${wasInterrupted ? `<button class="btn-primary" data-action="restart" data-id="${inst.id}">🔄 ${t('restart')}</button>` : ''}
          <button class="btn-small" data-action="logs" data-id="${inst.id}">${t('logs')}</button>
        </div>
      `;

      row.querySelectorAll('button[data-action]').forEach(btn => {
        btn.onclick = (e) => {
          e.stopPropagation();
          const action = btn.dataset.action;
          const id = btn.dataset.id;
          if (action === 'stop') stopInstance(id);
          if (action === 'delete') deleteInstance(id);
          if (action === 'restart') restartInstance(id);
          if (action === 'logs') showLogs(id);
        };
      });

      els.instancesList.appendChild(row);
    });
  }

  function scrollToInstance(id) {
    const row = document.querySelector(`.instance-row[data-id="${id}"]`);
    if (row) row.scrollIntoView({ behavior: 'smooth', block: 'center' });
  }

  async function stopInstance(id) {
    try {
      await api(`/api/tasks/instances/${id}/stop`, { method: 'POST' });
    } catch (e) {
      alert(t('stop_failed') + ': ' + e.message);
    }
  }

  async function deleteInstance(id) {
    if (!confirm(t('confirm_delete'))) return;
    try {
      await api(`/api/tasks/instances/${id}`, { method: 'DELETE' });
      delete state.instances[id];
      await loadInstances();
      renderMarket();
    } catch (e) {
      alert(t('delete') + ' ' + t('load_failed') + ': ' + e.message);
    }
  }

  async function restartInstance(id) {
    const inst = state.instances[id];
    if (!inst) return;
    if (!confirm(t('confirm_restart', {name: state.definitions[inst.definition_id]?.name || inst.definition_id}))) return;
    try {
      // 先删除旧实例
      await api(`/api/tasks/instances/${id}`, { method: 'DELETE' });
      delete state.instances[id];
      // 使用相同参数启动新实例
      const newInst = await api('/api/tasks/instances', {
        method: 'POST',
        body: JSON.stringify({
          definition_id: inst.definition_id,
          params: inst.params,
        }),
      });
      state.instances[newInst.id] = newInst;
      await loadInstances();
      renderMarket();
    } catch (e) {
      alert(t('reload_failed') + ': ' + e.message);
    }
  }

  // Logs
  async function showLogs(id) {
    state.activeLogId = id;
    const inst = state.instances[id];
    els.logTitle.textContent = id + (inst ? ` (${inst.definition_id})` : '');
    els.logPanel.classList.remove('hidden');
    els.logContent.textContent = t('loading');
    try {
      const data = await api(`/api/tasks/instances/${id}/logs?tail=200`);
      els.logContent.textContent = (data.logs || []).join('\n');
      els.logContent.scrollTop = els.logContent.scrollHeight;
    } catch (e) {
      els.logContent.textContent = t('load_log_failed') + ': ' + e.message;
    }
  }

  function appendLog(line) {
    if (!els.logContent) return;
    const txt = els.logContent;
    txt.textContent += '\n' + line;
    txt.scrollTop = txt.scrollHeight;
  }

  function closeLogs() {
    state.activeLogId = null;
    els.logPanel.classList.add('hidden');
  }

  // ---------- System Monitor ----------
  function renderSystemInfo(info) {
    if (!els.systemMonitor) return;
    const cpu = info.cpu_percent || 0;
    const mem = info.memory || {};
    const disks = info.disks || [];

    function fmtBytes(b) {
      if (!b) return '0 B';
      if (b < 1024) return b + ' B';
      if (b < 1048576) return (b/1024).toFixed(1) + ' KB';
      if (b < 1073741824) return (b/1048576).toFixed(1) + ' MB';
      return (b/1073741824).toFixed(1) + ' GB';
    }
    function fmtUptime(sec) {
      sec = Math.floor(sec || 0);
      const d = Math.floor(sec / 86400);
      const h = Math.floor((sec % 86400) / 3600);
      const m = Math.floor((sec % 3600) / 60);
      const s = sec % 60;
      let parts = [];
      if (d > 0) parts.push(d + '天');
      if (h > 0) parts.push(h + '小时');
      if (m > 0) parts.push(m + '分钟');
      parts.push(s + '秒');
      return parts.join(' ');
    }

    let html = '';
    // Hostname + Timestamp
    const ts = info.timestamp || '';
    html += `<div class="sys-card">
      <div class="sys-label">${t('drone')}</div>
      <div class="sys-value" style="font-size:14px">${escapeHtml(info.hostname || '-')}</div>
      <div class="sys-detail">${ts ? escapeHtml(ts.replace('T', ' ').substring(0, 19)) + ' UTC' : '-'}</div>
    </div>`;
    // System Uptime
    html += `<div class="sys-card">
      <div class="sys-label">${t('system_uptime')}</div>
      <div class="sys-value" style="font-size:13px">${escapeHtml(fmtUptime(info.system_uptime))}</div>
    </div>`;
    // Process Uptime
    html += `<div class="sys-card">
      <div class="sys-label">${t('process_uptime')}</div>
      <div class="sys-value" style="font-size:13px">${escapeHtml(fmtUptime(info.process_uptime))}</div>
    </div>`;
    // CPU
    html += `<div class="sys-card">
      <div class="sys-label">CPU</div>
      <div class="sys-value" style="color:${cpu > 80 ? 'var(--danger)' : 'var(--success)'}">${cpu.toFixed(1)}%</div>
      <div class="sys-bar"><div style="width:${Math.min(100, cpu)}%;background:${cpu > 80 ? 'var(--danger)' : 'var(--success)'}"></div></div>
    </div>`;
    // Memory
    const memPct = mem.percent || 0;
    html += `<div class="sys-card">
      <div class="sys-label">${t('memory')}</div>
      <div class="sys-value" style="color:${memPct > 80 ? 'var(--danger)' : 'var(--accent)'}">${memPct.toFixed(1)}%</div>
      <div class="sys-bar"><div style="width:${Math.min(100, memPct)}%;background:${memPct > 80 ? 'var(--danger)' : 'var(--accent)'}"></div></div>
      <div class="sys-detail">${fmtBytes(mem.used)} / ${fmtBytes(mem.total)}</div>
    </div>`;
    // Disks (multiple partitions)
    disks.forEach(d => {
      const pct = d.percent || 0;
      html += `<div class="sys-card">
        <div class="sys-label">${t('disk')} ${escapeHtml(d.mountpoint)}</div>
        <div class="sys-value" style="color:${pct > 90 ? 'var(--danger)' : pct > 70 ? 'var(--warning)' : 'var(--success)'}">${pct.toFixed(1)}%</div>
        <div class="sys-bar"><div style="width:${Math.min(100, pct)}%;background:${pct > 90 ? 'var(--danger)' : pct > 70 ? 'var(--warning)' : 'var(--success)'}"></div></div>
        <div class="sys-detail">${fmtBytes(d.used)} / ${fmtBytes(d.total)} (${escapeHtml(d.fstype)})</div>
      </div>`;
    });
    // Network interfaces
    const network = info.network || [];
    network.forEach(n => {
      const ipv4Str = (n.ipv4 || []).join(', ') || '-';
      const statusColor = n.is_up ? 'var(--success)' : 'var(--danger)';
      const statusText = n.is_up ? 'UP' : 'DOWN';
      html += `<div class="sys-card">
        <div class="sys-label">${escapeHtml(n.name)} <span style="color:${statusColor};font-size:11px">${statusText}</span></div>
        <div class="sys-value" style="font-size:12px">${escapeHtml(ipv4Str)}</div>
        <div class="sys-detail">MAC: ${escapeHtml(n.mac || '-')}${n.speed ? ' · ' + n.speed + ' Mbps' : ''}${n.mtu ? ' · MTU ' + n.mtu : ''}</div>
      </div>`;
    });

    els.systemMonitor.innerHTML = html;
  }

  // ---------- History ----------
  async function loadHistory() {
    if (!els.historyList) return;
    els.historyList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('loading') + '</div>';
    try {
      const params = new URLSearchParams();
      params.set('page', String(state.historyPage));
      params.set('page_size', String(state.historyPageSize));
      if (state.historyFilter) params.set('definition_id', state.historyFilter);
      const data = await api('/api/history?' + params.toString());
      state.historyTotal = data.total || 0;
      renderHistory(data.entries || []);
      renderHistoryPaging();
    } catch (e) {
      els.historyList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('load_history_failed') + ': ' + escapeHtml(e.message) + '</div>';
    }
  }

  function renderHistoryPaging() {
    if (!els.historyPaging) return;
    const total = state.historyTotal;
    const pageSize = state.historyPageSize;
    const page = state.historyPage;
    const totalPages = Math.max(1, Math.ceil(total / pageSize));
    if (els.historyPagingInfo) {
      els.historyPagingInfo.textContent = t('paging_info', {total: total, page: page, totalPages: totalPages});
    }
    let html = '';
    html += '<button class="btn-small" ' + (page <= 1 ? 'disabled' : '') + ' data-page="' + (page - 1) + '">' + t('prev_page') + '</button>';
    for (let i = 1; i <= totalPages; i++) {
      if (i === 1 || i === totalPages || (i >= page - 2 && i <= page + 2)) {
        html += '<button class="btn-small" ' + (i === page ? 'disabled style=\'background:var(--accent);color:#fff\'' : '') + ' data-page="' + i + '">' + i + '</button>';
      } else if (i === page - 3 || i === page + 3) {
        html += '<span style="color:var(--text-muted)">...</span>';
      }
    }
    html += '<button class="btn-small" ' + (page >= totalPages ? 'disabled' : '') + ' data-page="' + (page + 1) + '">' + t('next_page') + '</button>';
    els.historyPaging.innerHTML = html;
    els.historyPaging.querySelectorAll('button[data-page]').forEach(btn => {
      btn.onclick = () => {
        state.historyPage = parseInt(btn.dataset.page);
        loadHistory();
      };
    });
  }

  function renderInstancePaging() {
    if (!els.instancePaging) return;
    const total = state.instanceTotal;
    const pageSize = state.instancePageSize;
    const page = state.instancePage;
    const totalPages = Math.max(1, Math.ceil(total / pageSize));
    if (els.instancePagingInfo) {
      els.instancePagingInfo.textContent = t('paging_info', {total: total, page: page, totalPages: totalPages});
    }
    let html = '';
    html += '<button class="btn-small" ' + (page <= 1 ? 'disabled' : '') + ' data-page="' + (page - 1) + '">' + t('prev_page') + '</button>';
    for (let i = 1; i <= totalPages; i++) {
      if (i === 1 || i === totalPages || (i >= page - 2 && i <= page + 2)) {
        html += '<button class="btn-small" ' + (i === page ? 'disabled style=\'background:var(--accent);color:#fff\'' : '') + ' data-page="' + i + '">' + i + '</button>';
      } else if (i === page - 3 || i === page + 3) {
        html += '<span style="color:var(--text-muted)">...</span>';
      }
    }
    html += '<button class="btn-small" ' + (page >= totalPages ? 'disabled' : '') + ' data-page="' + (page + 1) + '">' + t('next_page') + '</button>';
    els.instancePaging.innerHTML = html;
    els.instancePaging.querySelectorAll('button[data-page]').forEach(btn => {
      btn.onclick = () => {
        state.instancePage = parseInt(btn.dataset.page);
        loadInstances();
      };
    });
  }

  function populateHistoryFilter() {
    if (!els.historyFilter) return;
    const current = els.historyFilter.value;
    let html = '<option value="">' + t('all_apps') + '</option>';
    Object.values(state.definitions).forEach(def => {
      html += '<option value="' + escapeHtml(def.id) + '">' + escapeHtml(getDefName(def)) + '</option>';
    });
    els.historyFilter.innerHTML = html;
    els.historyFilter.value = current;
  }

  function renderHistory(entries) {
    if (!entries.length) {
      els.historyList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('no_history') + '</div>';
      return;
    }
    els.historyList.innerHTML = '';
    entries.forEach((h, idx) => {
      const def = state.definitions[h.definition_id];
      const row = document.createElement('div');
      row.className = 'history-row';

      const statusClass = h.status;
      const meta = h.progress?.metadata || {};

      // 默认展开最新的一个，其余折叠
      const isFirst = idx === 0;
      const collapseCls = isFirst ? '' : ' collapsed';

      // Build file links with existence check on click
      let filesHtml = '';
      if (meta.files && Array.isArray(meta.files) && meta.files.length > 0) {
        const links = meta.files.map(f => {
          let title, path;
          if (typeof f === 'string') {
            path = f;
            title = f.split('/').pop();
          } else {
            path = f.path || '';
            title = f.title || f.path.split('/').pop();
          }
          return `<a class="file-link" href="/api/files/download?path=${encodeURIComponent(path)}" target="_blank" download title="${escapeHtml(path)}" onclick="return checkFileExists(this)">
            <span class="file-title">${escapeHtml(title)}</span>
          </a>`;
        }).join('');
        filesHtml = `<div class="result-block${collapseCls}">
          <div class="result-header" onclick="toggleCollapse(this)">
            <span class="result-label">文件</span>
            <span class="collapse-icon">▼</span>
          </div>
          <div class="result-body"><div class="history-files">${links}</div></div>
        </div>`;
      }

      let resultHtml = '';
      if (meta.result !== undefined) {
        const resultText = typeof meta.result === 'object' ? JSON.stringify(meta.result, null, 2) : String(meta.result);
        resultHtml = `<div class="result-block${collapseCls}">
          <div class="result-header" onclick="toggleCollapse(this)">
            <span class="result-label">结果</span>
            <span class="collapse-icon">▼</span>
          </div>
          <div class="result-body"><pre class="result-pre">${escapeHtml(resultText)}</pre></div>
        </div>`;
      }

      row.innerHTML = `
        <div class="history-meta">
          <span class="id">${h.id}</span>
          <span class="status-badge ${statusClass}">${h.status}</span>
          ${def ? `<span style="color:var(--text-muted);font-size:12px">${escapeHtml(getDefName(def))}</span>` : ''}
          <span style="color:var(--text-muted);font-size:11px">完成: ${formatDateTime(h.finished_at)}</span>
          <span style="color:var(--text-muted);font-size:11px">耗时: ${formatDuration(h.created_at, h.finished_at)}</span>
        </div>
        <div class="cmd" style="font-size:11px;color:var(--text-muted);margin-top:4px">${escapeHtml(h.command || '')}</div>
        ${resultHtml}
        ${filesHtml}
      `;
      els.historyList.appendChild(row);
    });
  }

  window.checkFileExists = async function(el) {
    try {
      const res = await fetch(el.href, {method: 'HEAD'});
      if (!res.ok) {
        el.classList.add('missing');
        el.title = t('file_deleted');
        el.removeAttribute('href');
        return false;
      }
      return true;
    } catch (e) {
      return true; // fallback to let browser handle
    }
  };

  async function cleanupHistory() {
    const days = prompt(t('keep_history_days'), '30');
    if (!days) return;
    try {
      const res = await api(`/api/history/cleanup?days=${encodeURIComponent(days)}`, {method: 'POST'});
      alert(t('cleaned_records', {count: res.removed}));
      loadHistory();
    } catch (e) {
      alert(t('cleanup_failed') + ': ' + e.message);
    }
  }

  // ---------- Files ----------
  function formatFileSize(bytes) {
    if (bytes < 1024) return bytes + ' B';
    if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
    if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB';
    return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB';
  }

  async function loadFiles() {
    if (!els.filesList) return;
    els.filesList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('loading') + '</div>';
    try {
      const params = new URLSearchParams();
      params.set('dir', state.filesDir);
      params.set('page', String(state.filesPage));
      params.set('page_size', String(state.filesPageSize));
      // saveimg / savebag 根目录下支持过滤
      const dirBase = state.filesDir.replace(/^~\//, '');
      let dirNorm = state.filesDir.replace(/\\/g, '/');
      if ((dirBase === 'saveimg' || dirNorm.endsWith('/saveimg')) && state.filesImgFilter) {
        params.set('prefix', state.filesImgFilter);
      }
      if ((dirBase === 'savebag' || dirNorm.endsWith('/savebag')) && state.filesBagFilter) {
        params.set('ext', state.filesBagFilter);
      }
      if ((dirBase === 'savelog' || dirNorm.endsWith('/savelog')) && state.filesLogFilter) {
        params.set('ext', state.filesLogFilter);
      }
      const data = await api('/api/files/browse?' + params.toString());
      state.filesTotal = data.total || 0;
      state.filesTotalSize = data.total_size || 0;
      if (els.filesCurrentDir) els.filesCurrentDir.textContent = data.dir || state.filesDir;
      renderFiles(data.items || []);
      renderFilesPaging();
      renderImgFilter(data.dir || state.filesDir);
      renderBagFilter(data.dir || state.filesDir);
      renderLogFilter(data.dir || state.filesDir);
      // 控制返回上一层按钮显示，并同步根目录下拉框
      dirNorm = (data.dir || state.filesDir).replace(/\\/g, '/');
      const rootMap = {savemap:'~/savemap', saveimg:'~/saveimg', savemission:'~/savemission', savebag:'~/savebag', savelog:'~/savelog', savepcd:'~/savepcd'};
      let matchedRoot = null;
      for (const name of Object.keys(rootMap)) {
        if (dirNorm.endsWith('/' + name)) {
          matchedRoot = rootMap[name];
          break;
        }
      }
      if (els.btnFilesUp) {
        els.btnFilesUp.style.display = matchedRoot ? 'none' : 'inline-block';
      }
      if (els.filesRootSelect && matchedRoot) {
        els.filesRootSelect.value = matchedRoot;
      }
    } catch (e) {
      els.filesList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('load_failed') + ': ' + escapeHtml(e.message) + '</div>';
    }
  }

  function renderFiles(items) {
    if (items.length === 0) {
      els.filesList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('empty_dir') + '</div>';
      return;
    }
    let html = '';
    html += '<div class="file-table-header" style="display:grid;grid-template-columns:2fr 1fr 1.5fr 1fr;gap:8px;padding:8px 12px;font-size:12px;color:var(--text-muted);border-bottom:1px solid var(--border);">';
    html += '<span>' + t('file_name') + '</span><span>' + t('file_size') + '</span><span>' + t('file_mtime') + '</span><span style="text-align:right">' + t('action') + '</span>';
    html += '</div>';
    if (state.filesTotalSize > 0) {
      html += '<div style="font-size:12px;color:var(--text-muted);padding:4px 12px;">' + t('total_size') + ': ' + formatFileSize(state.filesTotalSize) + '</div>';
    }
    items.forEach(item => {
      const isDir = item.is_dir;
      const name = escapeHtml(item.name);
      const size = formatFileSize(item.size || 0);
      const mtime = item.mtime ? item.mtime.replace('T', ' ').substring(0, 19) : '-';
      const icon = isDir ? '📁' : '📄';
      const ext = item.name.split('.').pop().toLowerCase();
      const isImg = !isDir && ['jpg','jpeg','png','gif','webp','bmp'].includes(ext);
      const isVideo = !isDir && ['mp4','webm','mov'].includes(ext);
      const isPcd = !isDir && ext === 'pcd';
      const isText = !isDir && ['traj','txt','log','yaml','yml','json','csv','xml','md','py','sh','cpp','h','c','js','html','css','mission','plan'].includes(ext);
      const isJson = !isDir && ['snapshot','json'].includes(ext);
      const previewBtn = (isImg || isVideo || isPcd || isText || isJson)
        ? `<button class="preview-btn" data-preview="${escapeHtml(item.path)}" data-name="${escapeHtml(item.name)}" data-type="${isImg ? 'img' : isVideo ? 'video' : isPcd ? 'pcd' : isJson ? 'json' : 'text'}">${t('preview')}</button>`
        : '';
      const downloadBtn = !isDir
        ? `<a href="/api/files/download?path=${encodeURIComponent(item.path)}" target="_blank" class="btn-small" style="text-decoration:none">${t('download')}</a>`
        : '';
      const deleteBtn = `<button class="btn-small btn-danger" data-delete="${escapeHtml(item.path)}" style="margin-left:4px">${t('delete')}</button>`;
      const navAction = isDir
        ? `onclick="window.enterDir('${escapeHtml(item.path)}')" style="cursor:pointer"`
        : '';
      html += `<div class="file-table-row" style="display:grid;grid-template-columns:2fr 1fr 1.5fr 1fr;gap:8px;padding:8px 12px;align-items:center;border-bottom:1px solid var(--border);${isDir ? 'cursor:pointer;' : ''}" ${navAction} onmouseenter="this.style.background='var(--bg-tertiary)'" onmouseleave="this.style.background=''">`;
      html += `<span>${icon} ${name}</span>`;
      html += `<span style="font-size:12px;color:var(--text-muted)">${size}</span>`;
      html += `<span style="font-size:12px;color:var(--text-muted)">${mtime}</span>`;
      html += `<span style="text-align:right">${previewBtn}${downloadBtn}${deleteBtn}</span>`;
      html += '</div>';
    });
    els.filesList.innerHTML = html;
    // Bind delete buttons
    els.filesList.querySelectorAll('button[data-delete]').forEach(btn => {
      btn.onclick = (e) => {
        e.stopPropagation();
        deleteFile(btn.dataset.delete);
      };
    });
    // Bind preview buttons
    els.filesList.querySelectorAll('button[data-preview]').forEach(btn => {
      btn.onclick = (e) => {
        e.stopPropagation();
        previewFile(btn.dataset.preview, btn.dataset.name, btn.dataset.type);
      };
    });
  }

  window.enterDir = function(dirPath) {
    state.filesDir = dirPath;
    localStorage.setItem('dtm-files-dir', dirPath);
    state.filesPage = 1;
    loadFiles();
  };

  window.previewFile = function(path, name, type) {
    if (type === 'pcd') {
      window.open('/static/pcd-preview.html?path=' + encodeURIComponent(path), '_blank');
      return;
    }
    if (!els.filePreview || !els.filePreviewContainer) return;
    els.filePreviewTitle.textContent = name || t('preview');
    const url = '/api/files/download?path=' + encodeURIComponent(path);
    if (type === 'img') {
      els.filePreviewContainer.innerHTML = `<img src="${url}" class="preview-img" alt="${escapeHtml(name || '')}">`;
    } else if (type === 'video') {
      els.filePreviewContainer.innerHTML = `<video src="${url}" class="preview-video" controls autoplay playsinline style="max-width:100%;max-height:calc(90vh - 60px);"></video>`;
    } else if (type === 'text' || type === 'json') {
      els.filePreviewContainer.innerHTML = `<div style="display:flex;justify-content:center;align-items:center;height:100%;"><div class="spinner"></div></div>`;
      fetch(url).then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); }).then(text => {
        let displayHtml = '';
        if (type === 'json' || (name && (name.endsWith('.mission') || name.endsWith('.plan')))) {
          try {
            const obj = JSON.parse(text);
            displayHtml = formatJson(obj);
          } catch (e) {
            displayHtml = escapeHtml(text);
          }
        } else {
          displayHtml = escapeHtml(text);
        }
        els.filePreviewContainer.innerHTML = `<pre style="max-width:90vw;max-height:calc(90vh - 60px);overflow:auto;background:#1a1a2e;color:#e0e0e0;padding:16px;border-radius:6px;font-size:13px;line-height:1.5;margin:0;text-align:left;white-space:pre-wrap;word-wrap:break-word;">${displayHtml}</pre>`;
      }).catch(err => {
        els.filePreviewContainer.innerHTML = `<div style="color:#ef4444;padding:20px;">加载失败: ${escapeHtml(err.message)}</div>`;
      });
    }
    els.filePreview.classList.remove('hidden');
  };

  window.closePreview = function() {
    if (!els.filePreview) return;
    els.filePreview.classList.add('hidden');
    if (els.filePreviewContainer) els.filePreviewContainer.innerHTML = '';
  };

  window.goUpDir = function() {
    const current = state.filesDir.replace(/\\/g, '/');
    const lastSlash = current.lastIndexOf('/');
    if (lastSlash > 0) {
      const parent = current.substring(0, lastSlash);
      state.filesDir = parent;
      localStorage.setItem('dtm-files-dir', parent);
      state.filesPage = 1;
      state.filesImgFilter = '';
      state.filesBagFilter = '';
      state.filesLogFilter = '';
      loadFiles();
    }
  };

  function renderImgFilter(dirPath) {
    if (!els.filesImgFilter) return;
    const isSaveimgRoot = dirPath === '~/saveimg' || (dirPath && dirPath.replace(/\\/g, '/').endsWith('/saveimg'));
    els.filesImgFilter.style.display = isSaveimgRoot ? 'flex' : 'none';
    if (!isSaveimgRoot) return;
    els.filesImgFilter.querySelectorAll('.img-filter-btn').forEach(btn => {
      btn.classList.toggle('active', btn.dataset.filter === state.filesImgFilter);
      btn.onclick = () => {
        state.filesImgFilter = btn.dataset.filter;
        state.filesPage = 1;
        localStorage.setItem('dtm-files-img-filter', state.filesImgFilter);
        renderImgFilter(dirPath);
        loadFiles();
      };
    });
  }

  function renderBagFilter(dirPath) {
    if (!els.filesBagFilter) return;
    const isSavebagRoot = dirPath === '~/savebag' || (dirPath && dirPath.replace(/\\/g, '/').endsWith('/savebag'));
    els.filesBagFilter.style.display = isSavebagRoot ? 'flex' : 'none';
    if (!isSavebagRoot) return;
    els.filesBagFilter.querySelectorAll('.bag-filter-btn').forEach(btn => {
      btn.classList.toggle('active', btn.dataset.filter === state.filesBagFilter);
      btn.onclick = () => {
        state.filesBagFilter = btn.dataset.filter;
        state.filesPage = 1;
        localStorage.setItem('dtm-files-bag-filter', state.filesBagFilter);
        renderBagFilter(dirPath);
        loadFiles();
      };
    });
  }

  function renderLogFilter(dirPath) {
    if (!els.filesLogFilter) return;
    const isSavelogRoot = dirPath === '~/savelog' || (dirPath && dirPath.replace(/\\/g, '/').endsWith('/savelog'));
    els.filesLogFilter.style.display = isSavelogRoot ? 'flex' : 'none';
    if (!isSavelogRoot) return;
    els.filesLogFilter.querySelectorAll('.log-filter-btn').forEach(btn => {
      btn.classList.toggle('active', btn.dataset.filter === state.filesLogFilter);
      btn.onclick = () => {
        state.filesLogFilter = btn.dataset.filter;
        state.filesPage = 1;
        localStorage.setItem('dtm-files-log-filter', state.filesLogFilter);
        renderLogFilter(dirPath);
        loadFiles();
      };
    });
  }

  async function deleteFile(path) {
    if (!confirm(t('confirm_delete_file') + '\n' + path)) return;
    try {
      await api(`/api/files/delete?path=${encodeURIComponent(path)}`, { method: 'DELETE' });
      loadFiles();
    } catch (e) {
      alert(t('delete') + ' ' + t('load_failed') + ': ' + e.message);
    }
  }

  async function uploadFile(file) {
    if (!els.filesList) return;
    try {
      const form = new FormData();
      form.append('file', file);
      const res = await fetch(`${API_BASE}/api/files/upload?dir=${encodeURIComponent(state.filesDir)}`, {
        method: 'POST',
        body: form,
      });
      if (!res.ok) throw new Error(await res.text());
      loadFiles();
    } catch (e) {
      alert(t('upload_failed') + ': ' + e.message);
    }
  }

  let _packDownloading = false;

  async function downloadCurrentDir(format) {
    if (_packDownloading) return;
    const dir = state.filesDir;
    if (!dir) { alert(t('empty_dir')); return; }

    _packDownloading = true;
    if (els.btnFilesDownloadZip) els.btnFilesDownloadZip.disabled = true;
    if (els.btnFilesDownloadTar) els.btnFilesDownloadTar.disabled = true;
    if (els.filesPackProgress) els.filesPackProgress.style.display = 'flex';
    if (els.filesPackBar) els.filesPackBar.style.width = '0%';
    if (els.filesPackPct) els.filesPackPct.textContent = '0%';

    const url = format === 'zip'
      ? `${API_BASE}/api/files/download-zip?dir=${encodeURIComponent(dir)}`
      : `${API_BASE}/api/files/download-tar?dir=${encodeURIComponent(dir)}`;
    const filename = format === 'zip' ? 'download.zip' : 'download.tar';

    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(await response.text());

      const contentLength = +(response.headers.get('Content-Length') || 0);
      const reader = response.body.getReader();
      const chunks = [];
      let received = 0;

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        chunks.push(value);
        received += value.length;
        if (contentLength) {
          const pct = Math.round((received / contentLength) * 100);
          if (els.filesPackBar) els.filesPackBar.style.width = pct + '%';
          if (els.filesPackPct) els.filesPackPct.textContent = pct + '%';
        } else {
          if (els.filesPackPct) els.filesPackPct.textContent = formatFileSize(received);
        }
      }

      const blob = new Blob(chunks);
      const blobUrl = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = blobUrl;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(blobUrl);
    } catch (e) {
      alert(t('pack_failed') + ': ' + e.message);
    } finally {
      _packDownloading = false;
      if (els.btnFilesDownloadZip) els.btnFilesDownloadZip.disabled = false;
      if (els.btnFilesDownloadTar) els.btnFilesDownloadTar.disabled = false;
      if (els.filesPackProgress) els.filesPackProgress.style.display = 'none';
    }
  }

  function renderFilesPaging() {
    if (!els.filesPaging) return;
    const total = state.filesTotal;
    const pageSize = state.filesPageSize;
    const page = state.filesPage;
    const totalPages = Math.max(1, Math.ceil(total / pageSize));
    if (els.filesPagingInfo) {
      els.filesPagingInfo.textContent = t('paging_info', {total: total, page: page, totalPages: totalPages});
    }
    let html = '';
    html += '<button class="btn-small" ' + (page <= 1 ? 'disabled' : '') + ' data-page="' + (page - 1) + '">' + t('prev_page') + '</button>';
    for (let i = 1; i <= totalPages; i++) {
      if (i === 1 || i === totalPages || (i >= page - 2 && i <= page + 2)) {
        html += '<button class="btn-small" ' + (i === page ? 'disabled style=\'background:var(--accent);color:#fff\'' : '') + ' data-page="' + i + '">' + i + '</button>';
      } else if (i === page - 3 || i === page + 3) {
        html += '<span style="color:var(--text-muted)">...</span>';
      }
    }
    html += '<button class="btn-small" ' + (page >= totalPages ? 'disabled' : '') + ' data-page="' + (page + 1) + '">' + t('next_page') + '</button>';
    els.filesPaging.innerHTML = html;
    els.filesPaging.querySelectorAll('button[data-page]').forEach(btn => {
      btn.onclick = () => {
        state.filesPage = parseInt(btn.dataset.page);
        loadFiles();
      };
    });
  }

  // ---------- Updater ----------
  async function loadUpdater() {
    if (!els.updaterList) return;
    try {
      const data = await api('/api/updater');
      renderUpdaterPackages(data.packages || []);
    } catch (e) {
      console.error('load updater failed:', e);
    }
  }

  function renderUpdaterPackages(packages) {
    if (!els.updaterList) return;
    if (packages.length === 0) {
      els.updaterList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('no_updates') + '</div>';
      return;
    }
    let html = '';
    packages.forEach(pkg => {
      const size = formatFileSize(pkg.size);
      const mtime = pkg.mtime ? pkg.mtime.replace('T', ' ').substring(0, 19) : '-';
      html += `<div class="app-row">
        <div>
          <div class="app-name">📦 ${escapeHtml(pkg.filename)}</div>
          <div class="app-meta">${size} · ${mtime}</div>
        </div>
        <a href="/api/files/download?path=${encodeURIComponent(pkg.path)}" target="_blank" class="btn-small" style="text-decoration:none">${t('download')}</a>
        <button class="btn-danger btn-small" data-delete="${escapeHtml(pkg.filename)}">${t('delete')}</button>
      </div>`;
    });
    els.updaterList.innerHTML = html;
    els.updaterList.querySelectorAll('button[data-delete]').forEach(btn => {
      btn.onclick = () => deleteUpdatePackage(btn.dataset.delete);
    });
  }

  async function uploadUpdatePackage(file) {
    try {
      const form = new FormData();
      form.append('file', file);
      const res = await fetch(`${API_BASE}/api/updater/upload`, {
        method: 'POST',
        body: form,
      });
      if (!res.ok) throw new Error(await res.text());
      loadUpdater();
    } catch (e) {
      alert(t('upload_failed') + ': ' + e.message);
    }
  }

  async function deleteUpdatePackage(filename) {
    if (!confirm(t('confirm_delete_file') + '\n' + filename)) return;
    try {
      await api(`/api/updater/${encodeURIComponent(filename)}`, { method: 'DELETE' });
      loadUpdater();
    } catch (e) {
      alert(t('delete') + ' ' + t('load_failed') + ': ' + e.message);
    }
  }

  // Init
  async function init() {
    connectWS();
    try {
      const defs = await api('/api/tasks/definitions?lang=' + currentLang);
      state.definitions = defs;
      renderMarket();
      populateHistoryFilter();
    } catch (e) {
      console.error(t('load_defs_failed'), e);
    }
    await loadInstances();
    loadHistory();
    loadApps();
    loadFiles();
    // 如果当前是 saveimg / savebag，提前渲染过滤 tab
    if (state.filesDir === '~/saveimg' || state.filesDir.endsWith('/saveimg')) {
      renderImgFilter(state.filesDir);
    }
    if (state.filesDir === '~/savebag' || state.filesDir.endsWith('/savebag')) {
      renderBagFilter(state.filesDir);
    }
    if (state.filesDir === '~/savelog' || state.filesDir.endsWith('/savelog')) {
      renderLogFilter(state.filesDir);
    }
    loadUpdater();
    initMediaService();
    initPushStream();
    initDetection();
    initSettings();
    loadMissions();
    // 如果之前保存的 active tab 是已移除的 updater，回退到 system
    const validTabs = ['system', 'market', 'instances', 'files', 'missions', 'history', 'services', 'live', 'push', 'detect'];
    if (state.activeTab === 'settings') {
      state.activeTab = 'system';
      localStorage.setItem('dtm-active-tab', 'system');
    }
    if (!validTabs.includes(state.activeTab)) {
      state.activeTab = 'system';
      localStorage.setItem('dtm-active-tab', 'system');
    }
    switchTab(state.activeTab);
  }

  // Events
  els.backBtn.onclick = closeConfig;

  async function doStart(defId, params, forceIgnoreLock) {
    els.startBtn.disabled = true;
    els.startBtn.textContent = t('starting');
    try {
      const body = { definition_id: defId, params };
      if (forceIgnoreLock) body.force_ignore_lock = true;
      const inst = await api('/api/tasks/instances', {
        method: 'POST',
        body: JSON.stringify(body),
      });
      state.instances[inst.id] = inst;
      renderInstances([inst]);
      renderMarket();
      state.prevTab = 'instances';
      closeConfig();
    } catch (e) {
      const msg = e.message || '';
      if (!forceIgnoreLock && msg.includes('423')) {
        // 第一次确认
        if (confirm(t('lock_detected'))) {
          // 第二次确认
          if (confirm(t('lock_confirm'))) {
            return doStart(defId, params, true);
          }
        }
      } else {
        alert(t('start_failed') + ': ' + e.message);
      }
      els.startBtn.disabled = false;
      els.startBtn.textContent = '▶ 启动任务';
    }
  }

  els.startBtn.onclick = async () => {
    const defId = state.currentDefinitionId;
    if (!defId) return;
    const params = collectParams();
    await doStart(defId, params, false);
  };
  els.closeLog.onclick = closeLogs;
  els.closeFileBrowser.onclick = closeFileBrowser;
  if (els.closeFilePreview) {
    els.closeFilePreview.onclick = window.closePreview;
    els.filePreview.onclick = (e) => {
      if (e.target === els.filePreview) window.closePreview();
    };
  }
  if (els.btnLoadHistory) els.btnLoadHistory.onclick = loadHistory;
  if (els.btnCleanupHistory) els.btnCleanupHistory.onclick = cleanupHistory;
  if (els.historyFilter) {
    els.historyFilter.onchange = () => {
      state.historyFilter = els.historyFilter.value;
      state.historyPage = 1;
      loadHistory();
    };
  }
  if (els.historyPageSize) {
    els.historyPageSize.onchange = () => {
      state.historyPageSize = parseInt(els.historyPageSize.value) || 5;
      state.historyPage = 1;
      loadHistory();
    };
  }
  if (els.instancePageSize) {
    els.instancePageSize.value = state.instancePageSize;
    els.instancePageSize.onchange = () => {
      state.instancePageSize = parseInt(els.instancePageSize.value) || 10;
      state.instancePage = 1;
      loadInstances();
    };
  }
  if (els.missionsPageSize) {
    els.missionsPageSize.value = state.missionsPageSize;
    els.missionsPageSize.onchange = () => {
      state.missionsPageSize = parseInt(els.missionsPageSize.value) || 20;
      state.missionsPage = 1;
      loadMissions();
    };
  }

  // Files events
  if (els.filesRootSelect) {
    els.filesRootSelect.value = state.filesDir;
    els.filesRootSelect.onchange = () => {
      state.filesDir = els.filesRootSelect.value;
      localStorage.setItem('dtm-files-dir', state.filesDir);
      state.filesPage = 1;
      state.filesImgFilter = '';
      state.filesBagFilter = '';
      loadFiles();
    };
  }
  if (els.btnFilesRefresh) els.btnFilesRefresh.onclick = loadFiles;
  if (els.btnFilesDownloadZip) els.btnFilesDownloadZip.onclick = () => downloadCurrentDir('zip');
  if (els.btnFilesDownloadTar) els.btnFilesDownloadTar.onclick = () => downloadCurrentDir('tar');
  if (els.btnFilesUpload) els.btnFilesUpload.onclick = () => els.filesUploadInput?.click();
  if (els.filesUploadInput) {
    els.filesUploadInput.onchange = (ev) => {
      const file = ev.target.files[0];
      if (file) uploadFile(file);
      ev.target.value = '';
    };
  }

  // Tab nav events
  if (els.tabNav) {
    els.tabNav.querySelectorAll('.tab-btn').forEach(btn => {
      btn.onclick = () => switchTab(btn.dataset.tab);
    });
    const btnHeaderSettings = document.getElementById('btn-header-settings');
    if (btnHeaderSettings) {
      btnHeaderSettings.onclick = () => switchTab('settings');
    }
  }

  // App upload events
  if (els.appUploadArea) {
    els.appUploadArea.onclick = () => els.appFileInput?.click();
    els.appUploadArea.ondragover = (ev) => { ev.preventDefault(); els.appUploadArea.classList.add('dragover'); };
    els.appUploadArea.ondragleave = () => els.appUploadArea.classList.remove('dragover');
    els.appUploadArea.ondrop = (ev) => {
      ev.preventDefault();
      els.appUploadArea.classList.remove('dragover');
      const file = ev.dataTransfer.files[0];
      if (file) uploadApp(file);
    };
  }
  if (els.appFileInput) {
    els.appFileInput.onchange = (ev) => {
      const file = ev.target.files[0];
      if (file) uploadApp(file);
      ev.target.value = ''; // reset
    };
  }
  if (els.btnRefreshApps) els.btnRefreshApps.onclick = loadApps;

  // Updater events
  if (els.updaterUploadArea) {
    els.updaterUploadArea.onclick = () => els.updaterFileInput?.click();
    els.updaterUploadArea.ondragover = (ev) => { ev.preventDefault(); els.updaterUploadArea.classList.add('dragover'); };
    els.updaterUploadArea.ondragleave = () => els.updaterUploadArea.classList.remove('dragover');
    els.updaterUploadArea.ondrop = (ev) => {
      ev.preventDefault();
      els.updaterUploadArea.classList.remove('dragover');
      const file = ev.dataTransfer.files[0];
      if (file) uploadUpdatePackage(file);
    };
  }
  if (els.updaterFileInput) {
    els.updaterFileInput.onchange = (ev) => {
      const file = ev.target.files[0];
      if (file) uploadUpdatePackage(file);
      ev.target.value = '';
    };
  }

  // File browser
  function openFileBrowser(inputEl, param) {
    els.fileBrowser.classList.remove('hidden');
    els.fileList.innerHTML = '<div class="text-muted" style="padding:12px">加载中...</div>';

    // 优先使用服务端预设的 choices API（基于任务定义中的 source_dir）
    const defId = state.currentDefinitionId;
    api(`/api/files/choices?definition_id=${encodeURIComponent(defId)}&parameter_name=${encodeURIComponent(param.name)}`)
      .then(data => renderFileList(data, inputEl))
      .catch(err => {
        // 如果 choices API 不可用（无 source_dir 配置），t('fallback_browser')
        fallbackFileBrowser(inputEl, param);
      });
  }

  function fallbackFileBrowser(inputEl, param) {
    // 确定目录：优先取输入框值的目录，否则取默认值的目录
    let dir = inputEl.value || param.default || '/home/ramy';
    // 如果 dir 看起来是文件路径，取父目录
    if (dir.match(/\.[a-zA-Z0-9]+$/)) {
      dir = dir.substring(0, dir.lastIndexOf('/')) || '/';
    }
    // 确定 pattern
    let pattern = '*';
    if (param.constraints && param.constraints.regex) {
      const rx = param.constraints.regex;
      if (rx.includes('\\.bag')) pattern = '*.bag';
      else if (rx.includes('\\.json')) pattern = '*.json';
      else if (rx.includes('\\.yaml')) pattern = '*.yaml';
      else if (rx.includes('\\.py')) pattern = '*.py';
    }
    if (!pattern && (param.name.includes('bag') || (param.description || '').includes('bag'))) {
      pattern = '*.bag';
    }

    api(`/api/files?dir=${encodeURIComponent(dir)}&pattern=${encodeURIComponent(pattern)}`)
      .then(data => renderFileList(data, inputEl))
      .catch(e => {
        els.fileList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('load_failed') + ': ' + escapeHtml(e.message) + '</div>';
      });
  }

  function renderFileList(data, inputEl) {
    if (!data.files || data.files.length === 0) {
      els.fileList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('empty_dir') + '</div>';
      return;
    }
    els.fileList.innerHTML = '';
    data.files.forEach(f => {
      const item = document.createElement('div');
      item.className = 'file-item';
      const isDir = f.name.endsWith('/');
      const size = isDir ? t('directory') : (f.size < 1024 ? f.size + ' B' : f.size < 1048576 ? (f.size/1024).toFixed(1) + ' KB' : (f.size/1048576).toFixed(1) + ' MB');
      const dirIcon = isDir ? '📁 ' : '';
      const displayName = isDir ? f.name.slice(0, -1) : f.name;
      item.innerHTML = `<span class="fname">${dirIcon}${escapeHtml(displayName)}</span><span class="fmeta">${size}</span>`;
      if (isDir) {
        item.style.color = 'var(--accent)';
        item.style.fontWeight = '600';
      }
      item.onclick = () => {
        inputEl.value = f.path;
        closeFileBrowser();
        // 触发 preview 更新
        const defId = state.currentDefinitionId;
        if (defId) updatePreview(defId);
      };
      els.fileList.appendChild(item);
    });
  }

  function closeFileBrowser() {
    els.fileBrowser.classList.add('hidden');
  }

  // App management
  async function uploadApp(file) {
    if (!file.name.toLowerCase().endsWith('.lldtp')) {
      alert(t('lldtp_only'));
      return;
    }
    const form = new FormData();
    form.append('file', file);
    try {
      const res = await fetch(`${API_BASE}/api/apps/upload`, {
        method: 'POST',
        body: form,
      });
      if (!res.ok) {
        const text = await res.text();
        throw new Error(`${res.status}: ${text}`);
      }
      const data = await res.json();
      alert(t('install_success', {name: data.app?.name || data.app?.app_id || '', version: data.app.full_id}));
      // 刷新任务定义和app列表
      const defs = await api('/api/tasks/definitions');
      state.definitions = defs;
      renderMarket();
      loadApps();
    } catch (e) {
      alert(t('install_failed') + ': ' + e.message);
    }
  }

  async function loadApps() {
    if (!els.appsList) return;
    try {
      const data = await api('/api/apps');
      renderApps(data.apps || []);
    } catch (e) {
      els.appsList.innerHTML = '<div class="text-muted">' + t('load_apps_failed') + ': ' + escapeHtml(e.message) + '</div>';
    }
  }

  function renderApps(apps) {
    if (!els.appsList) return;
    if (apps.length === 0) {
      els.appsList.innerHTML = '<div class="text-muted">' + t('no_apps') + '</div>';
      return;
    }
    els.appsList.innerHTML = '';
    apps.forEach(app => {
      const row = document.createElement('div');
      row.className = 'app-row';
      row.innerHTML = `
        <div>
          <span class="app-name">${escapeHtml(app.name || app.app_id || '')}</span>
          <span class="app-version">${escapeHtml(app.app_version)}</span>
          <div class="app-meta">${escapeHtml(app.app_id)} · ${formatDateTime(app.installed_at)}</div>
        </div>
        <div></div>
        <button class="btn-danger btn-small" data-app="${escapeHtml(app.app_id)}" data-ver="${escapeHtml(app.app_version)}">${t('uninstall')}</button>
      `;
      const btn = row.querySelector('button');
      btn.onclick = () => {
        if (confirm(t('confirm_uninstall', {name: app.name || app.app_id || '', version: app.app_version}))) {
          uninstallApp(app.app_id, app.app_version);
        }
      };
      els.appsList.appendChild(row);
    });
  }

  async function uninstallApp(appId, version) {
    try {
      await api(`/api/apps/${encodeURIComponent(appId)}?version=${encodeURIComponent(version)}`, {
        method: 'DELETE',
      });
      // 刷新
      const defs = await api('/api/tasks/definitions');
      state.definitions = defs;
      renderMarket();
      loadApps();
    } catch (e) {
      alert(t('uninstall_failed') + ': ' + e.message);
    }
  }

  window.toggleCollapse = function(header) {
    const block = header.closest('.result-block');
    if (block) block.classList.toggle('collapsed');
  };

  function escapeHtml(str) {
    if (!str) return '';
    return str.replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
  }

  function formatJson(obj, indent) {
    indent = indent || 0;
    const pad = '  '.repeat(indent);
    const padNext = '  '.repeat(indent + 1);
    if (obj === null) return '<span style="color:#7cafc2">null</span>';
    if (typeof obj === 'boolean') return '<span style="color:#7cafc2">' + obj + '</span>';
    if (typeof obj === 'number') return '<span style="color:#d18bba">' + obj + '</span>';
    if (typeof obj === 'string') return '<span style="color:#a1b56c">"' + escapeHtml(obj) + '"</span>';
    if (Array.isArray(obj)) {
      if (obj.length === 0) return '<span style="color:#e0e0e0">[]</span>';
      let html = '<span style="color:#e0e0e0">[</span>\n';
      for (let i = 0; i < obj.length; i++) {
        html += padNext + formatJson(obj[i], indent + 1);
        if (i < obj.length - 1) html += '<span style="color:#e0e0e0">,</span>';
        html += '\n';
      }
      html += pad + '<span style="color:#e0e0e0">]</span>';
      return html;
    }
    const keys = Object.keys(obj);
    if (keys.length === 0) return '<span style="color:#e0e0e0">{}</span>';
    let html = '<span style="color:#e0e0e0">{</span>\n';
    for (let i = 0; i < keys.length; i++) {
      const k = keys[i];
      html += padNext + '<span style="color:#ba8baf">"' + escapeHtml(k) + '"</span><span style="color:#e0e0e0">: </span>' + formatJson(obj[k], indent + 1);
      if (i < keys.length - 1) html += '<span style="color:#e0e0e0">,</span>';
      html += '\n';
    }
    html += pad + '<span style="color:#e0e0e0">}</span>';
    return html;
  }

  // ---------- Media Streams ----------
  let mediaPollTimer = null;
  let _pc = null;

  async function loadMediaStreams() {
    if (!els.mediaStreamsList) return;
    try {
      const data = await api('/api/media/streams');
      const streams = data.streams || [];
      if (streams.length === 0) {
        els.mediaStreamsList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('no_streams') + '</div>';
      } else {
        els.mediaStreamsList.innerHTML = '';
        streams.forEach(s => {
          const row = document.createElement('div');
          row.style.cssText = 'display:flex;align-items:center;gap:12px;padding:10px 12px;border:1px solid var(--border);border-radius:6px;margin-bottom:8px;background:var(--surface);';
          const statusColor = s.enabled ? 'var(--success)' : 'var(--text-muted)';
          const statusText = s.enabled ? t('media_enabled') : t('media_disabled');
          row.innerHTML = `
            <span style="width:8px;height:8px;border-radius:50%;background:${statusColor};display:inline-block;"></span>
            <div style="flex:1;min-width:0;">
              <div style="font-weight:bold;font-size:13px;">${escapeHtml(t(s.name) || s.id)}</div>
              <div style="font-size:12px;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(s.source_url)} · ${escapeHtml(s.codec)}</div>
            </div>
            <div style="display:flex;gap:6px;flex-wrap:wrap;">
              <button class="btn-small btn-primary" data-action="play" data-id="${s.id}" ${s.enabled ? '' : 'disabled'}>▶ ${t('media_play')}</button>
              ${s.enabled
                ? `<button class="btn-small btn-danger" data-action="stop" data-id="${s.id}">⏹ ${t('media_stop')}</button>`
                : `<button class="btn-small btn-primary" data-action="start" data-id="${s.id}">▶ ${t('media_start')}</button>`}
              <button class="btn-small" data-action="edit" data-id="${s.id}">✏️ ${t('edit')}</button>
              <button class="btn-small btn-danger" data-action="delete" data-id="${s.id}">🗑️ ${t('delete')}</button>
            </div>
          `;
          row.querySelectorAll('button[data-action]').forEach(btn => {
            btn.onclick = (e) => {
              e.stopPropagation();
              const action = btn.dataset.action;
              const id = btn.dataset.id;
              if (action === 'play') playMediaStream(id, t(s.name) || s.id);
              if (action === 'start') startMediaStream(id);
              if (action === 'stop') stopMediaStream(id);
              if (action === 'edit') editMediaStream(s);
              if (action === 'delete') deleteMediaStream(id);
            };
          });
          els.mediaStreamsList.appendChild(row);
        });
      }
      if (els.mediaError) els.mediaError.style.display = 'none';
    } catch (e) {
      els.mediaStreamsList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('load_failed') + ': ' + escapeHtml(e.message) + '</div>';
    }
  }

  async function startMediaStream(id) {
    try {
      if (els.mediaError) els.mediaError.style.display = 'none';
      await api('/api/media/streams/' + id + '/start', { method: 'POST' });
      await loadMediaStreams();
    } catch (e) {
      if (els.mediaError) {
        els.mediaError.style.display = 'block';
        let msg = e.message || '';
        if (msg.includes('423')) msg = t('media_takeoff_lock_blocked');
        els.mediaError.textContent = t('media_start_failed') + ': ' + msg;
      }
    }
  }

  async function stopMediaStream(id) {
    try {
      if (els.mediaError) els.mediaError.style.display = 'none';
      await api('/api/media/streams/' + id + '/stop', { method: 'POST' });
      await loadMediaStreams();
    } catch (e) {
      if (els.mediaError) {
        els.mediaError.style.display = 'block';
        els.mediaError.textContent = t('media_stop_failed') + ': ' + e.message;
      }
    }
  }

  async function deleteMediaStream(id) {
    if (!confirm(t('confirm_delete'))) return;
    try {
      await api('/api/media/streams/' + id, { method: 'DELETE' });
      await loadMediaStreams();
    } catch (e) {
      alert(t('delete') + ' ' + t('load_failed') + ': ' + e.message);
    }
  }

  function editMediaStream(s) {
    if (!els.mediaStreamForm) return;
    els.mediaStreamId.value = s.id;
    els.mediaStreamName.value = s.name || '';
    els.mediaStreamSource.value = s.source_url || '';
    els.mediaStreamCodec.value = s.codec || 'copy';
    els.mediaStreamForm.style.display = 'block';
  }

  function showMediaStreamForm(clear) {
    if (!els.mediaStreamForm) return;
    if (clear) {
      els.mediaStreamId.value = '';
      els.mediaStreamName.value = '';
      els.mediaStreamSource.value = 'rtsp://192.168.144.25:8554/main.264';
      els.mediaStreamCodec.value = 'h264';
    }
    els.mediaStreamForm.style.display = 'block';
  }

  function hideMediaStreamForm() {
    if (els.mediaStreamForm) els.mediaStreamForm.style.display = 'none';
  }

  async function saveMediaStream() {
    const id = els.mediaStreamId.value;
    const body = {
      name: els.mediaStreamName.value,
      source_url: els.mediaStreamSource.value,
      codec: els.mediaStreamCodec.value,
    };
    try {
      if (id) {
        await api('/api/media/streams/' + id, { method: 'PUT', body: JSON.stringify(body) });
      } else {
        await api('/api/media/streams', { method: 'POST', body: JSON.stringify(body) });
      }
      hideMediaStreamForm();
      await loadMediaStreams();
    } catch (e) {
      alert(t('save_failed') + ': ' + e.message);
    }
  }

  async function loadMediaAutoStopConfig() {
    if (!els.mediaAutoStop) return;
    try {
      const cfg = await api('/api/media/config');
      els.mediaAutoStop.checked = cfg.auto_stop_on_takeoff !== false;
    } catch (e) {
      // 默认保持勾选
    }
  }

  async function setMediaAutoStopConfig(enabled) {
    try {
      await api('/api/media/config', { method: 'POST', body: JSON.stringify({ auto_stop_on_takeoff: enabled }) });
    } catch (e) {
      console.error('Failed to save media auto-stop config:', e);
    }
  }

  // WebRTC WHEP player
  async function playMediaStream(streamId, title) {
    if (!els.mediaVideo) return;
    // Close previous connection
    if (_pc) {
      _pc.close();
      _pc = null;
    }
    els.mediaPlayerArea.style.display = 'block';
    els.mediaPlayerTitle.textContent = title || streamId;
    els.mediaPlayerStatus.textContent = t('media_connecting');
    els.mediaVideo.srcObject = null;

    const pc = new RTCPeerConnection({
      iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
    });
    _pc = pc;

    pc.addTransceiver('video', { direction: 'recvonly' });
    pc.addTransceiver('audio', { direction: 'recvonly' });

    pc.ontrack = (event) => {
      if (els.mediaVideo.srcObject !== event.streams[0]) {
        els.mediaVideo.srcObject = event.streams[0];
        els.mediaPlayerStatus.textContent = t('media_playing');
      }
    };

    pc.onconnectionstatechange = () => {
      if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
        els.mediaPlayerStatus.textContent = t('media_connection_failed');
      }
    };

    try {
      const offer = await pc.createOffer();
      await pc.setLocalDescription(offer);
      // Wait for ICE gathering to complete
      await new Promise((resolve) => {
        if (pc.iceGatheringState === 'complete') {
          resolve();
        } else {
          const check = () => {
            if (pc.iceGatheringState === 'complete') {
              pc.removeEventListener('icegatheringstatechange', check);
              resolve();
            }
          };
          pc.addEventListener('icegatheringstatechange', check);
          setTimeout(resolve, 3000); // fallback timeout
        }
      });
      const offerSdp = pc.localDescription.sdp;
      const res = await api('/api/media/webrtc-offer', {
        method: 'POST',
        body: JSON.stringify({ stream_id: streamId, sdp: offerSdp }),
      });
      await pc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: res.sdp }));
    } catch (e) {
      els.mediaPlayerStatus.textContent = t('media_connection_failed') + ': ' + e.message;
      pc.close();
      _pc = null;
    }
  }

  function closeMediaPlayer() {
    if (_pc) {
      _pc.close();
      _pc = null;
    }
    if (els.mediaVideo) els.mediaVideo.srcObject = null;
    if (els.mediaPlayerArea) els.mediaPlayerArea.style.display = 'none';
  }

  function initMediaService() {
    if (!els.mediaStreamsList) return;
    loadMediaStreams();
    loadMediaAutoStopConfig();
    // auto-refresh on tab switch only; no manual add/refresh buttons
    if (els.btnMediaSave) els.btnMediaSave.onclick = saveMediaStream;
    if (els.btnMediaCancel) els.btnMediaCancel.onclick = hideMediaStreamForm;
    if (els.btnMediaPlayerClose) els.btnMediaPlayerClose.onclick = closeMediaPlayer;
    if (els.mediaAutoStop) {
      els.mediaAutoStop.onchange = () => setMediaAutoStopConfig(els.mediaAutoStop.checked);
    }
    if (mediaPollTimer) clearInterval(mediaPollTimer);
    mediaPollTimer = setInterval(loadMediaStreams, 5000);
  }

  // ---------- Push Stream ----------
  let pushPollTimer = null;
  async function initPushStream() {
    if (!els.pushConfigList) return;
    await loadPushConfigs();
    if (els.pushGlobalAuto) {
      els.pushGlobalAuto.onchange = async () => {
        await api('/api/push-streams/settings', { method: 'PUT', body: JSON.stringify({ global_auto_push: els.pushGlobalAuto.checked }) });
      };
    }
    if (els.btnPushStartAll) els.btnPushStartAll.onclick = async () => {
      const data = await api('/api/push-streams');
      for (const cfg of (data.configs || [])) {
        if (cfg.auto_start && !cfg.is_running) {
          try { await api(`/api/push-streams/${cfg.id}/start`, { method: 'POST' }); } catch (e) {}
        }
      }
      await loadPushConfigs();
    };
    if (els.btnPushStopAll) els.btnPushStopAll.onclick = async () => {
      const data = await api('/api/push-streams');
      for (const cfg of (data.configs || [])) {
        if (cfg.is_running) {
          try { await api(`/api/push-streams/${cfg.id}/stop`, { method: 'POST' }); } catch (e) {}
        }
      }
      await loadPushConfigs();
    };
    if (els.btnPushAdd) els.btnPushAdd.onclick = () => { clearPushForm(); els.pushForm.style.display = 'block'; };
    if (els.btnPushSave) els.btnPushSave.onclick = savePushConfig;
    if (els.btnPushCancel) els.btnPushCancel.onclick = () => { els.pushForm.style.display = 'none'; };
    if (pushPollTimer) clearInterval(pushPollTimer);
    pushPollTimer = setInterval(loadPushConfigs, 3000);
  }
  function clearPushForm() {
    if (els.pushName) els.pushName.value = '';
    if (els.pushSourceUrl) els.pushSourceUrl.value = 'rtsp://192.168.144.25:8554/main.264';
    if (els.pushServerHost) els.pushServerHost.value = '';
    if (els.pushServerPort) els.pushServerPort.value = 1935;
    if (els.pushStreamKey) els.pushStreamKey.value = '';
    if (els.pushProtocol) els.pushProtocol.value = 'rtmp';
    if (els.pushCodec) els.pushCodec.value = 'copy';
    if (els.pushEnabled) els.pushEnabled.checked = true;
    if (els.pushAutoStart) els.pushAutoStart.checked = false;
    if (els.pushStopOnFlight) els.pushStopOnFlight.checked = true;
    els.pushForm.dataset.editing = '';
  }
  async function loadPushConfigs() {
    if (!els.pushConfigList) return;
    try {
      const data = await api('/api/push-streams');
      if (els.pushGlobalAuto) els.pushGlobalAuto.checked = data.global_auto_push;
      const configs = data.configs || [];
      if (configs.length === 0) {
        els.pushConfigList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('no_push_configs') + '</div>';
        return;
      }
      let html = '';
      configs.forEach(cfg => {
        html += `<div class="app-row">
          <div>
            <div class="app-name">📡 ${escapeHtml(cfg.name)} <span style="font-size:12px;color:var(--text-muted)">${cfg.is_running ? '🟢' : '⚪'} ${cfg.protocol}://${cfg.server_host}:${cfg.server_port}</span></div>
            <div class="app-meta">${cfg.is_running ? formatDuration(cfg.total_push_seconds + (Date.now()/1000 - (cfg.start_timestamp || Date.now()/1000))) : formatDuration(cfg.total_push_seconds)} · ${formatFileSize(cfg.total_bytes_sent)}</div>
          </div>
          <button class="btn-${cfg.is_running ? 'danger' : 'primary'} btn-small" onclick="window.togglePushStream('${cfg.id}', ${cfg.is_running})">${cfg.is_running ? t('stop') : t('play')}</button>
          <button class="btn-small" onclick="window.editPushStream('${cfg.id}')">${t('detect_settings')}</button>
          <button class="btn-danger btn-small" onclick="window.deletePushStream('${cfg.id}')">${t('delete')}</button>
        </div>`;
      });
      els.pushConfigList.innerHTML = html;
    } catch (e) { console.error('load push failed', e); }
  }
  window.togglePushStream = async function(id, isRunning) {
    try {
      if (isRunning) await api(`/api/push-streams/${id}/stop`, { method: 'POST' });
      else await api(`/api/push-streams/${id}/start`, { method: 'POST' });
      await loadPushConfigs();
    } catch (e) { alert(e.message); }
  };
  window.editPushStream = async function(id) {
    try {
      const cfg = await api(`/api/push-streams/${id}`);
      if (els.pushName) els.pushName.value = cfg.name || '';
      if (els.pushSourceUrl) els.pushSourceUrl.value = cfg.source_url || '';
      if (els.pushServerHost) els.pushServerHost.value = cfg.server_host || '';
      if (els.pushServerPort) els.pushServerPort.value = cfg.server_port || 1935;
      if (els.pushStreamKey) els.pushStreamKey.value = cfg.stream_key || '';
      if (els.pushProtocol) els.pushProtocol.value = cfg.protocol || 'rtmp';
      if (els.pushCodec) els.pushCodec.value = cfg.codec || 'copy';
      if (els.pushEnabled) els.pushEnabled.checked = cfg.enabled;
      if (els.pushAutoStart) els.pushAutoStart.checked = cfg.auto_start;
      if (els.pushStopOnFlight) els.pushStopOnFlight.checked = cfg.stop_on_flight;
      els.pushForm.dataset.editing = id;
      els.pushForm.style.display = 'block';
    } catch (e) { alert(e.message); }
  };
  window.deletePushStream = async function(id) {
    if (!confirm(t('confirm_delete'))) return;
    try { await api(`/api/push-streams/${id}`, { method: 'DELETE' }); await loadPushConfigs(); }
    catch (e) { alert(e.message); }
  };
  async function savePushConfig() {
    const body = {
      name: els.pushName?.value || '',
      source_url: els.pushSourceUrl?.value || '',
      server_host: els.pushServerHost?.value || '',
      server_port: parseInt(els.pushServerPort?.value || 1935),
      stream_key: els.pushStreamKey?.value || '',
      protocol: els.pushProtocol?.value || 'rtmp',
      codec: els.pushCodec?.value || 'copy',
      enabled: els.pushEnabled?.checked || true,
      auto_start: els.pushAutoStart?.checked || false,
      stop_on_flight: els.pushStopOnFlight?.checked || true,
    };
    const editing = els.pushForm.dataset.editing;
    try {
      if (editing) await api(`/api/push-streams/${editing}`, { method: 'PUT', body: JSON.stringify(body) });
      else await api('/api/push-streams', { method: 'POST', body: JSON.stringify(body) });
      els.pushForm.style.display = 'none';
      await loadPushConfigs();
    } catch (e) { alert(t('save_failed') + ': ' + e.message); }
  }

  // ---------- Detection ----------
  let detectPollTimer = null;
  async function initDetection() {
    if (!els.detectStatus) return;
    await loadDetectionSettings();
    if (els.btnDetectStart) els.btnDetectStart.onclick = async () => {
      try { await api('/api/detection/start', { method: 'POST' }); await loadDetectionSettings(); }
      catch (e) { alert(t('start_failed') + ': ' + e.message); }
    };
    if (els.btnDetectStop) els.btnDetectStop.onclick = async () => {
      try { await api('/api/detection/stop', { method: 'POST' }); await loadDetectionSettings(); }
      catch (e) { alert(t('stop_failed') + ': ' + e.message); }
    };
    if (els.btnDetectSave) els.btnDetectSave.onclick = async () => {
      const body = {
        enabled: els.detectEnabled?.checked || false,
        auto_start: els.detectAutoStart?.checked || false,
        stop_on_flight: els.detectStopOnFlight?.checked || true,
        stream_url: els.detectStreamUrl?.value || '',
        output_dir: els.detectOutputDir?.value || '',
        confidence_threshold: parseFloat(els.detectConfidence?.value || 0.5),
        detect_interval: parseInt(els.detectInterval?.value || 5),
        max_saved_images: parseInt(els.detectMaxImages?.value || 1000),
        target_labels: els.detectTargetLabels?.value ? els.detectTargetLabels.value.split(',').map(s => s.trim()).filter(Boolean) : [],
        model_path: els.detectModel?.value || '',
      };
      try { await api('/api/detection/settings', { method: 'PUT', body: JSON.stringify(body) }); alert(t('save') + ' OK'); }
      catch (e) { alert(t('save_failed') + ': ' + e.message); }
    };
    if (els.btnDetectUploadModel) els.btnDetectUploadModel.onclick = () => els.detectModelInput?.click();
    if (els.detectModelInput) els.detectModelInput.onchange = async (ev) => {
      const file = ev.target.files[0];
      if (!file) return;
      try {
        const form = new FormData();
        form.append('file', file);
        const res = await fetch(`${API_BASE}/api/detection/model/upload`, { method: 'POST', body: form });
        if (!res.ok) throw new Error(await res.text());
        await loadDetectionSettings();
      } catch (e) { alert(t('upload_failed') + ': ' + e.message); }
      ev.target.value = '';
    };
    if (detectPollTimer) clearInterval(detectPollTimer);
    detectPollTimer = setInterval(loadDetectionSettings, 2000);
  }
  async function loadDetectionSettings() {
    try {
      const data = await api('/api/detection/settings');
      if (els.detectEnabled) els.detectEnabled.checked = data.enabled;
      if (els.detectAutoStart) els.detectAutoStart.checked = data.auto_start;
      if (els.detectStopOnFlight) els.detectStopOnFlight.checked = data.stop_on_flight;
      if (els.detectStreamUrl) els.detectStreamUrl.value = data.stream_url || '';
      if (els.detectOutputDir) els.detectOutputDir.value = data.output_dir || '';
      if (els.detectConfidence) els.detectConfidence.value = data.confidence_threshold || 0.5;
      if (els.detectInterval) els.detectInterval.value = data.detect_interval || 5;
      if (els.detectMaxImages) els.detectMaxImages.value = data.max_saved_images || 1000;
      if (els.detectTargetLabels) els.detectTargetLabels.value = (data.target_labels || []).join(',');
      if (els.detectModel) {
        const models = data.models || [];
        let opts = '<option value="">' + t('detect_select_model') + '</option>';
        models.forEach(m => { opts += `<option value="${m}">${m}</option>`; });
        els.detectModel.innerHTML = opts;
        els.detectModel.value = data.model_path || '';
      }
      const statusText = `status: ${data.status || 'stopped'} | alerts: ${data.alert_count || 0} | frames: ${data.frame_count || 0}`;
      els.detectStatus.textContent = statusText;
      if (data.last_alert_time) {
        const now = Date.now();
        const alertTime = new Date(data.last_alert_time).getTime();
        if (now - alertTime < 10000) {
          els.detectAlert.style.display = 'block';
          setTimeout(() => { els.detectAlert.style.display = 'none'; }, 5000);
        }
      }
    } catch (e) { console.error('load detection failed', e); }
  }

  // ---------- Settings ----------
  function initSettings() {
    if (!els.settingsApiBase) return;
    els.settingsApiBase.value = localStorage.getItem('dtm-api-base') || '';
    els.settingsWsHost.value = localStorage.getItem('dtm-ws-host') || '';
    if (els.btnSettingsSave) {
      els.btnSettingsSave.onclick = () => {
        const apiBase = els.settingsApiBase.value.trim();
        const wsHost = els.settingsWsHost.value.trim();
        if (apiBase) localStorage.setItem('dtm-api-base', apiBase);
        else localStorage.removeItem('dtm-api-base');
        if (wsHost) localStorage.setItem('dtm-ws-host', wsHost);
        else localStorage.removeItem('dtm-ws-host');
        if (els.settingsStatus) els.settingsStatus.textContent = t('saved') + ' — ' + t('settings_hint');
      };
    }
    if (els.btnSettingsReset) {
      els.btnSettingsReset.onclick = () => {
        localStorage.removeItem('dtm-api-base');
        localStorage.removeItem('dtm-ws-host');
        els.settingsApiBase.value = '';
        els.settingsWsHost.value = '';
        if (els.settingsStatus) els.settingsStatus.textContent = t('saved') + ' — ' + t('settings_hint');
      };
    }
  }

  // ---------- Flight Missions ----------
  async function loadMissions() {
    if (!els.missionsList) return;
    els.missionsList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('loading') + '</div>';
    try {
      const params = new URLSearchParams();
      params.set('page', String(state.missionsPage));
      params.set('page_size', String(state.missionsPageSize));
      const data = await api('/api/flight-missions?' + params.toString());
      state.missionsTotal = data.total || 0;
      renderMissions(data.missions || []);
      renderMissionPaging();
    } catch (e) {
      els.missionsList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('load_failed') + ': ' + escapeHtml(e.message) + '</div>';
    }
  }

  function renderMissions(missions) {
    if (!els.missionsList) return;
    if (missions.length === 0) {
      els.missionsList.innerHTML = '';
      if (els.missionsEmpty) els.missionsEmpty.style.display = 'block';
      return;
    }
    if (els.missionsEmpty) els.missionsEmpty.style.display = 'none';
    let html = '';
    // Header
    html += '<div style="display:grid;grid-template-columns:2fr 1fr 1fr 1fr 1.5fr 1fr;gap:8px;padding:4px 12px;font-size:12px;color:var(--text-muted);border-bottom:1px solid var(--border);">';
    html += '<span>' + t('mission_name') + '</span><span>' + t('mission_images') + '</span><span>' + t('mission_videos') + '</span><span>' + t('mission_status') + '</span><span>' + t('file_mtime') + '</span><span style="text-align:right">' + t('action') + '</span>';
    html += '</div>';
    missions.forEach(m => {
      const mtime = m.mtime ? m.mtime.replace('T', ' ').substring(0, 19) : '-';
      const status = m.state !== null && m.state !== undefined ? String(m.state) : t('mission_unknown');
      // 把时间戳格式 YYYY-MM-DD-HH-MM-SS 格式化为 YYYY-MM-DD HH:MM:SS
      const displayName = m.name.replace(/^(\d{4})-(\d{2})-(\d{2})-(\d{2})-(\d{2})-(\d{2})$/, '$1-$2-$3 $4:$5:$6');
      html += `<div class="file-table-row" style="display:grid;grid-template-columns:2fr 1fr 1fr 1fr 1fr 1fr;gap:8px;padding:4px 12px;align-items:center;border-bottom:1px solid var(--border);">`;
      html += `<span>${escapeHtml(displayName)}</span>`;
      html += `<span style="font-size:12px;color:var(--text-muted)">${m.img_count}</span>`;
      html += `<span style="font-size:12px;color:var(--text-muted)">${m.video_count}</span>`;
      html += `<span style="font-size:12px;color:var(--text-muted)">${escapeHtml(status)}</span>`;
      html += `<span style="font-size:12px;color:var(--text-muted)">${mtime}</span>`;
      html += `<span style="text-align:right"><button class="btn-small" onclick="window.viewMissionDir('${escapeHtml(m.path)}')">${t('browse')}</button></span>`;
      html += '</div>';
    });
    els.missionsList.innerHTML = html;
  }

  function renderMissionPaging() {
    if (!els.missionsPaging) return;
    const total = state.missionsTotal;
    const pageSize = state.missionsPageSize;
    const page = state.missionsPage;
    const totalPages = Math.max(1, Math.ceil(total / pageSize));
    if (els.missionsPagingInfo) {
      els.missionsPagingInfo.textContent = t('paging_info', {total: total, page: page, totalPages: totalPages});
    }
    let html = '';
    html += '<button class="btn-small" ' + (page <= 1 ? 'disabled' : '') + ' data-page="' + (page - 1) + '">' + t('prev_page') + '</button>';
    for (let i = 1; i <= totalPages; i++) {
      if (i === 1 || i === totalPages || (i >= page - 2 && i <= page + 2)) {
        html += '<button class="btn-small" ' + (i === page ? 'disabled style="opacity:1;font-weight:bold;"' : '') + ' data-page="' + i + '">' + i + '</button>';
      } else if (i === page - 3 || i === page + 3) {
        html += '<span style="padding:0 4px;color:var(--text-muted);">...</span>';
      }
    }
    html += '<button class="btn-small" ' + (page >= totalPages ? 'disabled' : '') + ' data-page="' + (page + 1) + '">' + t('next_page') + '</button>';
    els.missionsPaging.innerHTML = html;
    els.missionsPaging.querySelectorAll('button[data-page]').forEach(btn => {
      btn.onclick = () => {
        state.missionsPage = parseInt(btn.dataset.page);
        loadMissions();
      };
    });
  }

  window.viewMissionDir = function(dirPath) {
    enterDir(dirPath);
    switchTab('files');
  };


  // ---------- System Power Control ----------
  async function sendPowerAction(action) {
    const msg1 = action === 'reboot' ? t('sys_reboot_confirm') : t('sys_poweroff_confirm');
    if (!confirm(msg1)) return;
    const msg2 = action === 'reboot' ? t('sys_reboot_confirm2') : t('sys_poweroff_confirm2');
    if (!confirm(msg2)) return;
    try {
      const res = await api('/api/system/power', {
        method: 'POST',
        body: JSON.stringify({ action, confirm: true }),
      });
      alert((action === 'reboot' ? t('sys_rebooting') : t('sys_powering_off')));
    } catch (e) {
      if (e.status === 423) {
        alert(t('diag_flight_locked'));
      } else {
        alert(t('start_failed') + ': ' + e.message);
      }
    }
  }

// ---------- Port Monitor ----------
  async function loadMonitorStatus() {
    if (!els.monitorNetwork) return;
    try {
      const data = await api('/api/port-monitor/status');
      if (els.monitorToggle) els.monitorToggle.checked = data.enabled;
      if (els.monitorStatusText) {
        els.monitorStatusText.textContent = data.enabled ? t('monitor_running') : t('monitor_stopped');
      }
      renderMonitorNetwork(data.network || []);
      renderMonitorUsb(data.usb || []);
      renderMonitorChanges(data.changes || []);
    } catch (e) { console.error('load monitor status failed', e); }
  }

  function renderMonitorChanges(changes) {
    let el = document.getElementById('monitor-changes');
    if (!el) {
      el = document.createElement('div');
      el.id = 'monitor-changes';
      el.style.cssText = 'margin-bottom:16px;';
      const networkEl = document.getElementById('monitor-network');
      if (networkEl && networkEl.parentNode) {
        networkEl.parentNode.insertBefore(el, networkEl);
      }
    }
    if (changes.length === 0) {
      el.innerHTML = '';
      return;
    }
    let html = '<div style="background:var(--bg-tertiary);border-radius:8px;padding:8px 12px;margin-bottom:8px;">';
    html += '<h4 style="font-size:13px;margin:0 0 6px;color:var(--accent);">⚡ ' + t('monitor_changes') + '</h4>';
    html += '<div style="display:grid;gap:4px;">';
    changes.forEach(c => {
      const typeLabel = c.type === 'network' ? t('monitor_network') : t('monitor_usb');
      let eventLabel = '';
      let color = 'var(--text-muted)';
      if (c.event === 'added') { eventLabel = t('monitor_added'); color = 'var(--success)'; }
      else if (c.event === 'removed') { eventLabel = t('monitor_removed'); color = 'var(--danger)'; }
      else { eventLabel = t('monitor_changed'); color = 'var(--warning)'; }
      const devName = c.device && (c.device.name || c.device.id || c.device.description || '?');
      html += `<div style="font-size:12px;padding:4px 0;border-bottom:1px solid var(--border);">`;
      html += `<span style="color:${color};font-weight:bold;">${eventLabel}</span> `;
      html += `<span style="color:var(--text-secondary)">${typeLabel}</span>: ${escapeHtml(devName)}`;
      html += `</div>`;
    });
    html += '</div></div>';
    el.innerHTML = html;
  }

  function renderMonitorNetwork(networks) {
    if (!els.monitorNetwork) return;
    let html = '<h3 style="font-size:14px;margin:0 0 8px;color:var(--text-muted)">🌐 ' + t('monitor_network') + '</h3>';
    if (networks.length === 0) {
      html += '<div class="text-muted" style="padding:12px;">' + t('monitor_no_network') + '</div>';
    } else {
      html += '<div style="display:grid;gap:4px;">';
      networks.forEach(n => {
        const ipv4 = (n.ipv4 || []).join(', ') || '-';
        const status = n.is_up ? '<span style="color:var(--success)">UP</span>' : '<span style="color:var(--danger)">DOWN</span>';
        html += `<div class="app-row" style="padding:6px 12px;font-size:13px;">`;
        html += `<b>${escapeHtml(n.name)}</b> ${status} · MAC: ${escapeHtml(n.mac || '-')} · IP: ${escapeHtml(ipv4)}`;
        html += `</div>`;
      });
      html += '</div>';
    }
    els.monitorNetwork.innerHTML = html;
  }

  function renderMonitorUsb(usbDevices) {
    if (!els.monitorUsb) return;
    let html = '<h3 style="font-size:14px;margin:0 0 8px;color:var(--text-muted)">🔌 ' + t('monitor_usb') + '</h3>';
    if (usbDevices.length === 0) {
      html += '<div class="text-muted" style="padding:12px;">' + t('monitor_no_usb') + '</div>';
    } else {
      html += '<div style="display:grid;gap:4px;">';
      usbDevices.forEach(u => {
        html += `<div class="app-row" style="padding:6px 12px;font-size:13px;">`;
        html += `Bus ${u.bus} Dev ${u.device} · <b>${escapeHtml(u.id || '-')}</b> · ${escapeHtml(u.description || '')}`;
        html += `</div>`;
      });
      html += '</div>';
    }
    els.monitorUsb.innerHTML = html;
  }

  async function loadMonitorLogs() {
    if (!els.monitorLogs) return;
    try {
      const data = await api('/api/port-monitor/logs');
      const logs = data.logs || [];
      let html = '<h3 style="font-size:14px;margin:0 0 8px;color:var(--text-muted)">📜 ' + t('monitor_logs') + '</h3>';
      if (logs.length === 0) {
        html += '<div class="text-muted" style="padding:12px;">' + t('monitor_no_logs') + '</div>';
      } else {
        html += '<div style="display:grid;gap:4px;">';
        logs.forEach(l => {
          html += `<div class="app-row" style="padding:6px 12px;font-size:13px;display:flex;justify-content:space-between;align-items:center;">`;
          html += `<span>${escapeHtml(l.filename)} · ${formatFileSize(l.size)} · ${l.mtime ? l.mtime.replace('T',' ').substring(0,19) : '-'}</span>`;
          html += `<a class="btn-small" href="${API_BASE}/api/port-monitor/logs/download?filename=${encodeURIComponent(l.filename)}" download>📥 ${t('download')}</a>`;
          html += `</div>`;
        });
        html += '</div>';
      }
      els.monitorLogs.innerHTML = html;
    } catch (e) { console.error('load monitor logs failed', e); }
  }

  async function toggleMonitor(enabled) {
    try {
      await api('/api/port-monitor/toggle', {
        method: 'POST',
        body: JSON.stringify({ enabled }),
      });
      loadMonitorStatus();
    } catch (e) { alert(t('save_failed') + ': ' + e.message); }
  }

  // ---------- System Diagnostics ----------
  let _diagTests = [];
  let _diagSessionId = null;
  let _diagPollTimer = null;
  let _lastDiagSessionId = null;
  let _lastSelectedTestIds = null;

  async function loadDiagnostics() {
    if (!els.diagResults) return;
    try {
      const data = await api('/api/system-diagnostics/tests');
      _diagTests = data.tests || [];
      renderDiagTestList();
    } catch (e) { console.error('load diagnostics tests failed', e); }
  }

  function renderDiagTestList() {
    if (!els.diagResults) return;
    if (_diagSessionId) return;
    if (els.diagAlert) { els.diagAlert.style.display = 'none'; els.diagAlert.textContent = ''; }
    if (els.diagDownload) els.diagDownload.style.display = 'none';
    if (els.diagBack) els.diagBack.style.display = 'none';
    if (els.diagProgressBar) els.diagProgressBar.style.display = 'none';
    if (els.diagProgressFill) els.diagProgressFill.style.width = '0%';
    if (els.diagProgressText) els.diagProgressText.textContent = '';
    let html = '<div id="diag-test-list" style="display:grid;gap:4px;">';
    _diagTests.forEach((t, idx) => {
      const isChecked = (_lastSelectedTestIds === null) ? true : _lastSelectedTestIds.includes(t.id);
      html += `<div class="app-row diag-test-row" style="padding:8px 12px;cursor:pointer;display:flex;align-items:center;" data-id="${escapeHtml(t.id)}">`;
      html += `<input type="checkbox" class="diag-check" data-id="${escapeHtml(t.id)}" ${isChecked ? 'checked' : ''} style="cursor:pointer;margin-right:8px;">`;
      html += `<span style="flex:1;">${idx + 1}. ${escapeHtml(t.name)}</span>`;
      html += `</div>`;
    });
    html += '</div>';
    els.diagResults.innerHTML = html;
    // 事件委托：点击行切换 checkbox，同步更新全选框状态
    const list = document.getElementById('diag-test-list');
    if (list) {
      list.onclick = function(e) {
        const row = e.target.closest('.diag-test-row');
        if (!row) return;
        const cb = row.querySelector('.diag-check');
        if (!cb) return;
        if (e.target !== cb) {
          cb.checked = !cb.checked;
        }
        syncDiagSelectAll();
      };
    }
  }

  function getSelectedDiagTests() {
    const checks = els.diagResults?.querySelectorAll('.diag-check:checked') || [];
    return Array.from(checks).map(ch => ch.dataset.id);
  }

  async function runDiagnostics(testIds) {
    if (!testIds || testIds.length === 0) {
      alert(t('diag_select_test'));
      return;
    }
    _lastSelectedTestIds = testIds;
    try {
      _diagSessionId = null;
      _lastDiagSessionId = null;
      if (els.diagAlert) { els.diagAlert.style.display = 'none'; els.diagAlert.textContent = ''; }
      if (els.diagDownload) els.diagDownload.style.display = 'none';
      if (els.diagBack) els.diagBack.style.display = 'none';
      if (els.diagProgressBar) els.diagProgressBar.style.display = 'block';
      if (els.diagProgressFill) els.diagProgressFill.style.width = '0%';
      if (els.diagProgressText) els.diagProgressText.textContent = t('diag_running') + '...';

      let html = '<div style="display:grid;gap:4px;">';
      _diagTests.forEach((testItem, idx) => {
        const isSelected = testIds.includes(testItem.id);
        const opacity = isSelected ? '1' : '0.4';
        html += `<div id="diag-row-${escapeHtml(testItem.id)}" class="app-row" style="padding:8px 12px;opacity:${opacity};">`;
        html += `<span style="width:28px;color:var(--text-muted);font-size:12px;">${idx + 1}.</span>`;
        html += `<span style="flex:1;">${escapeHtml(testItem.name)}</span>`;
        html += `<span class="diag-status" style="width:60px;font-size:12px;">${isSelected ? t('diag_pending') : '-'}</span>`;
        html += `<span class="diag-msg" style="flex:2;font-size:12px;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></span>`;
        html += `</div>`;
      });
      html += '</div>';
      if (els.diagResults) els.diagResults.innerHTML = html;

      const res = await api('/api/system-diagnostics/run', {
        method: 'POST',
        body: JSON.stringify({ test_ids: testIds }),
      });
      _diagSessionId = res.session_id;
      startDiagPolling();
    } catch (e) {
      if (els.diagProgressBar) els.diagProgressBar.style.display = 'none';
      if (els.diagProgressText) els.diagProgressText.textContent = '';
      if (e.status === 423) {
        if (els.diagAlert) {
          els.diagAlert.style.display = 'block';
          els.diagAlert.style.background = '#f8d7da';
          els.diagAlert.style.color = '#721c24';
          els.diagAlert.style.borderColor = '#f5c6cb';
          els.diagAlert.textContent = t('diag_flight_locked');
        }
      } else {
        alert(t('start_failed') + ': ' + e.message);
      }
      _diagSessionId = null;
    }
  }

  function startDiagPolling() {
    if (_diagPollTimer) clearInterval(_diagPollTimer);
    _diagPollTimer = setInterval(async () => {
      if (!_diagSessionId) { clearInterval(_diagPollTimer); return; }
      try {
        const session = await api(`/api/system-diagnostics/${_diagSessionId}`);
        updateDiagUI(session);
        if (session.status === 'completed' || session.status === 'error') {
          clearInterval(_diagPollTimer);
          _diagPollTimer = null;
          _lastDiagSessionId = _diagSessionId;
          _diagSessionId = null;
        }
      } catch (e) {
        console.error('diag poll failed', e);
        clearInterval(_diagPollTimer);
        _diagPollTimer = null;
        _diagSessionId = null;
      }
    }, 800);
  }

  function updateDiagUI(session) {
    const total = session.total || 0;
    const completed = session.completed || 0;
    const pct = total > 0 ? Math.round((completed / total) * 100) : 0;
    if (els.diagProgressFill) els.diagProgressFill.style.width = pct + '%';
    if (els.diagProgressText) els.diagProgressText.textContent = `${completed}/${total} (${pct}%)`;

    (session.results || []).forEach(r => {
      const row = document.getElementById('diag-row-' + r.test_id);
      if (!row) return;
      const statusEl = row.querySelector('.diag-status');
      const msgEl = row.querySelector('.diag-msg');
      if (statusEl) {
        statusEl.textContent = r.success ? t('diag_pass') : t('diag_fail');
        statusEl.style.color = r.success ? '#4caf50' : '#f44336';
      }
      if (msgEl) {
        msgEl.textContent = r.message || '';
        msgEl.title = r.details || '';
      }
    });

    if (session.status === 'completed' || session.status === 'error') {
      if (els.diagProgressText) els.diagProgressText.textContent = t('diag_completed');
      if (els.diagDownload) els.diagDownload.style.display = 'inline-block';
      if (els.diagBack) els.diagBack.style.display = 'inline-block';
    }
  }

  function downloadDiagResult() {
    const sid = _lastDiagSessionId || _diagSessionId;
    if (!sid) { alert(t('diag_no_session')); return; }
    const url = `${API_BASE}/api/system-diagnostics/${sid}/download`;
    const a = document.createElement('a');
    a.href = url;
    a.download = '';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
  }

  function clearDiagnostics() {
    if (_diagPollTimer) { clearInterval(_diagPollTimer); _diagPollTimer = null; }
    _diagSessionId = null;
    if (els.diagProgressBar) els.diagProgressBar.style.display = 'none';
    if (els.diagProgressFill) els.diagProgressFill.style.width = '0%';
    if (els.diagProgressText) els.diagProgressText.textContent = '';
    if (els.diagAlert) { els.diagAlert.style.display = 'none'; els.diagAlert.textContent = ''; }
    if (els.diagDownload) els.diagDownload.style.display = 'none';
    if (els.diagBack) els.diagBack.style.display = 'none';
    renderDiagTestList();
  }

  // ---------- Services ----------
  let _servicesPollTimer = null;

  async function loadServices() {
    if (!els.servicesList) return;
    try {
      const data = await api('/api/services');
      renderServices(data.services || []);
    } catch (e) {
      els.servicesList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('load_failed') + ': ' + escapeHtml(e.message) + '</div>';
    }
  }

  function renderServices(services) {
    if (!els.servicesList) return;
    if (services.length === 0) {
      els.servicesList.innerHTML = '<div class="text-muted" style="padding:12px">' + t('empty_dir') + '</div>';
      return;
    }
    let html = '<div style="display:grid;gap:8px;">';
    services.forEach(svc => {
      const isActive = svc.active;
      const statusText = isActive ? t('service_active') : (svc.status === 'inactive' ? t('service_inactive') : t('service_unknown'));
      const statusColor = isActive ? 'var(--success)' : 'var(--text-muted)';
      const label = svc['label_' + window.currentLang] || svc.label || svc.name;
      html += '<div class="app-row" style="display:flex;align-items:center;gap:12px;padding:12px;">';
      html += '<div style="flex:1;">';
      html += '<div class="app-name">' + escapeHtml(label) + '</div>';
      html += '<div class="app-meta" style="color:' + statusColor + ';">' + escapeHtml(statusText) + (svc.sub_state ? ' (' + escapeHtml(svc.sub_state) + ')' : '') + (svc.pid ? ' · PID ' + escapeHtml(svc.pid) : '') + '</div>';
      html += '</div>';
      html += '<div style="display:flex;gap:6px;">';
      html += '<button class="btn btn-small" ' + (isActive ? 'disabled' : '') + ' onclick="window.controlService(\'' + escapeHtml(svc.name) + '\', \'start\')">' + t('service_start') + '</button>';
      html += '<button class="btn btn-small" ' + (!isActive ? 'disabled' : '') + ' onclick="window.controlService(\'' + escapeHtml(svc.name) + '\', \'stop\')">' + t('service_stop') + '</button>';
      html += '<button class="btn btn-small" onclick="window.controlService(\'' + escapeHtml(svc.name) + '\', \'restart\')">' + t('service_restart') + '</button>';
      html += '</div>';
      html += '</div>';
    });
    html += '</div>';
    els.servicesList.innerHTML = html;
  }

  window.controlService = async function(name, action) {
    if ((action === 'stop' || action === 'restart')) {
      const actionText = action === 'stop' ? t('service_stop') : t('service_restart');
      const msg = t('confirm_stop_service', {action: actionText, name: name});
      if (!confirm(msg)) return;
    }
    try {
      await api('/api/services/' + encodeURIComponent(name) + '/' + action, { method: 'POST' });
      await loadServices();
    } catch (e) {
      alert(t('service_control_failed') + ': ' + e.message);
    }
  };

  function startServicesPoll() {
    if (_servicesPollTimer) return;
    _servicesPollTimer = setInterval(loadServices, 3000);
  }
  function stopServicesPoll() {
    if (_servicesPollTimer) { clearInterval(_servicesPollTimer); _servicesPollTimer = null; }
  }

  // ---------- Self Update ----------
  let selfUpdatePollTimer = null;
  async function loadSelfUpdate() {
    if (!els.selfUpdatePackages) return;
    try {
      const data = await api('/api/self-update');
      if (els.selfUpdateVersion) {
        const v = data.current_version || {};
        els.selfUpdateVersion.innerHTML = `${t('self_update_current_version')}: <b>${v.version || 'unknown'}</b> | git: ${v.git_commit || 'unknown'} | ${v.build_time || v.build_date || 'unknown'}`;
      }
      if (els.selfUpdateStatus) {
        const s = data.status || {};
        const st = s.status || 'idle';
        if (st === 'idle') { els.selfUpdateStatus.style.display = 'none'; }
        else {
          els.selfUpdateStatus.style.display = 'block';
          const colors = { applying: '#2196f3', restarting: '#ff9800', completed: '#4caf50', failed: '#f44336', rolling_back: '#ff9800', rolled_back: '#2196f3', rollback_failed: '#f44336' };
          els.selfUpdateStatus.style.background = colors[st] || '#666';
          els.selfUpdateStatus.style.color = '#fff';
          els.selfUpdateStatus.textContent = `[${st}] ${s.step || ''}${s.error ? ' — ' + s.error : ''}`;
        }
      }
      const pkgs = (data.packages || []).filter(p => p.filename.endsWith('.tar.gz') || p.filename.endsWith('.tgz'));
      if (els.selfUpdatePackages) {
        if (pkgs.length === 0) els.selfUpdatePackages.innerHTML = '<div class="text-muted" style="padding:12px">' + t('self_update_no_packages') + '</div>';
        else {
          let html = '';
          pkgs.forEach(p => {
            html += `<div class="app-row"><div><div class="app-name">📦 ${escapeHtml(p.filename)}</div><div class="app-meta">${formatFileSize(p.size)} · ${p.mtime ? p.mtime.replace('T',' ').substring(0,19) : '-'}</div></div><button class="btn-primary btn-small" onclick="window.applySelfUpdate('${escapeHtml(p.filename)}')">${t('self_update_apply')}</button><button class="btn-danger btn-small" onclick="window.deleteSelfUpdatePackage('${escapeHtml(p.filename)}')">${t('delete')}</button></div>`;
          });
          els.selfUpdatePackages.innerHTML = html;
        }
      }
      const backups = data.backups || [];
      if (els.selfUpdateBackups) {
        if (backups.length === 0) els.selfUpdateBackups.innerHTML = '<div class="text-muted" style="padding:12px">' + t('self_update_no_backups') + '</div>';
        else {
          let html = '';
          backups.forEach(b => {
            html += `<div class="app-row"><div><div class="app-name">🗄️ ${escapeHtml(b.name)}</div><div class="app-meta">${formatFileSize(b.size)} · ${b.mtime ? b.mtime.replace('T',' ').substring(0,19) : '-'}</div></div><button class="btn-danger btn-small" onclick="window.deleteSelfUpdateBackup('${escapeHtml(b.name)}')">${t('delete')}</button></div>`;
          });
          html += `<div style="margin-top:8px"><button class="btn-danger btn-small" onclick="window.rollbackSelfUpdate()">${t('self_update_rollback')}</button></div>`;
          els.selfUpdateBackups.innerHTML = html;
        }
      }
    } catch (e) { console.error('load self-update failed', e); }
  }
  window.applySelfUpdate = async function(filename) {
    if (!confirm(t('self_update_confirm_apply') + '\n' + filename)) return;
    try { await api('/api/self-update/apply', { method: 'POST', body: JSON.stringify({ filename }) }); alert(t('self_update_restarting')); loadSelfUpdate(); }
    catch (e) { alert(t('self_update_apply_failed') + ': ' + e.message); }
  };
  window.rollbackSelfUpdate = async function() {
    if (!confirm(t('self_update_confirm_rollback'))) return;
    try { await api('/api/self-update/rollback', { method: 'POST' }); alert(t('self_update_restarting')); loadSelfUpdate(); }
    catch (e) { alert(t('self_update_rollback_failed') + ': ' + e.message); }
  };
  window.deleteSelfUpdatePackage = async function(filename) {
    if (!confirm(t('confirm_delete_file') + '\n' + filename)) return;
    try { await api(`/api/self-update/packages/${encodeURIComponent(filename)}`, { method: 'DELETE' }); loadSelfUpdate(); }
    catch (e) { alert(t('delete') + ' ' + t('load_failed') + ': ' + e.message); }
  };
  window.deleteSelfUpdateBackup = async function(name) {
    if (!confirm(t('confirm_delete_file') + '\n' + name)) return;
    try { await api(`/api/self-update/backups/${encodeURIComponent(name)}`, { method: 'DELETE' }); loadSelfUpdate(); }
    catch (e) { alert(t('delete') + ' ' + t('load_failed') + ': ' + e.message); }
  };
  function uploadSelfUpdatePackage(file) {
    if (els.selfUpdateProgress) els.selfUpdateProgress.style.display = 'block';
    if (els.selfUpdateUploadDone) els.selfUpdateUploadDone.style.display = 'none';
    if (els.selfUpdateProgressBar) els.selfUpdateProgressBar.style.width = '0%';
    if (els.selfUpdateProgressPct) els.selfUpdateProgressPct.textContent = '0%';

    const xhr = new XMLHttpRequest();
    const form = new FormData();
    form.append('file', file);

    xhr.upload.addEventListener('progress', (ev) => {
      if (ev.lengthComputable && els.selfUpdateProgressBar && els.selfUpdateProgressPct) {
        const pct = Math.round((ev.loaded / ev.total) * 100);
        els.selfUpdateProgressBar.style.width = pct + '%';
        els.selfUpdateProgressPct.textContent = pct + '%';
      }
    });

    xhr.addEventListener('load', () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        if (els.selfUpdateProgress) els.selfUpdateProgress.style.display = 'none';
        if (els.selfUpdateUploadDone) els.selfUpdateUploadDone.style.display = 'block';
        loadSelfUpdate();
        setTimeout(() => {
          if (els.selfUpdateUploadDone) els.selfUpdateUploadDone.style.display = 'none';
        }, 3000);
      } else {
        if (els.selfUpdateProgress) els.selfUpdateProgress.style.display = 'none';
        alert(t('upload_failed') + ': ' + (xhr.responseText || xhr.statusText));
      }
    });

    xhr.addEventListener('error', () => {
      if (els.selfUpdateProgress) els.selfUpdateProgress.style.display = 'none';
      alert(t('upload_failed'));
    });

    xhr.addEventListener('abort', () => {
      if (els.selfUpdateProgress) els.selfUpdateProgress.style.display = 'none';
    });

    xhr.open('POST', `${API_BASE}/api/self-update/upload`);
    xhr.send(form);
  }

  // ---------- Event bindings ----------
  if (els.btnSysReboot) {
    els.btnSysReboot.onclick = () => sendPowerAction('reboot');
  }
  if (els.btnSysPoweroff) {
    els.btnSysPoweroff.onclick = () => sendPowerAction('poweroff');
  }
  if (els.monitorToggle) {
    els.monitorToggle.onchange = (ev) => toggleMonitor(ev.target.checked);
  }
  if (els.btnMonitorRefresh) {
    els.btnMonitorRefresh.onclick = () => { loadMonitorStatus(); loadMonitorLogs(); };
  }
  if (els.diagRunAll) {
    els.diagRunAll.onclick = () => runDiagnostics(_diagTests.map(t => t.id));
  }
  if (els.diagRunSelected) {
    els.diagRunSelected.onclick = () => {
      const selected = getSelectedDiagTests();
      if (selected.length === 0) { alert(t('diag_select_test')); return; }
      runDiagnostics(selected);
    };
  }
  // diag clear button removed
  if (els.diagDownload) {
    els.diagDownload.onclick = downloadDiagResult;
  }
  function syncDiagSelectAll() {
    if (!els.diagSelectAll) return;
    const all = document.querySelectorAll('.diag-check');
    const checked = document.querySelectorAll('.diag-check:checked');
    els.diagSelectAll.checked = all.length > 0 && all.length === checked.length;
  }
  if (els.diagSelectAll) {
    els.diagSelectAll.onchange = () => {
      const checked = els.diagSelectAll.checked;
      document.querySelectorAll('.diag-check').forEach(cb => { cb.checked = checked; });
    };
  }
  if (els.diagBack) {
    els.diagBack.onclick = clearDiagnostics;
  }
  if (els.selfUpdateUploadArea) {
    els.selfUpdateUploadArea.onclick = () => els.selfUpdateFileInput?.click();
    els.selfUpdateUploadArea.ondragover = (ev) => { ev.preventDefault(); els.selfUpdateUploadArea.classList.add('dragover'); };
    els.selfUpdateUploadArea.ondragleave = () => els.selfUpdateUploadArea.classList.remove('dragover');
    els.selfUpdateUploadArea.ondrop = (ev) => {
      ev.preventDefault(); els.selfUpdateUploadArea.classList.remove('dragover');
      const file = ev.dataTransfer.files[0]; if (file) uploadSelfUpdatePackage(file);
    };
  }
  if (els.selfUpdateFileInput) {
    els.selfUpdateFileInput.onchange = (ev) => {
      const file = ev.target.files[0]; if (file) uploadSelfUpdatePackage(file); ev.target.value = '';
    };
  }

  init();
})();
