{% extends "base.html" %}
{% block title %}{{ t(request, 'title.maintenance') }}{% endblock %}

{% block content %}
<div class="d-flex align-items-center justify-content-between mb-3">
  <h5 class="mb-0 fw-bold"><i class="bi bi-tools me-2 text-primary"></i>{{ t(request, 'nav.maintenance') }}</h5>
  <span class="text-muted small">{{ inference_servers|length }} {{ t(request, 'js.active_servers') }}</span>
</div>
<div id="flash-container" class="mb-2"></div>

<div class="row g-3">

  <!-- Card 1: Ontology trigger -->
  <div class="col-12 col-lg-6">
    <div class="card h-100">
      <div class="card-header bg-primary bg-opacity-10">
        <i class="bi bi-diagram-3 me-2"></i><strong>{{ t(request, 'lbl.ontology_runner') }}</strong>
      </div>
      <div class="card-body">
        <p class="text-muted small mb-3">{{ t(request, 'msg.ontology_runner_desc') }}</p>
        <div class="row g-2 mb-3">
          <div class="col-6">
            <label class="form-label small mb-1">Concurrency</label>
            <input type="number" id="onto-concurrency" class="form-control form-control-sm" value="4" min="1" max="32">
          </div>
          <div class="col-6">
            <label class="form-label small mb-1">Batch size</label>
            <input type="number" id="onto-batch" class="form-control form-control-sm" value="20" min="1" max="200">
          </div>
        </div>
        <button class="btn btn-primary btn-sm w-100" id="btn-onto-trigger" onclick="triggerOntology()">
          <i class="bi bi-play-fill me-1"></i>{{ t(request, 'btn.trigger_ontology') }}
        </button>
        <hr class="my-3">
        <div class="small">
          <div class="d-flex justify-content-between"><span class="text-muted">Status:</span>
            <span id="onto-status" class="fw-bold">—</span></div>
          <div class="d-flex justify-content-between"><span class="text-muted">Run ID:</span>
            <span id="onto-run-id" class="font-monospace small">—</span></div>
          <div class="d-flex justify-content-between"><span class="text-muted">Processed:</span>
            <span id="onto-processed">0</span></div>
          <div class="d-flex justify-content-between"><span class="text-muted">Written:</span>
            <span id="onto-written" class="text-success">0</span></div>
          <div class="d-flex justify-content-between"><span class="text-muted">Failed:</span>
            <span id="onto-failed" class="text-danger">0</span></div>
        </div>
      </div>
    </div>
  </div>

  <!-- Card 1b: Dedicated Healer -->
  <div class="col-12 col-lg-6">
    <div class="card h-100">
      <div class="card-header bg-danger bg-opacity-10">
        <i class="bi bi-infinity me-2"></i><strong>{{ t(request, 'lbl.dedicated_healer') }}</strong>
      </div>
      <div class="card-body">
        <p class="text-muted small mb-3">{{ t(request, 'msg.dedicated_healer_desc') }}</p>
        <div class="mb-3">
          <label class="form-label small mb-1">{{ t(request, 'lbl.dedicated_healer_template') }}</label>
          <select id="dedicated-template" class="form-select form-select-sm">
            <option value="">— {{ t(request, 'msg.dedicated_healer_loading') }} —</option>
          </select>
        </div>
        <div class="form-check form-switch mb-3">
          <input class="form-check-input" type="checkbox" role="switch" id="dedicated-block-server" checked>
          <label class="form-check-label small" for="dedicated-block-server">
            {{ t(request, 'lbl.dedicated_healer_block') }}
          </label>
        </div>
        <div class="d-flex gap-2 mb-3">
          <button class="btn btn-success btn-sm flex-fill" id="btn-dedicated-start" onclick="startDedicatedHealer()">
            <i class="bi bi-play-fill me-1"></i>{{ t(request, 'btn.dedicated_healer_start') }}
          </button>
          <button class="btn btn-danger btn-sm flex-fill" id="btn-dedicated-stop" disabled onclick="stopDedicatedHealer()">
            <i class="bi bi-stop-fill me-1"></i>{{ t(request, 'btn.dedicated_healer_stop') }}
          </button>
        </div>
        <hr class="my-3">
        <div class="small">
          <div class="d-flex justify-content-between"><span class="text-muted">Status:</span>
            <span id="dedicated-status" class="fw-bold">—</span></div>
          <div class="d-flex justify-content-between"><span class="text-muted">Template:</span>
            <span id="dedicated-tmpl" class="font-monospace small text-truncate" style="max-width:180px">—</span></div>
          <div class="d-flex justify-content-between"><span class="text-muted">PID:</span>
            <span id="dedicated-pid" class="font-monospace small">—</span></div>
          <div class="d-flex justify-content-between"><span class="text-muted">Processed:</span>
            <span id="dedicated-processed">0</span></div>
          <div class="d-flex justify-content-between"><span class="text-muted">Written:</span>
            <span id="dedicated-written" class="text-success">0</span></div>
          <div class="d-flex justify-content-between"><span class="text-muted">Failed:</span>
            <span id="dedicated-failed" class="text-danger">0</span></div>
        </div>
      </div>
    </div>
  </div>

  <!-- Card 2: Scan + Cleanup -->
  <div class="col-12 col-lg-6">
    <div class="card h-100">
      <div class="card-header bg-warning bg-opacity-10">
        <i class="bi bi-trash me-2"></i><strong>{{ t(request, 'lbl.cleanup') }}</strong>
      </div>
      <div class="card-body">
        <p class="text-muted small mb-3">{{ t(request, 'msg.cleanup_desc') }}</p>
        <div class="d-flex gap-2 mb-3">
          <button class="btn btn-outline-secondary btn-sm flex-fill" onclick="scanOrphans()">
            <i class="bi bi-search me-1"></i>{{ t(request, 'btn.cleanup_scan') }}
          </button>
          <button class="btn btn-warning btn-sm flex-fill" id="btn-cleanup-run" disabled onclick="runCleanup(false)">
            <i class="bi bi-trash me-1"></i>{{ t(request, 'btn.cleanup_run') }}
          </button>
        </div>
        <div id="cleanup-report" class="small" style="max-height:340px;overflow-y:auto">
          <div class="text-muted text-center py-4">{{ t(request, 'msg.cleanup_click_scan') }}</div>
        </div>
      </div>
    </div>
  </div>

  <!-- Card 2b: Permission cleanup -->
  <div class="col-12 col-lg-6">
    <div class="card h-100">
      <div class="card-header bg-danger bg-opacity-10">
        <i class="bi bi-shield-x me-2"></i><strong>{{ t(request, 'lbl.perm_cleanup') }}</strong>
      </div>
      <div class="card-body">
        <p class="text-muted small mb-3">{{ t(request, 'msg.perm_cleanup_desc') }}</p>
        <div class="d-flex gap-2 mb-3">
          <button class="btn btn-outline-secondary btn-sm flex-fill" onclick="scanPermissions()">
            <i class="bi bi-search me-1"></i>{{ t(request, 'btn.cleanup_scan') }}
          </button>
          <button class="btn btn-danger btn-sm flex-fill" id="btn-perm-cleanup-run" disabled onclick="runPermissionCleanup()">
            <i class="bi bi-shield-x me-1"></i>{{ t(request, 'btn.perm_cleanup_run') }}
          </button>
        </div>
        <div id="perm-cleanup-report" class="small" style="max-height:280px;overflow-y:auto">
          <div class="text-muted text-center py-4">{{ t(request, 'msg.perm_cleanup_click_scan') }}</div>
        </div>
      </div>
    </div>
  </div>

  <!-- Card 2c: Template verification -->
  <div class="col-12">
    <div class="card">
      <div class="card-header bg-info bg-opacity-10">
        <i class="bi bi-check2-square me-2"></i><strong>{{ t(request, 'lbl.template_verify') }}</strong>
      </div>
      <div class="card-body">
        <p class="text-muted small mb-3">{{ t(request, 'msg.template_verify_desc') }}</p>
        <div class="d-flex gap-2 mb-3">
          <button class="btn btn-outline-info btn-sm" onclick="verifyTemplates()">
            <i class="bi bi-check2-square me-1"></i>{{ t(request, 'btn.template_verify') }}
          </button>
          <button class="btn btn-info btn-sm" id="btn-pull-missing" disabled onclick="pullMissing()">
            <i class="bi bi-cloud-download me-1"></i>{{ t(request, 'btn.template_pull_missing') }}
          </button>
        </div>
        <div id="verify-report" class="small" style="max-height:320px;overflow-y:auto">
          <div class="text-muted text-center py-3">{{ t(request, 'msg.template_verify_click') }}</div>
        </div>
      </div>
    </div>
  </div>

  <!-- Card 2d: Template reference cleanup -->
  <div class="col-12">
    <div class="card">
      <div class="card-header bg-warning bg-opacity-10">
        <i class="bi bi-link-45deg me-2"></i><strong>{{ t(request, 'lbl.template_ref_cleanup') }}</strong>
      </div>
      <div class="card-body">
        <p class="text-muted small mb-3">{{ t(request, 'msg.template_ref_cleanup_desc') }}</p>
        <div class="d-flex gap-2 mb-3">
          <button class="btn btn-outline-warning btn-sm" onclick="scanTemplateRefs()">
            <i class="bi bi-search me-1"></i>{{ t(request, 'btn.template_ref_scan') }}
          </button>
          <button class="btn btn-warning btn-sm" id="btn-tmplref-dryrun" disabled onclick="runTemplateRefCleanup(true)">
            <i class="bi bi-eye me-1"></i>Dry-Run
          </button>
          <button class="btn btn-danger btn-sm" id="btn-tmplref-run" disabled onclick="runTemplateRefCleanup(false)">
            <i class="bi bi-trash me-1"></i>{{ t(request, 'btn.template_ref_cleanup') }}
          </button>
        </div>
        <div id="tmplref-report" class="small" style="max-height:320px;overflow-y:auto">
          <div class="text-muted text-center py-3">{{ t(request, 'msg.template_ref_click') }}</div>
        </div>
      </div>
    </div>
  </div>

  <!-- ── Cleanup Manager ──────────────────────────────────────────────── -->
  <div class="col-12">
    <div class="card">
      <div class="card-header bg-dark bg-opacity-10 d-flex justify-content-between align-items-center">
        <span><i class="bi bi-recycle me-2"></i><strong>{{ t(request, 'lbl.cleanup_manager') }}</strong></span>
        <button class="btn btn-outline-secondary btn-sm" onclick="loadCleanupStatus()">
          <i class="bi bi-arrow-clockwise me-1"></i>{{ t(request, 'btn.refresh') }}
        </button>
      </div>
      <div class="card-body p-0">
        <div id="cleanup-manager-body" class="p-3">
          <div class="text-center text-muted py-4">
            <div class="spinner-border spinner-border-sm me-2"></div>{{ t(request, 'state.loading') }}
          </div>
        </div>
      </div>
    </div>
  </div>

  <!-- Card 3: Regeneration buttons -->
  <div class="col-12">
    <div class="card">
      <div class="card-header bg-success bg-opacity-10">
        <i class="bi bi-arrow-clockwise me-2"></i><strong>{{ t(request, 'lbl.regeneration') }}</strong>
      </div>
      <div class="card-body">
        <p class="text-muted small mb-3">{{ t(request, 'msg.regeneration_desc') }}</p>
        <div class="d-flex gap-2 flex-wrap">
          <button class="btn btn-outline-success btn-sm" onclick="regenDashboard()">
            <i class="bi bi-graph-up-arrow me-1"></i>{{ t(request, 'btn.regen_dashboard') }}
          </button>
          <button class="btn btn-outline-success btn-sm" onclick="reloadPrometheus()">
            <i class="bi bi-arrow-repeat me-1"></i>{{ t(request, 'btn.reload_prometheus') }}
          </button>
        </div>
        <div id="regen-result" class="small mt-3 text-muted"></div>
      </div>
    </div>
  </div>

</div>
{% endblock %}

{% block scripts %}
<script>
const _t = {
  noOntologyServers:     "{{ t(request, 'js.no_ontology_servers') }}",
  loadingError:          "{{ t(request, 'js.loading_error') }}",
  noOutputSince:         "{{ t(request, 'js.no_output_since') }}",
  selectTemplateFirst:   "{{ t(request, 'js.select_template_first') }}",
  startFailed:           "{{ t(request, 'js.start_failed') }}",
  startError:            "{{ t(request, 'js.start_error') }}",
  healerStartedUnverified: {{ t(request, 'js.healer_started_unverified') | tojson }},
  confirmStopHealer:     "{{ t(request, 'js.confirm_stop_healer') }}",
  couldNotStart:         "{{ t(request, 'js.could_not_start') }}",
  noOrphansFound:        "{{ t(request, 'js.no_orphans_found') }}",
  activeServersLabel:    "{{ t(request, 'js.active_servers_label') }}",
  scanning:              "{{ t(request, 'js.scanning') }}",
  cleaning:              "{{ t(request, 'js.cleaning') }}",
  confirmCleanup:        "{{ t(request, 'js.confirm_cleanup') }}",
  checking:              "{{ t(request, 'js.checking') }}",
  regeneratingDashboard: "{{ t(request, 'js.regenerating_dashboard') }}",
  btnSave:               "{{ t(request, 'btn.save_short') }}",
  btnNow:                "{{ t(request, 'btn.now') }}",
  never:                 "{{ t(request, 'js.never') }}",
  checkpointArchive:     "{{ t(request, 'lbl.checkpoint_archive') }}",
  lastRun:               "{{ t(request, 'lbl.last_run') }}",
  duration:              "{{ t(request, 'lbl.duration') }}",
  freed:                 "{{ t(request, 'lbl.freed') }}",
  totalRuns:             "{{ t(request, 'lbl.total_runs') }}",
  jobActive:             "{{ t(request, 'lbl.job_active') }}",
  cfgSaved:              "{{ t(request, 'js.config_saved') }}",
  liveDatabaseRows:      "{{ t(request, 'lbl.live_db_rows') }}",
  archiveRetention:      "{{ t(request, 'lbl.archive_retention') }}",
  maxFileSize:           "{{ t(request, 'lbl.max_file_size') }}",
  backupFiles:           "{{ t(request, 'lbl.backup_files') }}",
  maxStorage:            "{{ t(request, 'lbl.max_storage') }}",
  retentionDays:         "{{ t(request, 'lbl.retention_days') }}",
  prometheusRestartNote: "{{ t(request, 'msg.prometheus_restart_note') }}",
  konfig:                "{{ t(request, 'btn.konfig') }}",
  jobStarted:            "{{ t(request, 'js.job_started') }}",
  prometheusReloading:   "{{ t(request, 'js.prometheus_reloading') }}",
  prometheusReloaded:    "{{ t(request, 'js.prometheus_reloaded') }}",
  noStalePermissions:       "{{ t(request, 'js.no_stale_permissions') }}",
  stalePermissionsFound:    "{{ t(request, 'js.stale_permissions_found') }}",
  confirmPermCleanup:       "{{ t(request, 'js.confirm_perm_cleanup') }}",
  permCleanupDone:          "{{ t(request, 'js.perm_cleanup_done') }}",
  noStaleTmplRefs:          "{{ t(request, 'js.no_stale_template_refs') }}",
  staleTmplRefsFound:       "{{ t(request, 'js.stale_template_refs_found') }}",
  confirmTmplRefCleanup:    "{{ t(request, 'js.confirm_template_ref_cleanup') }}",
  tmplRefCleanupDone:       "{{ t(request, 'js.template_ref_cleanup_done') }}",
};

function flash(msg, type='success') {
  const c = document.getElementById('flash-container');
  c.innerHTML = `<div class="alert alert-${type} alert-dismissible fade show py-2" role="alert">
    ${msg}<button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>`;
  setTimeout(() => { if (c.firstChild) c.firstChild.remove(); }, 5000);
}

function fmtStatus(s, extra) {
  const map = {running: 'warning', ready: 'success', failed: 'danger', idle: 'secondary', starting: 'info', stalled: 'danger', stopped: 'secondary'};
  const cls = map[s] || 'secondary';
  const label = extra ? `${s} — ${extra}` : s;
  return `<span class="badge bg-${cls}">${label}</span>`;
}

// ─── Dedicated Healer ────────────────────────────────────────────────────────

async function loadDedicatedTemplates() {
  try {
    const templates = await fetch('/api/expert-templates').then(r => r.json());
    const sel = document.getElementById('dedicated-template');
    sel.innerHTML = '';
    const curatorTpls = (templates || []).filter(t => (t.name || '').startsWith('moe-ontology-curator-'));
    if (curatorTpls.length === 0) {
      sel.innerHTML = `<option value="">— ${_t.noOntologyServers} —</option>`;
      return;
    }
    curatorTpls.forEach(t => {
      const opt = document.createElement('option');
      opt.value = t.name;
      opt.textContent = t.name;
      sel.appendChild(opt);
    });
    // Pre-select currently running template if any
    pollDedicatedHealer();
  } catch (e) {
    document.getElementById('dedicated-template').innerHTML = `<option value="">— ${_t.loadingError} —</option>`;
  }
}

async function pollDedicatedHealer() {
  try {
    const d = await fetch('/api/maintenance/ontology/dedicated/status').then(r => r.json());
    const running = d.status === 'running' || d.status === 'starting';
    const stalledExtra = d.stalled === '1' ? _t.noOutputSince : null;
    const displayStatus = d.stalled === '1' ? 'stalled' : (d.status || 'stopped');
    document.getElementById('dedicated-status').innerHTML = fmtStatus(displayStatus, stalledExtra);
    document.getElementById('dedicated-tmpl').textContent = d.template || '—';
    document.getElementById('dedicated-pid').textContent = d.pid || '—';
    document.getElementById('dedicated-processed').textContent = d.processed || 0;
    document.getElementById('dedicated-written').textContent = d.written || 0;
    document.getElementById('dedicated-failed').textContent = d.failed || 0;
    document.getElementById('btn-dedicated-start').disabled = running;
    document.getElementById('btn-dedicated-stop').disabled = !running;
    // Pre-select template in dropdown if running
    if (running && d.template) {
      const sel = document.getElementById('dedicated-template');
      for (const opt of sel.options) {
        if (opt.value === d.template) { sel.value = opt.value; break; }
      }
    }
  } catch (e) { /* ignore */ }
}

// Extract the node name from a curator template name
// "moe-ontology-curator-n04-rtx" → "n04-rtx"
function _tplToServerName(tpl) {
  const prefix = 'moe-ontology-curator-';
  return tpl.startsWith(prefix) ? tpl.slice(prefix.length) : tpl;
}

async function startDedicatedHealer() {
  const template = document.getElementById('dedicated-template').value.trim();
  if (!template) { alert(_t.selectTemplateFirst); return; }
  const blockServer = document.getElementById('dedicated-block-server').checked;
  const btnStart = document.getElementById('btn-dedicated-start');
  const btnStop  = document.getElementById('btn-dedicated-stop');
  btnStart.disabled = true;

  // 1. Block server for regular routing if requested.
  if (blockServer) {
    const serverName = _tplToServerName(template);
    try {
      await fetch(`/api/servers/${encodeURIComponent(serverName)}/block-toggle`, {
        method: 'POST', headers: {'Content-Type':'application/json'},
        body: JSON.stringify({blocked: true}),
      });
    } catch (e) { /* non-fatal */ }
  }

  // 2. Clean stale Redis state from any previous run before starting.
  try {
    await fetch('/api/maintenance/ontology/dedicated/status', {method: 'DELETE'});
  } catch (e) { /* non-fatal */ }

  // 3. Start the healer.
  let startOk = false;
  try {
    const r = await fetch('/api/maintenance/ontology/dedicated/start', {
      method: 'POST', headers: {'Content-Type':'application/json'},
      body: JSON.stringify({template, block_server: blockServer}),
    }).then(r => r.json());
    if (!r.ok) {
      alert(_t.startFailed + ': ' + (r.reason || JSON.stringify(r)));
      btnStart.disabled = false;
      return;
    }
    startOk = true;
  } catch (e) {
    alert(_t.startError + ': ' + e.message);
    btnStart.disabled = false;
    return;
  }

  // 4. Show "starting" state and poll verify endpoint until PID + active request confirmed.
  document.getElementById('dedicated-status').innerHTML = fmtStatus('starting');
  btnStop.disabled = false;

  let verified = false;
  for (let i = 0; i < 10; i++) {
    await new Promise(res => setTimeout(res, 1500));
    try {
      const v = await fetch('/api/maintenance/ontology/dedicated/verify').then(r => r.json());
      // Verified if: PID alive AND (active request visible OR healer produced output within 60 s)
      if (v.pid_alive && v.is_active) { verified = true; break; }
      // If PID died after 3 retries, no point waiting further.
      if (!v.pid_alive && i >= 2) break;
    } catch (e) { /* ignore network hiccup */ }
  }

  if (!verified) {
    alert(_t.healerStartedUnverified);
  }

  pollDedicatedHealer();
}

async function stopDedicatedHealer() {
  if (!confirm(_t.confirmStopHealer)) return;
  document.getElementById('btn-dedicated-stop').disabled = true;
  let template = '';
  try {
    // Get current template from status before stopping
    const s = await fetch('/api/maintenance/ontology/dedicated/status').then(r => r.json());
    template = s.template || '';
    await fetch('/api/maintenance/ontology/dedicated/stop', {method: 'POST'});
  } catch (e) { alert('Error: ' + e.message); }

  // If block_server was set, unblock now
  if (template) {
    const serverName = _tplToServerName(template);
    try {
      await fetch(`/api/servers/${encodeURIComponent(serverName)}/block-toggle`, {
        method: 'POST', headers: {'Content-Type':'application/json'},
        body: JSON.stringify({blocked: false}),
      });
    } catch (e) { /* non-fatal */ }
  }
  setTimeout(pollDedicatedHealer, 800);
}

async function pollOntology() {
  try {
    const d = await fetch('/api/maintenance/ontology/status').then(r => r.json());
    document.getElementById('onto-status').innerHTML = fmtStatus(d.status || 'idle');
    document.getElementById('onto-run-id').textContent = d.run_id || '—';
    document.getElementById('onto-processed').textContent = d.processed || 0;
    document.getElementById('onto-written').textContent = d.written || 0;
    document.getElementById('onto-failed').textContent = d.failed || 0;
    document.getElementById('btn-onto-trigger').disabled = (d.status === 'running');
  } catch (e) { /* ignore */ }
}

async function triggerOntology() {
  const btn = document.getElementById('btn-onto-trigger');
  btn.disabled = true;
  try {
    const body = {
      concurrency: parseInt(document.getElementById('onto-concurrency').value) || 4,
      batch_size: parseInt(document.getElementById('onto-batch').value) || 20,
    };
    const r = await fetch('/api/maintenance/ontology/trigger', {
      method: 'POST', headers: {'Content-Type':'application/json'},
      body: JSON.stringify(body),
    }).then(r => r.json());
    if (!r.ok) alert(_t.couldNotStart + ': ' + (r.reason || 'unknown'));
  } catch (e) { alert('Error: ' + e.message); }
  finally { pollOntology(); }
}

function orphanCount(o) {
  return (o.templates_by_name?.length || 0) + (o.templates_by_pin?.length || 0)
       + (o.redis_keys?.length || 0) + (o.neo4j_curator_entities || 0)
       + (o.prometheus_static_targets?.length || 0);
}

function renderScanReport(d) {
  const o = d.orphans;
  const total = orphanCount(o);
  const btn = document.getElementById('btn-cleanup-run');
  btn.disabled = total === 0;
  if (total === 0) {
    return `<div class="text-success text-center py-3"><i class="bi bi-check-circle fs-3"></i><br>${_t.noOrphansFound}</div>`;
  }
  const rows = [
    ['{{ t(request, "js.orphan_curator_templates") }}', o.templates_by_name.map(t => `${t.name} (${t.dead_node})`)],
    ['{{ t(request, "js.templates_deadnode_pin") }}', o.templates_by_pin.map(t => `${t.name} → ${t.dead_nodes.join(',')}`)],
    ['{{ t(request, "js.user_templates_deadnode") }}', o.user_templates_by_pin.map(t => `user=${t.user_id} → ${t.dead_nodes.join(',')}`)],
    ['{{ t(request, "js.user_cc_profiles_deadnode") }}', o.user_cc_profiles_by_pin.map(t => `user=${t.user_id} → ${t.dead_nodes.join(',')}`)],
    ['{{ t(request, "js.valkey_keys") }}', o.redis_keys],
    ['{{ t(request, "js.neo4j_curator_entities") }}', [o.neo4j_curator_entities + ' {{ t(request, "js.pieces") }}']],
    ['{{ t(request, "js.prometheus_stale_targets") }}', o.prometheus_static_targets.map(t => `${t.job}: ${t.node} (${t.targets.join(',')})`)],
  ].filter(r => r[1].length > 0);
  let html = `<table class="table table-sm mb-0"><thead><tr><th>{{ t(request, 'table.category') }}</th><th>{{ t(request, 'lbl.entries') }}</th></tr></thead><tbody>`;
  for (const [k, vs] of rows) {
    html += `<tr><td class="fw-bold">${k}</td><td>`;
    html += vs.map(v => `<div class="font-monospace" style="font-size:.78rem">${escapeHtml(String(v))}</div>`).join('');
    html += `</td></tr>`;
  }
  html += `</tbody></table>`;
  html += `<div class="small text-muted mt-2">${_t.activeServersLabel} ${d.active_servers.join(', ')}</div>`;
  return html;
}

function escapeHtml(s) {
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}

async function scanOrphans() {
  const box = document.getElementById('cleanup-report');
  box.innerHTML = `<div class="text-center py-3"><span class="spinner-border spinner-border-sm"></span> ${_t.scanning}</div>`;
  try {
    const d = await fetch('/api/maintenance/scan').then(r => r.json());
    box.innerHTML = renderScanReport(d);
  } catch (e) {
    box.innerHTML = `<div class="text-danger">Error: ${e.message}</div>`;
  }
}

async function runCleanup(dryRun) {
  const box = document.getElementById('cleanup-report');
  if (!dryRun && !confirm(_t.confirmCleanup)) return;
  box.innerHTML = `<div class="text-center py-3"><span class="spinner-border spinner-border-sm"></span> ${_t.cleaning}</div>`;
  try {
    const d = await fetch('/api/maintenance/cleanup', {
      method: 'POST', headers: {'Content-Type':'application/json'},
      body: JSON.stringify({dry_run: dryRun}),
    }).then(r => r.json());
    let html = `<div class="alert alert-success"><strong>{{ t(request, 'js.cleanup_done') }} ${dryRun ? '(dry-run)' : ''}:</strong><br>`;
    for (const [k, v] of Object.entries(d.removed)) {
      html += `<div>${k}: <strong>${v}</strong></div>`;
    }
    html += `</div>`;
    if (d.errors && d.errors.length) {
      html += `<div class="alert alert-warning small"><strong>{{ t(request, 'js.warnings') }}:</strong><ul class="mb-0">`;
      for (const e of d.errors) html += `<li>${escapeHtml(e)}</li>`;
      html += `</ul></div>`;
    }
    box.innerHTML = html;
    document.getElementById('btn-cleanup-run').disabled = true;
  } catch (e) {
    box.innerHTML = `<div class="text-danger">Error: ${e.message}</div>`;
  }
}

async function scanPermissions() {
  const box = document.getElementById('perm-cleanup-report');
  box.innerHTML = `<div class="text-center py-3"><span class="spinner-border spinner-border-sm"></span> ${_t.scanning}</div>`;
  try {
    const d = await fetch('/api/maintenance/permissions/scan').then(r => r.json());
    if (d.count === 0) {
      box.innerHTML = `<div class="text-success text-center py-3"><i class="bi bi-check-circle fs-3"></i><br>${_t.noStalePermissions}</div>`;
      document.getElementById('btn-perm-cleanup-run').disabled = true;
      return;
    }
    let html = `<div class="alert alert-warning mb-2"><strong>${d.count} ${_t.stalePermissionsFound}</strong></div>`;
    html += `<ul class="list-unstyled mb-0">`;
    for (const rid of d.stale_resource_ids) {
      html += `<li class="font-monospace small text-danger"><i class="bi bi-x-circle me-1"></i>${escapeHtml(rid)}</li>`;
    }
    html += `</ul>`;
    box.innerHTML = html;
    document.getElementById('btn-perm-cleanup-run').disabled = false;
  } catch (e) {
    box.innerHTML = `<div class="text-danger">Error: ${e.message}</div>`;
  }
}

async function runPermissionCleanup() {
  const box = document.getElementById('perm-cleanup-report');
  if (!confirm(_t.confirmPermCleanup)) return;
  box.innerHTML = `<div class="text-center py-3"><span class="spinner-border spinner-border-sm"></span> ${_t.cleaning}</div>`;
  try {
    const d = await fetch('/api/maintenance/permissions/cleanup', {
      method: 'POST', headers: {'Content-Type':'application/json'},
      body: JSON.stringify({dry_run: false}),
    }).then(r => r.json());
    box.innerHTML = `<div class="alert alert-success"><strong>${_t.permCleanupDone}:</strong> ${d.deleted}</div>`;
    document.getElementById('btn-perm-cleanup-run').disabled = true;
  } catch (e) {
    box.innerHTML = `<div class="text-danger">Error: ${e.message}</div>`;
  }
}

async function verifyTemplates() {
  const box = document.getElementById('verify-report');
  box.innerHTML = `<div class="text-center py-3"><span class="spinner-border spinner-border-sm"></span> ${_t.checking}</div>`;
  try {
    const d = await fetch('/api/maintenance/templates/verify').then(r => r.json());
    const missing = d.issues.filter(t => t.issues.some(i => i.kind === 'model_missing')).length;
    document.getElementById('btn-pull-missing').disabled = missing === 0;
    if (d.issues.length === 0) {
      box.innerHTML = `<div class="text-success text-center py-3"><i class="bi bi-check-circle fs-3"></i><br>{{ t(request, 'js.all_templates_ok_prefix') }} ${d.checked} {{ t(request, 'js.all_templates_ok_suffix') }}</div>`;
      return;
    }
    let html = `<div class="mb-2 text-muted">{{ t(request, 'js.verified_prefix') }}: ${d.checked}, ok: ${d.clean.length}, {{ t(request, 'js.with_issues') }}: ${d.issues.length}</div>`;
    html += `<table class="table table-sm mb-0"><thead><tr><th>Template</th><th>{{ t(request, 'js.problem') }}</th></tr></thead><tbody>`;
    for (const t of d.issues) {
      const details = t.issues.map(i => {
        const cls = i.kind === 'model_missing' ? 'text-warning' : i.kind === 'dead_node' ? 'text-danger' : 'text-muted';
        const label = i.kind === 'model_missing' ? '{{ t(request, "js.model_missing") }}' : i.kind === 'dead_node' ? '{{ t(request, "js.node_not_exist") }}' : '{{ t(request, "js.node_unreachable") }}';
        return `<div class="${cls}"><span class="font-monospace">${escapeHtml(i.model)}@${escapeHtml(i.node)}</span>: ${label}</div>`;
      }).join('');
      html += `<tr><td class="font-monospace" style="font-size:.82rem">${escapeHtml(t.name)}</td><td>${details}</td></tr>`;
    }
    html += `</tbody></table>`;
    box.innerHTML = html;
  } catch (e) {
    box.innerHTML = `<div class="text-danger">Error: ${e.message}</div>`;
  }
}

async function pullMissing() {
  const box = document.getElementById('verify-report');
  if (!confirm('{{ t(request, "js.confirm_pull_missing") }}')) return;
  box.innerHTML = `<div class="text-center py-3"><span class="spinner-border spinner-border-sm"></span> {{ t(request, 'js.loading_models') }}</div>`;
  try {
    const d = await fetch('/api/maintenance/templates/pull-missing', {method:'POST'}).then(r => r.json());
    let html = `<div class="alert alert-info"><strong>{{ t(request, 'js.pull_done') }}:</strong> ${d.pulled.length} {{ t(request, 'lbl.entries') }}</div>`;
    html += `<table class="table table-sm"><thead><tr><th>{{ t(request, 'table.model') }}</th><th>Node</th><th>Status</th></tr></thead><tbody>`;
    for (const p of d.pulled) {
      const cls = p.ok ? 'text-success' : 'text-danger';
      html += `<tr><td class="font-monospace">${escapeHtml(p.model)}</td><td>${escapeHtml(p.node)}</td><td class="${cls}">${p.ok ? (p.last_status||'ok') : escapeHtml(p.error||'failed')}</td></tr>`;
    }
    html += `</tbody></table>`;
    box.innerHTML = html;
    document.getElementById('btn-pull-missing').disabled = true;
  } catch (e) {
    box.innerHTML = `<div class="text-danger">Error: ${e.message}</div>`;
  }
}

async function regenDashboard() {
  const box = document.getElementById('regen-result');
  box.textContent = _t.regeneratingDashboard;
  try {
    const d = await fetch('/api/dashboards/regenerate', {method:'POST'}).then(r => r.json());
    box.innerHTML = `{{ t(request, 'js.dashboard_updated') }} Probed: ${d.probed.length} Server. Prometheus reloaded: ${d.prometheus_reloaded}.`;
    if (d.dashboard_url) window.open(d.dashboard_url, '_blank');
  } catch (e) { box.textContent = 'Error: ' + e.message; }
}

async function reloadPrometheus() {
  const box = document.getElementById('regen-result');
  box.textContent = _t.prometheusReloading;
  try {
    const d = await fetch('/api/maintenance/prometheus/reload', {method:'POST'}).then(r => r.json());
    box.textContent = d.ok ? _t.prometheusReloaded : 'Error: ' + (d.error || d.status_code);
  } catch (e) { box.textContent = 'Error: ' + e.message; }
}

// ─── Cleanup Manager ─────────────────────────────────────────────────────────

// JOB_META labels are resolved at runtime via _t to support i18n
const JOB_META_RAW = {
  docker_prune:       { icon: 'bi-layers',             labelKey: null, label: 'Docker Prune',    color: 'primary',  canRun: true,  hasConfig: false },
  checkpoint_archive: { icon: 'bi-database-down',      labelKey: 'checkpointArchive',            color: 'info',     canRun: true,  hasConfig: true  },
  admin_logs:         { icon: 'bi-file-earmark-text',  label: 'Admin-Log Rotation',              color: 'warning',  canRun: false, hasConfig: true  },
  journal:            { icon: 'bi-journal-text',       label: 'systemd Journal',                 color: 'secondary',canRun: false, hasConfig: true  },
  prometheus:         { icon: 'bi-graph-up',           label: 'Prometheus TSDB',                 color: 'danger',   canRun: false, hasConfig: true  },
};
// Resolve translated labels
const JOB_META = Object.fromEntries(
  Object.entries(JOB_META_RAW).map(([k, v]) => [k, {...v, label: v.labelKey ? _t[v.labelKey] : v.label}])
);

const ALL_JOB_KEYS = Object.keys(JOB_META);

function fmtBytes(b) {
  if (!b || b === 0) return '—';
  if (b >= 1073741824) return (b/1073741824).toFixed(1) + ' GB';
  if (b >= 1048576)    return (b/1048576).toFixed(1) + ' MB';
  if (b >= 1024)       return (b/1024).toFixed(1) + ' KB';
  return b + ' B';
}

function fmtDur(s) {
  if (!s || s === 0) return '—';
  if (s >= 60) return Math.round(s/60) + ' min';
  return s + ' s';
}

function fmtRelTime(ts) {
  if (!ts) return '—';
  const diff = (Date.now() - new Date(ts).getTime()) / 1000;
  if (diff < 60)    return Math.round(diff) + ' s ago';
  if (diff < 3600)  return Math.round(diff/60) + ' min ago';
  if (diff < 86400) return Math.round(diff/3600) + ' h ago';
  return Math.round(diff/86400) + ' d ago';
}

function buildConfigFields(key, cfg) {
  const job = cfg[key] || {};
  const fields = [];

  if (key === 'checkpoint_archive') {
    fields.push(`
      <div class="row g-2 mt-1">
        <div class="col-6">
          <label class="form-label small mb-1">${_t.liveDatabaseRows}</label>
          <input type="number" class="form-control form-control-sm" id="cfg-${key}-keep_rows"
                 value="${job.keep_rows ?? 30000}" min="1000" step="1000">
        </div>
        <div class="col-6">
          <label class="form-label small mb-1">${_t.archiveRetention}</label>
          <input type="number" class="form-control form-control-sm" id="cfg-${key}-retain_days"
                 value="${job.retain_days ?? 90}" min="7">
        </div>
      </div>`);
  }
  if (key === 'admin_logs') {
    fields.push(`
      <div class="row g-2 mt-1">
        <div class="col-6">
          <label class="form-label small mb-1">${_t.maxFileSize}</label>
          <input type="number" class="form-control form-control-sm" id="cfg-${key}-max_mb"
                 value="${Math.round((job.max_bytes ?? 52428800)/1048576)}" min="5">
        </div>
        <div class="col-6">
          <label class="form-label small mb-1">${_t.backupFiles}</label>
          <input type="number" class="form-control form-control-sm" id="cfg-${key}-backup_count"
                 value="${job.backup_count ?? 2}" min="1" max="10">
        </div>
      </div>`);
  }
  if (key === 'journal') {
    fields.push(`
      <div class="row g-2 mt-1">
        <div class="col-6">
          <label class="form-label small mb-1">${_t.maxStorage}</label>
          <input type="number" class="form-control form-control-sm" id="cfg-${key}-max_use_mb"
                 value="${job.max_use_mb ?? 512}" min="64">
        </div>
        <div class="col-6">
          <label class="form-label small mb-1">${_t.retentionDays}</label>
          <input type="number" class="form-control form-control-sm" id="cfg-${key}-retain_days"
                 value="${job.retain_days ?? 30}" min="7">
        </div>
      </div>`);
  }
  if (key === 'prometheus') {
    fields.push(`
      <div class="row g-2 mt-1">
        <div class="col-6">
          <label class="form-label small mb-1">${_t.retentionDays}</label>
          <input type="number" class="form-control form-control-sm" id="cfg-${key}-retention_days"
                 value="${job.retention_days ?? 30}" min="7">
        </div>
        <div class="col-12"><small class="text-muted">${_t.prometheusRestartNote}</small></div>
      </div>`);
  }

  if (!fields.length) return '';
  const enabled = job.enabled !== false;
  return `
    <div class="collapse mt-2" id="cfg-panel-${key}">
      <div class="border rounded p-2 bg-light small">
        <div class="form-check form-switch mb-2">
          <input class="form-check-input" type="checkbox" id="cfg-${key}-enabled" ${enabled ? 'checked' : ''}>
          <label class="form-check-label" for="cfg-${key}-enabled">${_t.jobActive}</label>
        </div>
        ${fields.join('')}
        <button class="btn btn-sm btn-outline-primary mt-2" onclick="saveJobConfig('${key}')">
          <i class="bi bi-floppy me-1"></i>${_t.btnSave}
        </button>
      </div>
    </div>`;
}

function buildJobCard(key, stats, cfg) {
  const meta  = JOB_META[key] || { icon: 'bi-gear', label: key, color: 'secondary', canRun: false, hasConfig: false };
  const st    = stats[key] || {};
  const jobCfg = cfg[key] || {};
  const enabled = jobCfg.enabled !== false;

  const lastRunBadge = st.last_run_ts
    ? `<span class="badge bg-success bg-opacity-75">${fmtRelTime(st.last_run_ts)}</span>`
    : `<span class="badge bg-secondary">${_t.never}</span>`;

  const runBtn = meta.canRun
    ? `<button class="btn btn-sm btn-outline-${meta.color}" onclick="runJobNow('${key}')" id="run-btn-${key}">
         <i class="bi bi-play-fill me-1"></i>${_t.btnNow}
       </button>`
    : '';

  const cfgBtn = meta.hasConfig
    ? `<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="collapse" data-bs-target="#cfg-panel-${key}">
         <i class="bi bi-sliders me-1"></i>${_t.konfig}
       </button>`
    : '';

  const histRows = (st.last_details && Object.keys(st.last_details).length)
    ? Object.entries(st.last_details).map(([k,v]) =>
        `<div class="d-flex justify-content-between"><span class="text-muted">${escapeHtml(k)}:</span><span class="font-monospace">${escapeHtml(String(v))}</span></div>`
      ).join('')
    : '';

  return `
    <div class="col-12 col-lg-6 col-xl-4">
      <div class="card h-100 ${enabled ? '' : 'opacity-50'}">
        <div class="card-header d-flex justify-content-between align-items-center py-2">
          <span class="fw-semibold small"><i class="bi ${meta.icon} me-2 text-${meta.color}"></i>${meta.label}</span>
          ${lastRunBadge}
        </div>
        <div class="card-body py-2 small">
          <div class="d-flex justify-content-between mb-1">
            <span class="text-muted">${_t.lastRun}:</span>
            <span>${st.last_run_ts ? new Date(st.last_run_ts).toLocaleString() : '—'}</span>
          </div>
          <div class="d-flex justify-content-between mb-1">
            <span class="text-muted">${_t.duration}:</span>
            <span>${fmtDur(st.last_duration_s)} <span class="text-muted">(Ø ${fmtDur(st.avg_duration_s)})</span></span>
          </div>
          <div class="d-flex justify-content-between mb-1">
            <span class="text-muted">${_t.freed}:</span>
            <span>${fmtBytes(st.last_freed_bytes)} <span class="text-muted">(Ø ${fmtBytes(st.avg_freed_bytes)})</span></span>
          </div>
          <div class="d-flex justify-content-between mb-2">
            <span class="text-muted">${_t.totalRuns}:</span>
            <span>${st.run_count ?? '—'}</span>
          </div>
          ${histRows ? `<div class="border-top pt-2 mt-1">${histRows}</div>` : ''}
          ${buildConfigFields(key, cfg)}
        </div>
        <div class="card-footer d-flex gap-2 py-2">
          ${runBtn}${cfgBtn}
        </div>
      </div>
    </div>`;
}

async function loadCleanupStatus() {
  const box = document.getElementById('cleanup-manager-body');
  try {
    const d = await fetch('/api/cleanup/status').then(r => r.json());
    const { jobs, config } = d;

    let html = '<div class="row g-3">';
    for (const key of ALL_JOB_KEYS) {
      html += buildJobCard(key, jobs, config);
    }
    html += '</div>';
    box.innerHTML = html;
  } catch (e) {
    box.innerHTML = `<div class="text-danger p-3">Error: ${e.message}</div>`;
  }
}

async function runJobNow(job) {
  const btn = document.getElementById(`run-btn-${job}`);
  if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>'; }
  try {
    const r = await fetch(`/api/cleanup/run/${job}`, {method:'POST'}).then(r => r.json());
    flash(r.message || _t.jobStarted, 'success');
    setTimeout(loadCleanupStatus, 5000);
  } catch (e) {
    flash('Error: ' + e.message, 'danger');
  } finally {
    if (btn) { btn.disabled = false; btn.innerHTML = `<i class="bi bi-play-fill me-1"></i>${_t.btnNow}`; }
  }
}

async function saveJobConfig(key) {
  const cfg = await fetch('/api/cleanup/config').then(r => r.json());

  const get = id => {
    const el = document.getElementById(id);
    return el ? el.value : null;
  };
  const getNum = id => { const v = get(id); return v !== null ? Number(v) : null; };
  const getBool = id => { const el = document.getElementById(id); return el ? el.checked : null; };

  if (!cfg[key]) cfg[key] = {};
  cfg[key].enabled = getBool(`cfg-${key}-enabled`) ?? true;

  if (key === 'checkpoint_archive') {
    cfg[key].keep_rows   = getNum(`cfg-${key}-keep_rows`)   ?? cfg[key].keep_rows;
    cfg[key].retain_days = getNum(`cfg-${key}-retain_days`) ?? cfg[key].retain_days;
    // Update cron .env file as well
    await fetch('/api/cleanup/config', {method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify(cfg)});
    // Update moe-checkpoint-archive.env directly via a dedicated call
    const envContent = `CHECKPOINT_KEEP_ROWS=${cfg[key].keep_rows}\nARCHIVE_RETAIN_DAYS=${cfg[key].retain_days}\n`;
    await fetch('/api/cleanup/write-cron-env', {method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({job: key, content: envContent})});
  } else if (key === 'admin_logs') {
    const maxMb = getNum(`cfg-${key}-max_mb`);
    cfg[key].max_bytes    = maxMb !== null ? maxMb * 1048576 : cfg[key].max_bytes;
    cfg[key].backup_count = getNum(`cfg-${key}-backup_count`) ?? cfg[key].backup_count;
  } else if (key === 'journal') {
    cfg[key].max_use_mb  = getNum(`cfg-${key}-max_use_mb`)  ?? cfg[key].max_use_mb;
    cfg[key].retain_days = getNum(`cfg-${key}-retain_days`) ?? cfg[key].retain_days;
  } else if (key === 'prometheus') {
    cfg[key].retention_days = getNum(`cfg-${key}-retention_days`) ?? cfg[key].retention_days;
  }

  const r = await fetch('/api/cleanup/config', {
    method:'POST', headers:{'Content-Type':'application/json'},
    body: JSON.stringify(cfg)
  }).then(r => r.json());

  if (r.ok) {
    flash(_t.cfgSaved, 'success');
    await loadCleanupStatus();
  } else {
    flash('Error: ' + (r.detail || JSON.stringify(r)), 'danger');
  }
}

// ─── Template reference cleanup ──────────────────────────────────────────────

async function scanTemplateRefs() {
  const box = document.getElementById('tmplref-report');
  box.innerHTML = `<div class="text-center py-3"><span class="spinner-border spinner-border-sm"></span> ${_t.scanning}</div>`;
  try {
    const d = await fetch('/api/maintenance/template-refs/scan').then(r => r.json());
    const stale = d.stale_cc_profiles || [];
    document.getElementById('btn-tmplref-dryrun').disabled = stale.length === 0;
    document.getElementById('btn-tmplref-run').disabled    = stale.length === 0;
    if (stale.length === 0) {
      box.innerHTML = `<div class="text-success text-center py-3"><i class="bi bi-check-circle fs-3"></i><br>${_t.noStaleTmplRefs}</div>`;
      return;
    }
    let html = `<div class="alert alert-warning mb-2"><strong>${stale.length} ${_t.staleTmplRefsFound}</strong>
      <span class="text-muted ms-2 small">(${d.valid_template_count} valid templates)</span></div>`;
    html += `<table class="table table-sm mb-0"><thead><tr>
      <th>Profile</th><th>User ID</th><th>Stale Template ID</th>
    </tr></thead><tbody>`;
    for (const e of stale) {
      html += `<tr>
        <td class="font-monospace" style="font-size:.78rem">${escapeHtml(e.profile_name || e.profile_id)}</td>
        <td class="font-monospace text-muted" style="font-size:.75rem">${escapeHtml(e.user_id)}</td>
        <td class="font-monospace text-danger" style="font-size:.75rem">${escapeHtml(e.stale_template_id)}</td>
      </tr>`;
    }
    html += `</tbody></table>`;
    box.innerHTML = html;
  } catch (e) {
    box.innerHTML = `<div class="text-danger">Error: ${e.message}</div>`;
  }
}

async function runTemplateRefCleanup(dryRun) {
  const box = document.getElementById('tmplref-report');
  if (!dryRun && !confirm(_t.confirmTmplRefCleanup)) return;
  box.innerHTML = `<div class="text-center py-3"><span class="spinner-border spinner-border-sm"></span> ${_t.cleaning}</div>`;
  try {
    const d = await fetch('/api/maintenance/template-refs/cleanup', {
      method: 'POST', headers: {'Content-Type':'application/json'},
      body: JSON.stringify({dry_run: dryRun}),
    }).then(r => r.json());
    const label = dryRun ? ' (dry-run)' : '';
    let html = `<div class="alert alert-success"><strong>${_t.tmplRefCleanupDone}${label}:</strong>
      ${d.cleared_cc_profiles} profile(s) cleared, ${d.affected_users} user cache(s) invalidated.</div>`;
    if (d.errors && d.errors.length) {
      html += `<div class="alert alert-warning small"><strong>Errors:</strong><ul class="mb-0">`;
      for (const e of d.errors) html += `<li>${escapeHtml(e)}</li>`;
      html += `</ul></div>`;
    }
    box.innerHTML = html;
    if (!dryRun) {
      document.getElementById('btn-tmplref-dryrun').disabled = true;
      document.getElementById('btn-tmplref-run').disabled    = true;
    }
  } catch (e) {
    box.innerHTML = `<div class="text-danger">Error: ${e.message}</div>`;
  }
}

document.addEventListener('DOMContentLoaded', () => {
  pollOntology();
  setInterval(pollOntology, 3000);
  loadDedicatedTemplates();
  setInterval(pollDedicatedHealer, 5000);
  loadCleanupStatus();
  setInterval(loadCleanupStatus, 60000);
});
</script>
{% endblock %}
