#!/usr/bin/env python3
"""Aria Control Panel - Dashboard fuer Kais"""

from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
import subprocess
import json
import os
import time
import secrets
from datetime import datetime, timedelta
import sys
sys.path.insert(0, '/root/aria/scripts')
from importlib import import_module

TOKEN_FILE = "/root/.aria-control-token"

def get_token():
    if os.path.exists(TOKEN_FILE):
        with open(TOKEN_FILE) as f:
            return f.read().strip()
    token = secrets.token_urlsafe(24)
    with open(TOKEN_FILE, "w") as f:
        f.write(token)
    os.chmod(TOKEN_FILE, 0o600)
    return token

TOKEN = get_token()
PLANER_TASKS_FILE = "/root/.aria-planer-tasks.json"

def _read_env(key):
    for path in ('/root/aria/.env', '/root/.claude/channels/telegram/.env'):
        try:
            with open(path) as f:
                for line in f:
                    line = line.strip()
                    if line.startswith(key + '='):
                        return line.split('=', 1)[1].strip().strip('"').strip("'")
        except Exception:
            pass
    return os.environ.get(key, '')

TELEGRAM_BOT_TOKEN = _read_env('TELEGRAM_BOT_TOKEN')
LINEAR_TEAM_KEY = _read_env('LINEAR_TEAM_KEY') or 'ARI'
# Phase 0 fix (2026-05-17): systemd unit has no EnvironmentFile, so
# os.environ.get('TELEGRAM_CHAT_ID') was empty → chat_id=0 → Telegram API
# returned 400 'chat not found'. Read from .env files like the bot token.
OWNER_CHAT_ID = int(_read_env('TELEGRAM_CHAT_ID') or '0')

PLANER_PAGE = """<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no,viewport-fit=cover">
<title>Aria Planer</title>
<meta name="theme-color" content="#000000">
<meta name="apple-mobile-web-app-capable" content="yes">
<style>
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
:root{
  --bg:#14141a;--card:#1c1c1e;--card2:#2c2c2e;--sep:rgba(84,84,88,.65);
  --text:#fff;--t2:rgba(235,235,245,.6);--t3:rgba(235,235,245,.3);
  --accent:#1EF0A1;--green:#30d158;--amber:#ffd60a;--red:#ff453a;--cyan:#1EF0A1;
  --safe-top:env(safe-area-inset-top,0px);--safe-bot:env(safe-area-inset-bottom,0px);
  --hour-h:80px;--gutter:52px;
}
body{font-family:-apple-system,'Inter',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;overflow-x:hidden;-webkit-font-smoothing:antialiased}

/* Header */
.hdr{position:sticky;top:0;z-index:100;background:rgba(20,20,26,.92);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
  padding:max(12px,var(--safe-top)) 12px 12px;display:flex;align-items:center;justify-content:space-between;gap:8px;border-bottom:.5px solid var(--sep);flex-wrap:nowrap}
.hdr-nav{display:flex;align-items:center;gap:8px;flex-shrink:1;min-width:0}
.nav-btn{background:var(--card);border:none;color:var(--text);font-size:18px;width:34px;height:34px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.nav-btn:active{opacity:.6}
.date-label{font-size:14px;font-weight:700;min-width:0;flex-shrink:1;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.back-link{font-size:13px;color:var(--accent);text-decoration:none;padding:8px 0;white-space:nowrap;flex-shrink:0}
@media (max-width:480px){
  .back-link{font-size:0}
  .back-link::before{content:"←";font-size:18px}
}

/* Planer body */
.planer-wrap{display:flex;flex-direction:column;height:calc(100vh - 56px - var(--safe-bot));overflow:hidden}
.agent-headers{display:flex;margin-left:var(--gutter);border-bottom:.5px solid var(--sep);background:var(--bg);position:sticky;top:56px;z-index:90}
.agent-hdr{flex:1;text-align:center;padding:8px 4px;font-size:12px;font-weight:600;color:var(--t2);border-right:.5px solid var(--sep)}
.agent-hdr:last-child{border-right:none}

/* Timeline scroll */
.timeline-scroll{flex:1;overflow-y:auto;overflow-x:hidden;position:relative}
.timeline-inner{display:flex;position:relative}

/* Time gutter */
.gutter{width:var(--gutter);flex-shrink:0;position:sticky;left:0;background:var(--bg);z-index:80}
.gutter-hour{height:var(--hour-h);display:flex;align-items:flex-start;justify-content:flex-end;padding:2px 8px 0 0;font-size:10px;font-weight:500;color:var(--t3)}

/* Lanes */
.lanes{flex:1;display:flex;position:relative}
.lane{flex:1;position:relative;border-right:.5px solid var(--sep);min-width:120px}
.lane:last-child{border-right:none}
.lane-hour{height:var(--hour-h);border-bottom:.5px solid rgba(84,84,88,.3);position:relative}
.lane-hour:nth-child(2n){background:rgba(255,255,255,.01)}
.hour-line{position:absolute;top:50%;left:0;right:0;height:.5px;background:rgba(84,84,88,.15)}

/* Task blocks */
.task-block{position:absolute;left:4px;right:4px;border-radius:8px;padding:6px 8px;cursor:grab;user-select:none;touch-action:none;z-index:10;min-height:20px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,.4)}
.task-block:active{cursor:grabbing;opacity:.85}
.task-title{font-size:11px;font-weight:600;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.task-time{font-size:10px;opacity:.75;margin-top:1px}
.task-inactive{opacity:.4}
.t-research{background:linear-gradient(135deg,#1EF0A1,#009F6B)}
.t-social{background:linear-gradient(135deg,#9333ea,#c084fc)}
.t-code{background:linear-gradient(135deg,#0e7490,#64d2ff20);border:1px solid #64d2ff40}
.t-content{background:linear-gradient(135deg,#92400e,#fbbf24)}
.t-sync{background:linear-gradient(135deg,#166534,#30d158)}
.t-custom{background:linear-gradient(135deg,#374151,#6b7280)}

/* Current time line */
.now-line{position:absolute;left:0;right:0;height:2px;background:var(--accent);z-index:50;pointer-events:none;box-shadow:0 0 6px rgba(30,240,161,0.5)}
.now-dot{position:absolute;left:-4px;top:-4px;width:10px;height:10px;border-radius:50%;background:var(--accent);box-shadow:0 0 8px rgba(30,240,161,0.7)}

/* FAB */
.fab{position:fixed;bottom:calc(24px + var(--safe-bot));right:20px;width:52px;height:52px;border-radius:50%;border:none;background:var(--accent);color:#000;font-size:26px;cursor:pointer;box-shadow:0 4px 20px rgba(30,240,161,.4);z-index:200;display:flex;align-items:center;justify-content:center;transition:transform .15s,box-shadow .15s}
.fab:active{transform:scale(.92);box-shadow:0 2px 10px rgba(30,240,161,.3)}

/* Desktop: center planer content */
@media(min-width:960px){
  body{max-width:1200px;margin:0 auto;border-left:.5px solid rgba(255,255,255,.05);border-right:.5px solid rgba(255,255,255,.05)}
  .fab{right:calc(max(20px,(100vw - 1200px)/2 + 20px))}
  .agent-hdr{font-size:13px;padding:10px 8px}
}

/* Modal */
.overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:300;display:flex;align-items:flex-end;justify-content:center}
.overlay.hidden{display:none}
.modal{background:var(--card);border-radius:20px 20px 0 0;width:100%;max-width:600px;padding:20px 20px calc(20px + var(--safe-bot));max-height:90vh;overflow-y:auto}
.modal-handle{width:36px;height:4px;background:var(--sep);border-radius:2px;margin:0 auto 16px}
.modal-title{font-size:17px;font-weight:700;margin-bottom:20px}
.form-row{margin-bottom:16px}
.form-label{font-size:12px;color:var(--t2);font-weight:600;margin-bottom:6px;display:block}
.form-input,.form-select{width:100%;background:var(--card2);border:.5px solid var(--sep);border-radius:10px;padding:12px;color:var(--text);font-size:15px;font-family:inherit;-webkit-appearance:none;appearance:none}
.form-select option{background:#1c1c1e}
.form-row-2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.form-toggle{display:flex;align-items:center;justify-content:space-between}
.toggle{width:51px;height:31px;background:var(--sep);border-radius:16px;position:relative;cursor:pointer;transition:background .2s;flex-shrink:0}
.toggle.on{background:var(--green)}
.toggle-knob{position:absolute;top:2px;left:2px;width:27px;height:27px;border-radius:50%;background:#fff;transition:transform .2s;box-shadow:0 1px 4px rgba(0,0,0,.4)}
.toggle.on .toggle-knob{transform:translateX(20px)}
.modal-btns{display:flex;gap:10px;margin-top:20px}
.btn-cancel{flex:1;padding:14px;border:none;border-radius:12px;background:var(--card2);color:var(--t2);font-size:15px;font-weight:600;cursor:pointer}
.btn-save{flex:2;padding:14px;border:none;border-radius:12px;background:var(--accent);color:#000;font-size:15px;font-weight:600;cursor:pointer}
.btn-del{flex:1;padding:14px;border:none;border-radius:12px;background:rgba(255,69,58,.15);color:var(--red);font-size:15px;font-weight:600;cursor:pointer}
</style>
</head>
<body>
<div class="hdr">
  <a href="/?t=__TOKEN__" class="back-link">← Dashboard</a>
  <div class="hdr-nav">
    <button class="nav-btn" onclick="changeDate(-1)">‹</button>
    <div class="date-label" id="date-label">Heute</div>
    <button class="nav-btn" onclick="changeDate(1)">›</button>
  </div>
  <div style="width:80px;text-align:right;font-size:12px;color:var(--t3)" id="task-count">0 Tasks</div>
</div>

<div class="planer-wrap">
  <div class="agent-headers" id="agent-headers">
    <div class="agent-hdr">Aria</div>
  </div>
  <div class="timeline-scroll" id="timeline-scroll">
    <div class="timeline-inner">
      <div class="gutter" id="gutter"></div>
      <div class="lanes" id="lanes">
        <div class="lane" id="lane-aria" data-agent="aria"></div>
      </div>
    </div>
  </div>
</div>

<button class="fab" onclick="openModal(null)">+</button>

<div class="overlay hidden" id="overlay" onclick="overlayClick(event)">
  <div class="modal" id="modal">
    <div class="modal-handle"></div>
    <div class="modal-title" id="modal-title">Neuer Task</div>
    <div class="form-row">
      <label class="form-label">Aufgabe</label>
      <input class="form-input" id="f-title" placeholder="Was soll Aria tun?">
    </div>
    <div class="form-row">
      <label class="form-label">Typ</label>
      <select class="form-select" id="f-type">
        <option value="research">🔬 Recherche / Analyse</option>
        <option value="social">📱 Social Media</option>
        <option value="code">💻 Code / Deploy</option>
        <option value="content">✍️ Content erstellen</option>
        <option value="sync">🔄 Sync / Backup</option>
        <option value="custom">📋 Custom</option>
      </select>
    </div>
    <div class="form-row form-row-2">
      <div>
        <label class="form-label">Startzeit</label>
        <input class="form-input" id="f-start" type="time" value="02:00">
      </div>
      <div>
        <label class="form-label">Dauer (Min)</label>
        <input class="form-input" id="f-dur" type="number" value="30" min="15" step="15">
      </div>
    </div>
    <div class="form-row">
      <label class="form-label">Notizen</label>
      <input class="form-input" id="f-notes" placeholder="Optional">
    </div>
    <div class="form-row form-toggle">
      <span class="form-label" style="margin:0">Aktiv</span>
      <div class="toggle on" id="f-toggle" onclick="toggleActive()"><div class="toggle-knob"></div></div>
    </div>
    <div class="modal-btns">
      <button class="btn-del" id="btn-del" style="display:none" onclick="deleteTask()">Löschen</button>
      <button class="btn-cancel" onclick="closeModal()">Abbrechen</button>
      <button class="btn-save" onclick="saveTask()">Speichern</button>
    </div>
  </div>
</div>

<script>
const TOKEN = new URLSearchParams(window.location.search).get('t') || '';
const HOUR_H = 80;
const TYPES = {research:'t-research',social:'t-social',code:'t-code',content:'t-content',sync:'t-sync',custom:'t-custom'};
const TYPE_LABELS = {research:'Recherche',social:'Social',code:'Code',content:'Content',sync:'Sync',custom:'Custom'};

let currentDate = new Date();
let tasks = [];
let editId = null;
let activeToggle = true;
let dragState = null;

// Format date
function fmtDate(d) {
  return d.toLocaleDateString('de-DE',{weekday:'short',day:'numeric',month:'long',year:'numeric'});
}
function fmtDateKey(d) {
  return d.toISOString().slice(0,10);
}

// Time helpers
function timeToMin(t) { const [h,m] = t.split(':').map(Number); return h*60+m; }
function minToTime(m) { return String(Math.floor(m/60)).padStart(2,'0')+':'+String(m%60).padStart(2,'0'); }
function minToY(m) { return m * HOUR_H / 60; }
function yToMin(y) { return Math.round(y * 60 / HOUR_H / 15) * 15; }

// API
async function apiFetch(path, opts) {
  const sep = path.includes('?') ? '&' : '?';
  return fetch(path + sep + 't=' + TOKEN, opts).then(r => r.json());
}

// Build timeline structure
function buildTimeline() {
  const gutter = document.getElementById('gutter');
  const lane = document.getElementById('lane-aria');
  gutter.innerHTML = '';
  lane.innerHTML = '';
  for (let h = 0; h < 24; h++) {
    const g = document.createElement('div');
    g.className = 'gutter-hour';
    g.textContent = String(h).padStart(2,'0') + ':00';
    gutter.appendChild(g);
    const row = document.createElement('div');
    row.className = 'lane-hour';
    row.dataset.hour = h;
    const hl = document.createElement('div');
    hl.className = 'hour-line';
    row.appendChild(hl);
    lane.appendChild(row);
  }
  // Tap on lane to create
  lane.addEventListener('pointerup', onLaneTap);
}

function onLaneTap(e) {
  if (dragState) return;
  if (e.target.closest('.task-block')) return;
  const rect = e.currentTarget.getBoundingClientRect();
  const y = e.clientY - rect.top;
  const minutes = yToMin(y);
  openModal(null, minToTime(Math.max(0, Math.min(1410, minutes))));
}

// Render tasks for current date
function renderTasks() {
  const lane = document.getElementById('lane-aria');
  lane.querySelectorAll('.task-block').forEach(el => el.remove());
  const dateKey = fmtDateKey(currentDate);
  const dayTasks = tasks.filter(t => t.date === dateKey && t.agent === 'aria');
  document.getElementById('task-count').textContent = dayTasks.length + ' Task' + (dayTasks.length !== 1 ? 's' : '');
  dayTasks.forEach(task => {
    const block = document.createElement('div');
    block.className = 'task-block ' + (TYPES[task.type] || 't-custom') + (task.active ? '' : ' task-inactive');
    block.dataset.id = task.id;
    const startMin = timeToMin(task.start);
    const topPx = minToY(startMin);
    const hPx = Math.max(24, minToY(task.duration));
    block.style.top = topPx + 'px';
    block.style.height = hPx + 'px';
    block.innerHTML = '<div class="task-title">' + escHtml(task.title) + '</div>' +
      (hPx > 36 ? '<div class="task-time">' + task.start + ' · ' + task.duration + 'min</div>' : '');
    // Drag (also handles tap-to-edit at end of pointerup if no movement)
    initDrag(block, task);
    lane.appendChild(block);
  });
  updateNowLine();
}

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

// Current time indicator
function updateNowLine() {
  const lane = document.getElementById('lane-aria');
  const old = lane.querySelector('.now-line');
  if (old) old.remove();
  const today = new Date();
  if (fmtDateKey(today) !== fmtDateKey(currentDate)) return;
  const mins = today.getHours()*60 + today.getMinutes();
  const line = document.createElement('div');
  line.className = 'now-line';
  line.style.top = minToY(mins) + 'px';
  line.innerHTML = '<div class="now-dot"></div>';
  lane.appendChild(line);
}

// Drag & Drop
function initDrag(el, task) {
  let startY, startTop, startMin;
  el.addEventListener('pointerdown', e => {
    if (e.button && e.button !== 0) return;
    e.preventDefault();
    el.setPointerCapture(e.pointerId);
    const rect = el.getBoundingClientRect();
    startY = e.clientY;
    startTop = parseInt(el.style.top);
    startMin = timeToMin(task.start);
    dragState = {id: task.id, moving: false};
  });
  el.addEventListener('pointermove', e => {
    if (!dragState || dragState.id !== task.id) return;
    const dy = e.clientY - startY;
    if (Math.abs(dy) > 5) dragState.moving = true;
    if (!dragState.moving) return;
    const newTop = Math.max(0, Math.min(minToY(24*60 - task.duration), startTop + dy));
    el.style.top = newTop + 'px';
  });
  el.addEventListener('pointerup', e => {
    if (!dragState || dragState.id !== task.id) return;
    const wasMoving = dragState.moving;
    dragState = null;
    if (!wasMoving) {
      // Tap (no drag) = open edit modal
      e.stopPropagation();
      openModal(task.id);
      return;
    }
    const newMin = yToMin(parseInt(el.style.top));
    const clamped = Math.max(0, Math.min(1410, newMin));
    task.start = minToTime(clamped);
    apiFetch('/api/tasks', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(task)});
    renderTasks();
  });
}

// Modal
function openModal(id, presetTime) {
  editId = id;
  activeToggle = true;
  const isEdit = id !== null;
  document.getElementById('modal-title').textContent = isEdit ? 'Task bearbeiten' : 'Neuer Task';
  document.getElementById('btn-del').style.display = isEdit ? '' : 'none';
  if (isEdit) {
    const t = tasks.find(x => x.id === id);
    if (!t) return;
    document.getElementById('f-title').value = t.title;
    document.getElementById('f-type').value = t.type;
    document.getElementById('f-start').value = t.start;
    document.getElementById('f-dur').value = t.duration;
    document.getElementById('f-notes').value = t.notes || '';
    activeToggle = t.active;
  } else {
    document.getElementById('f-title').value = '';
    document.getElementById('f-type').value = 'research';
    document.getElementById('f-start').value = presetTime || '02:00';
    document.getElementById('f-dur').value = '30';
    document.getElementById('f-notes').value = '';
    activeToggle = true;
  }
  setToggleUI(activeToggle);
  document.getElementById('overlay').classList.remove('hidden');
  setTimeout(() => document.getElementById('f-title').focus(), 100);
}

function closeModal() { document.getElementById('overlay').classList.add('hidden'); editId = null; }
function overlayClick(e) { if (e.target === document.getElementById('overlay')) closeModal(); }
function toggleActive() { activeToggle = !activeToggle; setToggleUI(activeToggle); }
function setToggleUI(v) { document.getElementById('f-toggle').className = 'toggle' + (v ? ' on' : ''); }

async function saveTask() {
  try {
    const titleEl = document.getElementById('f-title');
    const title = (titleEl && titleEl.value || '').trim();
    if (!title) {
      alert('Bitte Aufgabe eingeben');
      if (titleEl) titleEl.focus();
      return;
    }
    const newId = (typeof crypto !== 'undefined' && crypto.randomUUID)
      ? crypto.randomUUID()
      : 't_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
    const task = {
      id: editId || newId,
      agent: 'aria',
      title,
      type: (document.getElementById('f-type') || {}).value || 'custom',
      date: fmtDateKey(currentDate),
      start: (document.getElementById('f-start') || {}).value || '08:00',
      duration: parseInt((document.getElementById('f-dur') || {}).value) || 30,
      active: activeToggle,
      status: 'pending',
      notes: ((document.getElementById('f-notes') || {}).value || '').trim()
    };
    const res = await apiFetch('/api/tasks', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(task)});
    if (res && res.ok === false) {
      alert('Fehler beim Speichern: ' + (res.message || res.error || 'unbekannt'));
      return;
    }
    closeModal();
    await loadTasks();
  } catch (err) {
    alert('Speichern fehlgeschlagen: ' + (err && err.message ? err.message : err));
    console.error('saveTask error', err);
  }
}

async function deleteTask() {
  try {
    if (!editId) {
      alert('Kein Task ausgewaehlt. Tippe auf einen bestehenden Task in der Timeline um ihn zu bearbeiten.');
      return;
    }
    if (!confirm('Task wirklich loeschen?')) return;
    const res = await apiFetch('/api/tasks/delete', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:editId,token:TOKEN})});
    if (res && res.ok === false) {
      alert('Fehler beim Loeschen: ' + (res.message || res.error || 'unbekannt'));
      return;
    }
    closeModal();
    await loadTasks();
  } catch (err) {
    alert('Loeschen fehlgeschlagen: ' + (err && err.message ? err.message : err));
    console.error('deleteTask error', err);
  }
}

// Load tasks
async function loadTasks() {
  const data = await apiFetch('/api/tasks');
  tasks = data.tasks || [];
  renderTasks();
}

// Date nav
function changeDate(dir) {
  currentDate = new Date(currentDate.getTime() + dir * 86400000);
  document.getElementById('date-label').textContent = fmtDate(currentDate);
  renderTasks();
}

// Scroll to current time or 22:00 if evening
function scrollToNow() {
  const now = new Date();
  const h = now.getHours();
  const targetHour = h >= 20 ? 22 : (h >= 8 ? h - 1 : 0);
  document.getElementById('timeline-scroll').scrollTop = targetHour * HOUR_H;
}

// Init
buildTimeline();
document.getElementById('date-label').textContent = fmtDate(currentDate);
loadTasks().then(scrollToNow);
setInterval(updateNowLine, 60000);
</script>
</body>
</html>"""

# === KAR-679 Worker-Fleet Team-View (additive) ===
WORKERS_STATE = "/root/aria/state/workers"
_WORKER_NAME_RE = __import__('re').compile(r'^[a-zA-Z0-9][a-zA-Z0-9-]*$')

TEAM_PAGE = """<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover,user-scalable=no">
<title>Aria Team</title>
<meta name="theme-color" content="#080808">
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
:root{
  --bg:#14141a;--card:rgba(255,255,255,0.045);--card-solid:#1c1d25;
  --text:#F0EEE8;--t2:rgba(240,238,232,0.85);--t3:rgba(240,238,232,0.55);--t4:rgba(240,238,232,0.28);
  --accent:#1EF0A1;--amber:#ffd60a;--red:#ff453a;--green:#1EF0A1;
  --border:rgba(255,255,255,0.08);--sep:rgba(255,255,255,0.07);
  --r:12px;--r-sm:8px;--safe-bot:env(safe-area-inset-bottom,0px);
}
body{font-family:'IBM Plex Sans',-apple-system,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;overflow-x:hidden;-webkit-font-smoothing:antialiased}
.app{max-width:960px;margin:0 auto;padding-bottom:calc(80px + var(--safe-bot))}
.hdr{position:sticky;top:0;z-index:50;background:rgba(15,15,22,0.92);backdrop-filter:blur(24px);-webkit-backdrop-filter:blur(24px);
  border-bottom:1px solid var(--sep);padding:14px 20px;padding-top:max(14px,env(safe-area-inset-top));
  display:flex;align-items:center;justify-content:space-between}
.hdr-left{display:flex;align-items:center;gap:12px}
.hdr-title{font-size:16px;font-weight:700}
.back-link{font-size:13px;color:var(--accent);text-decoration:none;padding:8px 0}
.sec-hdr{font-size:10px;font-weight:700;color:var(--t4);text-transform:uppercase;letter-spacing:0.9px;
  padding:14px 20px 6px;display:flex;align-items:center;gap:8px}
.sec-hdr::before{content:'';width:2px;height:12px;background:linear-gradient(to bottom,var(--accent),var(--accent));border-radius:1px;flex-shrink:0}
.workers-list{padding:0 16px;display:flex;flex-direction:column;gap:10px}
.worker-card{background:var(--card);border:1px solid var(--border);border-radius:var(--r);overflow:hidden}
.worker-card.ready{border-color:rgba(30,240,161,0.2)}
.worker-card.dead{border-color:rgba(255,69,58,0.18);opacity:0.7}
.worker-head{padding:10px 14px;display:flex;align-items:center;gap:10px;cursor:pointer;user-select:none}
.worker-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.worker-dot.ready{background:var(--green);box-shadow:0 0 0 3px rgba(30,240,161,0.15);animation:pulse 2.4s ease-in-out infinite}
.worker-dot.busy{background:var(--amber);box-shadow:0 0 0 3px rgba(255,214,10,0.15)}
.worker-dot.dead{background:var(--red)}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.3}}
.worker-name{font-size:14px;font-weight:700;letter-spacing:-0.2px}
.worker-role{font-size:11px;color:var(--t3);margin-top:1px}
.worker-meta{display:flex;align-items:center;gap:8px;margin-left:auto;flex-shrink:0}
.mode-badge{font-size:10px;padding:2px 7px;border-radius:5px;font-weight:700;
  background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);color:var(--t2)}
.mode-badge.yolo{background:rgba(255,214,10,0.12);border-color:rgba(255,214,10,0.2);color:var(--amber)}
.mode-badge.off{background:rgba(255,69,58,0.08);border-color:rgba(255,69,58,0.15);color:var(--red)}
.ago-label{font-size:11px;color:var(--t4);font-variant-numeric:tabular-nums;white-space:nowrap}
.worker-body{display:none;border-top:1px solid var(--sep)}
.worker-body.open{display:block}
.pane-box{margin:10px 14px;background:rgba(0,0,0,0.45);border:1px solid rgba(255,255,255,0.04);
  border-radius:8px;padding:10px 12px;font-family:'JetBrains Mono',monospace;font-size:11px;
  color:var(--t3);max-height:180px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;line-height:1.65}
.worker-info{padding:4px 14px 8px;display:flex;flex-wrap:wrap;gap:6px}
.info-chip{font-size:10px;color:var(--t4);background:rgba(255,255,255,0.04);
  border:1px solid rgba(255,255,255,0.06);border-radius:5px;padding:2px 7px;font-family:'JetBrains Mono',monospace}
.worker-controls{padding:8px 14px 12px;display:flex;flex-wrap:wrap;gap:6px}
.wbtn{padding:6px 11px;border-radius:7px;border:1px solid;font-size:12px;font-weight:600;
  font-family:inherit;cursor:pointer;transition:all 0.15s;background:transparent}
.wbtn:active{transform:scale(0.96);opacity:0.8}
.wbtn:disabled{opacity:0.3;cursor:wait}
.wbtn-view{border-color:rgba(100,210,255,0.3);color:#64d2ff}
.wbtn-send{border-color:rgba(30,240,161,0.3);color:var(--accent)}
.wbtn-stop{border-color:rgba(255,69,58,0.25);color:var(--red)}
.wbtn-mode{border-color:rgba(255,255,255,0.12);color:var(--t2)}
.wbtn-refresh{border-color:rgba(255,255,255,0.1);color:var(--t3)}
.prompt-row{display:flex;gap:6px;padding:0 14px 10px}
.prompt-input{flex:1;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);
  border-radius:10px;color:var(--text);font-family:inherit;font-size:13px;padding:7px 12px;
  outline:none;transition:border-color 0.2s}
.prompt-input:focus{border-color:rgba(30,240,161,0.35)}
.prompt-input::placeholder{color:var(--t4)}
.prompt-fb{font-size:11px;color:var(--t4);padding:0 14px 8px;height:20px;transition:color 0.3s}
.prompt-fb.ok{color:var(--accent)}
.prompt-fb.err{color:var(--red)}
.spawn-form{margin:0 16px 10px;background:var(--card);border:1px solid var(--border);
  border-radius:var(--r);padding:14px}
.spawn-form .form-row{margin-bottom:10px}
.spawn-form label{font-size:11px;color:var(--t3);font-weight:600;display:block;margin-bottom:4px;text-transform:uppercase;letter-spacing:0.4px}
.spawn-form input{width:100%;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);
  border-radius:8px;color:var(--text);font-family:inherit;font-size:14px;padding:9px 12px;outline:none;transition:border-color 0.2s}
.spawn-form input:focus{border-color:rgba(30,240,161,0.35)}
.spawn-btn{width:100%;padding:10px;border:none;border-radius:10px;background:var(--accent);
  color:#000;font-family:inherit;font-size:14px;font-weight:700;cursor:pointer;transition:opacity 0.15s}
.spawn-btn:active{opacity:0.8}
.spawn-btn:disabled{opacity:0.4;cursor:wait}
.spawn-fb{font-size:12px;color:var(--t4);text-align:center;margin-top:8px;min-height:18px;transition:color 0.3s}
.spawn-fb.ok{color:var(--accent)}
.spawn-fb.err{color:var(--red)}
.empty-state{padding:40px 20px;text-align:center;color:var(--t4);font-size:13px}
.refresh-bar{display:flex;align-items:center;gap:8px;padding:6px 20px;font-size:11px;color:var(--t4)}
.refresh-bar button{background:none;border:1px solid var(--border);border-radius:6px;
  color:var(--t3);font-family:inherit;font-size:11px;padding:4px 10px;cursor:pointer}
.refresh-bar button:active{opacity:0.7}
@media(min-width:640px){
  .workers-list{padding:0 24px}
  .spawn-form{margin:0 24px 10px}
  .hdr{padding:14px 24px}
  .sec-hdr{padding:14px 24px 6px}
  .refresh-bar{padding:6px 24px}
}
</style>
</head>
<body>
<div class="app">
<div class="hdr">
  <div class="hdr-left">
    <a href="/?t=__TOKEN__" class="back-link">&#8592; Dashboard</a>
    <div class="hdr-title">Team</div>
  </div>
  <div style="font-size:11px;color:var(--t4)" id="hdr-count">&#8212; workers</div>
</div>
<div class="sec-hdr">Aktive Worker</div>
<div class="refresh-bar">
  <span id="refresh-ts">&#8212;</span>
  <button onclick="loadWorkers()">Refresh</button>
  <span style="margin-left:auto;color:var(--t4);font-size:10px">Auto 10s</span>
</div>
<div class="workers-list" id="workers-list">
  <div class="empty-state">Lade...</div>
</div>
<div class="sec-hdr" style="margin-top:16px">Worker starten</div>
<div class="spawn-form">
  <div class="form-row">
    <label>Name</label>
    <input id="sp-name" placeholder="kadi-dev" pattern="[a-zA-Z0-9][a-zA-Z0-9-]*">
  </div>
  <div class="form-row">
    <label>Projekt-Pfad</label>
    <input id="sp-project" placeholder="/root/projekte/Kadi-v2">
  </div>
  <div class="form-row" style="margin-bottom:14px">
    <label>Rolle</label>
    <input id="sp-role" placeholder="frontend">
  </div>
  <button class="spawn-btn" id="spawn-btn" onclick="spawnWorker()">Worker starten</button>
  <div class="spawn-fb" id="spawn-fb"></div>
</div>
</div>
<script>
const TOKEN = new URLSearchParams(window.location.search).get('t') || '';
let _workers = [];
function escH(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
async function apiFetch(path, opts) {
  const sep = path.includes('?') ? '&' : '?';
  return fetch(path + sep + 't=' + TOKEN, opts).then(r => r.json());
}
async function loadWorkers() {
  try {
    const d = await apiFetch('/api/workers');
    if (!d.ok) throw new Error(d.error || 'unknown');
    _workers = d.workers || [];
    renderWorkers(_workers);
    const ts = new Date().toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit',second:'2-digit'});
    document.getElementById('refresh-ts').textContent = 'Stand ' + ts;
    document.getElementById('hdr-count').textContent = _workers.length + ' worker' + (_workers.length !== 1 ? 's' : '');
  } catch(e) {
    document.getElementById('workers-list').innerHTML =
      '<div class="empty-state" style="color:var(--red)">Fehler: ' + escH(e.message) + '</div>';
  }
}
function renderWorkers(workers) {
  const list = document.getElementById('workers-list');
  if (!workers.length) {
    list.innerHTML = '<div class="empty-state">Keine aktiven Worker. Worker oben starten.</div>';
    return;
  }
  const openSet = new Set([...list.querySelectorAll('.worker-body.open')].map(el => el.dataset.name));
  list.innerHTML = workers.map(w => workerCard(w, openSet.has(w.name))).join('');
}
function dotClass(w) {
  if (!w.alive) return 'dead';
  if (w.ready) return 'ready';
  return 'busy';
}
function workerCard(w, open) {
  const dc = dotClass(w);
  const mc = w.mode === 'yolo' ? 'yolo' : (w.mode === 'off' ? 'off' : '');
  const branch = w.branch ? '<span class="info-chip">' + escH(w.branch) + '</span>' : '';
  const workdir = w.workdir ? '<span class="info-chip">' + escH(w.workdir) + '</span>' : '';
  const pid = w.pid && w.pid !== 'unknown' ? '<span class="info-chip">PID ' + escH(w.pid) + '</span>' : '';
  return `
<div class="worker-card ${dc}" id="wcard-${escH(w.name)}">
  <div class="worker-head" onclick="toggleCard('${escH(w.name)}')">
    <span class="worker-dot ${dc}"></span>
    <div style="flex:1;min-width:0">
      <div class="worker-name">${escH(w.name)}</div>
      <div class="worker-role">${escH(w.role || '—')} &middot; ${escH(w.scope || '—')}</div>
    </div>
    <div class="worker-meta">
      <span class="mode-badge ${mc}">${escH(w.mode || 'assist')}</span>
      <span class="ago-label">${escH(w.last_activity_ago || '—')}</span>
      <span style="color:var(--t4);font-size:14px">${open ? '▾' : '›'}</span>
    </div>
  </div>
  <div class="worker-body${open ? ' open' : ''}" data-name="${escH(w.name)}">
    <div class="pane-box" id="pane-${escH(w.name)}">${escH(w.pane_snapshot || '—')}</div>
    <div class="worker-info">
      ${workdir}${branch}${pid}
    </div>
    <div class="worker-controls">
      <button class="wbtn wbtn-view" onclick="attachWorker('${escH(w.name)}')">Attach (info)</button>
      <button class="wbtn wbtn-refresh" onclick="refreshPane('${escH(w.name)}')">Pane</button>
      <button class="wbtn wbtn-mode" onclick="cycleMode('${escH(w.name)}','${escH(w.mode || 'assist')}')">Mode</button>
      <button class="wbtn wbtn-stop" onclick="stopWorker('${escH(w.name)}')">Stop</button>
    </div>
    <div class="prompt-row">
      <input class="prompt-input" id="pinput-${escH(w.name)}" placeholder="Prompt an ${escH(w.name)} senden…"
        onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendPrompt('${escH(w.name)}');}">
      <button class="wbtn wbtn-send" onclick="sendPrompt('${escH(w.name)}')">Send</button>
    </div>
    <div class="prompt-fb" id="pfb-${escH(w.name)}"></div>
  </div>
</div>`;
}
function toggleCard(name) {
  const body = document.querySelector(`#wcard-${name} .worker-body`);
  const arrow = document.querySelector(`#wcard-${name} .worker-head span:last-child`);
  if (!body) return;
  body.classList.toggle('open');
  if (arrow) arrow.textContent = body.classList.contains('open') ? '▾' : '›';
}
async function refreshPane(name) {
  const box = document.getElementById('pane-' + name);
  if (!box) return;
  box.textContent = 'Lade…';
  try {
    const d = await apiFetch('/api/worker-pane?name=' + encodeURIComponent(name));
    box.textContent = d.ok ? (d.pane || '(leer)') : ('Fehler: ' + (d.message || 'unknown'));
    box.scrollTop = box.scrollHeight;
  } catch(e) {
    box.textContent = 'Fehler: ' + e.message;
  }
}
async function sendPrompt(name) {
  const input = document.getElementById('pinput-' + name);
  const fb = document.getElementById('pfb-' + name);
  const text = (input && input.value || '').trim();
  if (!text) return;
  input.disabled = true;
  if (fb) { fb.textContent = 'Sende…'; fb.className = 'prompt-fb'; }
  try {
    const d = await apiFetch('/api/worker-send', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({name, text})
    });
    if (d.ok) {
      input.value = '';
      if (fb) { fb.textContent = '✓ Gesendet'; fb.className = 'prompt-fb ok'; }
      setTimeout(() => { if (fb) { fb.textContent = ''; fb.className = 'prompt-fb'; } }, 2500);
      setTimeout(() => refreshPane(name), 4000);
    } else {
      if (fb) { fb.textContent = '✗ ' + (d.message || 'Fehler'); fb.className = 'prompt-fb err'; }
    }
  } catch(e) {
    if (fb) { fb.textContent = '✗ Verbindungsfehler'; fb.className = 'prompt-fb err'; }
  }
  input.disabled = false;
  if (input) input.focus();
}
function attachWorker(name) {
  alert('tmux attach -t aria-' + name + '\\n\\nIn Terminal einfügen zum live Attach.');
}
async function cycleMode(name, current) {
  const modes = ['assist', 'auto', 'yolo'];
  const next = modes[(modes.indexOf(current) + 1) % modes.length];
  if (!confirm('Mode von "' + current + '" auf "' + next + '" ändern?')) return;
  try {
    const d = await apiFetch('/api/worker-mode', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({name, mode: next})
    });
    if (d.ok) { loadWorkers(); }
    else { alert('Fehler: ' + (d.message || 'unknown')); }
  } catch(e) { alert('Fehler: ' + e.message); }
}
async function stopWorker(name) {
  if (!confirm('Worker "' + name + '" stoppen?')) return;
  const card = document.getElementById('wcard-' + name);
  if (card) card.style.opacity = '0.4';
  try {
    const d = await apiFetch('/api/worker-stop', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({name})
    });
    setTimeout(loadWorkers, 1500);
    if (!d.ok) alert('Stop-Fehler: ' + (d.message || 'unknown'));
  } catch(e) {
    alert('Fehler: ' + e.message);
    if (card) card.style.opacity = '';
  }
}
async function spawnWorker() {
  const name    = (document.getElementById('sp-name').value || '').trim();
  const project = (document.getElementById('sp-project').value || '').trim();
  const role    = (document.getElementById('sp-role').value || '').trim();
  const fb = document.getElementById('spawn-fb');
  const btn = document.getElementById('spawn-btn');
  if (!name || !project || !role) {
    fb.textContent = 'Alle 3 Felder ausfüllen';
    fb.className = 'spawn-fb err';
    return;
  }
  btn.disabled = true;
  fb.textContent = 'Starte… (~30s)';
  fb.className = 'spawn-fb';
  try {
    const d = await apiFetch('/api/worker-start', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({name, project, role})
    });
    if (d.ok) {
      fb.textContent = '✓ Spawn gestartet für ' + name + '. Erscheint in ~30s.';
      fb.className = 'spawn-fb ok';
      document.getElementById('sp-name').value = '';
      document.getElementById('sp-role').value = '';
      let polls = 0;
      const poll = setInterval(async () => {
        polls++;
        await loadWorkers();
        const found = _workers.some(w => w.name === name);
        if (found || polls > 24) {
          clearInterval(poll);
          if (found) {
            fb.textContent = '✓ ' + name + ' ist online';
          } else {
            fb.textContent = 'Spawn läuft (prüfe /var/log/aria-worker-spawn.log)';
          }
        }
      }, 5000);
    } else {
      fb.textContent = '✗ ' + (d.message || 'Fehler');
      fb.className = 'spawn-fb err';
    }
  } catch(e) {
    fb.textContent = '✗ ' + e.message;
    fb.className = 'spawn-fb err';
  }
  btn.disabled = false;
}
loadWorkers();
setInterval(loadWorkers, 10000);
</script>
</body>
</html>"""

HTML_PAGE = """<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no">
<title>Aria Control</title>
<meta name="theme-color" content="#080808">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="Aria">
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='none' stroke='%231EF0A1' stroke-width='1.5'><circle cx='16' cy='16' r='13' opacity='0.3'/><path d='M16 6 L24 23 L8 23 Z'/><circle cx='16' cy='17' r='2' fill='%231EF0A1'/></svg>">
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
:root {
    --bg: #14141a; --bg-card: rgba(255,255,255,0.045); --bg-card-solid: #1c1d25;
    --text: #F0EEE8; --text-2: rgba(240,238,232,0.85); --text-3: rgba(240,238,232,0.55); --text-4: rgba(240,238,232,0.28);
    --accent: #1EF0A1; --cyan: #1EF0A1; --green: #1EF0A1; --amber: #ffd60a; --red: #ff453a;
    --border: rgba(255,255,255,0.08); --separator: rgba(255,255,255,0.07);
    --safe-bottom: env(safe-area-inset-bottom, 0px); --r: 12px; --r-sm: 8px;
}
body { font-family: 'IBM Plex Sans', -apple-system, sans-serif; background: var(--bg); color: var(--text); font-size: 15px; line-height: 1.47; margin: 0; -webkit-font-smoothing: antialiased; min-height: 100vh; overflow-x: hidden; }
.app { max-width: 960px; margin: 0 auto; padding-bottom: calc(180px + var(--safe-bottom)); overflow-x: hidden; }

/* Header */
.header {
    padding: 14px 20px; padding-top: max(14px, env(safe-area-inset-top));
    display: flex; align-items: center; justify-content: space-between;
    background: rgba(15,15,22,0.92); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
    position: sticky; top: 0; z-index: 50; border-bottom: 1px solid var(--separator);
    position: relative;
}
.header::after { content: ''; position: absolute; bottom: -1px; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent 5%, rgba(30,240,161,0.4) 35%, rgba(100,210,255,0.4) 65%, transparent 95%); }
.header { position: sticky; }
.header-left { display: flex; align-items: center; gap: 12px; }
.header-icon { width: 30px; height: 34px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.header-title { font-size: 16px; font-weight: 700; letter-spacing: -0.3px; }
.header-status { display: flex; align-items: center; gap: 5px; font-size: 11px; font-weight: 500; color: var(--text-3); margin-top: 1px; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); animation: pulse 2s ease-in-out infinite; flex-shrink: 0; }
.status-dot--off { background: var(--red); animation: none; }
.status-dot--check { background: var(--cyan); }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }

/* Sections */
.section { padding: 0 16px; margin-bottom: 16px; }
.section-header { font-size: 10px; font-weight: 700; color: var(--text-4); text-transform: uppercase; letter-spacing: 0.9px; padding: 10px 16px 4px; display: flex; align-items: center; gap: 8px; }
.section-header::before { content: ''; width: 2px; height: 12px; background: linear-gradient(to bottom, var(--accent), var(--cyan)); border-radius: 1px; flex-shrink: 0; }
.card-group { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; }
.card-row { display: flex; align-items: center; justify-content: space-between; padding: 7px 14px; min-height: 32px; border-bottom: 1px solid var(--separator); }
.card-row:last-child { border-bottom: none; }
.card-row-left { display: flex; align-items: center; gap: 8px; flex: 1; }
.card-row-label { font-size: 12px; }
.card-row-value { font-size: 11px; color: var(--text-3); }

/* Buttons */
.btn-row { display: flex; gap: 6px; padding: 0 14px; margin-bottom: 10px; }
.btn { flex: 1; padding: 7px; border-radius: var(--r-sm); cursor: pointer; font-size: 12px; font-weight: 600; font-family: inherit; transition: all 0.15s; min-height: 32px; }
.btn:active { transform: scale(0.97); opacity: 0.8; }
.btn-restart { background: transparent; border: 1px solid rgba(255,69,58,0.25); color: var(--red); }
.btn-refresh { background: transparent; border: 1px solid rgba(30,240,161,0.2); color: var(--accent); }
.btn-git { background: transparent; border: 1px solid rgba(48,209,88,0.25); color: var(--green); }
.btn:disabled { opacity: 0.3; cursor: wait; transform: none; }

/* Stats tiles */
.sys-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; padding: 0 14px; margin-bottom: 10px; }
.sys-tile { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 8px 10px; }
.sys-tile-label { font-size: 9px; color: var(--text-4); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
.sys-tile-value { font-size: 14px; font-weight: 700; margin-top: 2px; letter-spacing: -0.3px; }
.stat-bar { height: 2px; background: rgba(255,255,255,0.06); border-radius: 2px; margin-top: 8px; overflow: hidden; }
.stat-bar-fill { height: 100%; border-radius: 2px; transition: width 0.5s; }
.bar-green { background: linear-gradient(90deg, var(--green), #34d399); }
.bar-yellow { background: linear-gradient(90deg, var(--amber), #fb923c); }
.bar-red { background: linear-gradient(90deg, var(--red), #f87171); }

/* Live banner */
.live-banner { background: linear-gradient(135deg, rgba(30,240,161,0.06), rgba(100,210,255,0.03)); border: 1px solid rgba(30,240,161,0.12); border-radius: var(--r); padding: 14px 16px; margin: 12px 16px; display: flex; align-items: center; gap: 12px; }
.live-spinner { width: 16px; height: 16px; border: 1.5px solid rgba(30,240,161,0.2); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; flex-shrink: 0; }
@keyframes spin { to { transform: rotate(360deg); } }
.live-label { font-size: 13px; color: var(--text-3); }
.live-label strong { color: var(--text); font-weight: 600; }

/* Log box */
.log-box { background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.04); border-radius: 8px; padding: 12px; font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--text-3); max-height: 200px; overflow-y: auto; overflow-x: hidden; line-height: 1.7; white-space: pre-wrap; word-break: break-all; width: 100%; }

/* Badges */
.badge { font-size: 11px; padding: 2px 8px; border-radius: 6px; font-weight: 600; letter-spacing: 0.2px; }
.badge-g { background: rgba(48,209,88,0.1); color: var(--green); border: 1px solid rgba(48,209,88,0.2); }
.badge-r { background: rgba(255,69,58,0.1); color: var(--red); border: 1px solid rgba(255,69,58,0.2); }

/* Tasks */
.task-item { font-size: 13px; padding: 6px 0; display: flex; align-items: center; gap: 8px; }
.task-done { color: var(--green); } .task-active { color: var(--cyan); } .task-pending { color: var(--text-4); }

/* Action log */
.action-log { margin: 0 16px; padding: 10px 12px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; font-family: monospace; font-size: 11px; color: var(--text-4); max-height: 80px; overflow-y: auto; display: none; }

/* Dot */
.dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 6px; }
.dot-g { background: var(--green); box-shadow: 0 0 5px rgba(48,209,88,0.5); }
.dot-a { background: var(--amber); }
.dot-r { background: var(--red); box-shadow: 0 0 5px rgba(255,69,58,0.5); }

/* Tab Bar */
.tab-bar { position: fixed; bottom: 0; left: 0; right: 0; background: rgba(8,8,12,0.95); backdrop-filter: saturate(180%) blur(24px); -webkit-backdrop-filter: saturate(180%) blur(24px); border-top: 0.5px solid rgba(255,255,255,0.07); display: flex; align-items: center; padding: 8px 0; padding-bottom: max(8px, var(--safe-bottom)); z-index: 100; justify-content: space-around; scrollbar-width: none; }
.tab-bar::-webkit-scrollbar { display: none; }
.tab-item { display: flex; flex-direction: column; align-items: center; gap: 3px; padding: 4px 8px; border: none; background: none; cursor: pointer; -webkit-tap-highlight-color: transparent; flex: 1; min-width: 0; position: relative; transition: opacity 0.15s; }
.tab-item svg { width: 22px; height: 22px; color: var(--text-4); transition: color 0.2s; }
.tab-item span { font-size: 9px; font-weight: 600; color: var(--text-4); transition: color 0.2s; letter-spacing: 0.4px; text-transform: uppercase; }
.tab-item.active svg { color: var(--accent); }
.tab-item.active span { color: var(--accent); }
.tab-item.active::after { content: ''; position: absolute; top: -8px; left: 50%; transform: translateX(-50%); width: 20px; height: 2px; background: var(--accent); border-radius: 0 0 3px 3px; box-shadow: 0 0 8px rgba(30,240,161,0.7); }

/* Issue / Calendar items */
.issue-item { padding: 7px 14px; border-bottom: 1px solid var(--separator); }
.issue-item:last-child { border-bottom: none; }
.issue-tappable { cursor: pointer; -webkit-tap-highlight-color: transparent; }
.issue-id { font-size: 9px; font-weight: 700; color: var(--text-4); margin-bottom: 1px; letter-spacing: 0.5px; text-transform: uppercase; }
.issue-title { font-size: 12px; color: var(--text); line-height: 1.35; }
.issue-sub { font-size: 11px; color: var(--amber); margin-top: 1px; }
.issue-row { display: flex; align-items: center; gap: 6px; }
.issue-row .issue-content { flex: 1; min-width: 0; }
.issue-actions { display: flex; gap: 4px; flex-shrink: 0; }
.issue-btn { height: 24px; border: 1px solid; border-radius: 6px; background: transparent; cursor: pointer; font-size: 10px; font-weight: 700; padding: 0 7px; transition: all 0.15s; white-space: nowrap; }
.issue-btn.start { border-color: rgba(147,197,253,0.4); color: var(--cyan); }
.issue-btn.start:hover { background: rgba(100,210,255,0.1); border-color: var(--cyan); }
.issue-btn.done { border-color: rgba(30,240,161,0.35); color: var(--accent); }
.issue-btn.done:hover { background: rgba(30,240,161,0.1); border-color: var(--accent); }
.issue-btn:disabled { opacity: 0.35; cursor: default; }
.cal-time { font-size: 13px; font-weight: 700; color: var(--cyan); white-space: nowrap; padding-left: 12px; }

/* Responsive */
@media (min-width: 640px) {
    .sys-grid { grid-template-columns: 1fr 1fr 1fr; padding: 0 24px; gap: 10px; }
    .section { padding: 0 24px; }
    .section-header { padding: 16px 24px 6px; }
    .btn-row { padding: 0 24px; }
    .header { padding: 14px 24px; }
    .live-banner { margin: 12px 24px; }
    .action-log { margin: 0 24px; }
}
@media (min-width: 960px) {
    .tab-bar { left: 50%; right: auto; transform: translateX(-50%); width: 960px; border-left: 0.5px solid rgba(255,255,255,0.07); border-right: 0.5px solid rgba(255,255,255,0.07); justify-content: center; }
}
/* Summary grid */
.sum-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; padding: 0 14px; margin-bottom: 4px; width: 100%; }
.sum-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 9px 11px 9px; overflow: hidden; min-width: 0; }
.sum-label { font-size: 9px; color: var(--text-4); font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3px; }
.sum-val { font-size: 14px; font-weight: 700; letter-spacing: -0.3px; line-height: 1.1; }
.sum-sub { font-size: 10px; color: var(--text-4); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (min-width: 640px) { .sum-grid { grid-template-columns: repeat(4,1fr); padding: 0 24px; } }

/* Logo */
#aria-logo { filter: invert(1); transition: filter 0.2s; }

/* Theme toggle button */
#theme-btn, #brain-btn { background: none; border: none; cursor: pointer; padding: 6px; border-radius: 8px; color: var(--text-4); font-size: 16px; line-height: 1; display: flex; align-items: center; justify-content: center; transition: color 0.2s; }
#theme-btn:active, #brain-btn:active { opacity: 0.7; }
#brain-btn:hover { color: var(--accent); }
.qs-btn { background: var(--bg-card-solid); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 12px 8px; color: var(--text); font-family: inherit; cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 6px; transition: border-color 0.2s, transform 0.1s; -webkit-tap-highlight-color: transparent; }
.qs-btn:hover { border-color: var(--accent); }
.qs-btn:active { transform: scale(0.97); }
.qs-btn:disabled { cursor: wait; }
.qs-label { font-size: 11px; color: var(--text-2); font-weight: 500; text-align: center; line-height: 1.2; }

/* Light mode */
body.light {
    --bg: #F2F2F7; --bg-card: rgba(0,0,0,0.04); --bg-card-solid: #FFFFFF;
    --text: #1C1C1E; --text-2: rgba(28,28,30,0.75); --text-3: rgba(28,28,30,0.45); --text-4: rgba(28,28,30,0.22);
    --accent: #059669; --cyan: #0891B2; --green: #059669; --amber: #D97706; --red: #DC2626;
    --border: rgba(0,0,0,0.08); --separator: rgba(0,0,0,0.05);
}
body.light .header { background: rgba(242,242,247,0.92); }
body.light .tab-bar { background: rgba(242,242,247,0.95); border-top-color: rgba(0,0,0,0.08); }
body.light .log-box { background: rgba(0,0,0,0.04); border-color: rgba(0,0,0,0.06); }
body.light .stat-bar { background: rgba(0,0,0,0.06); }
body.light .btn-restart { border-color: rgba(220,38,38,0.2); }
body.light .btn-refresh { border-color: rgba(5,150,105,0.2); }
body.light .btn-git { border-color: rgba(5,150,105,0.2); }
body.light .live-banner { background: linear-gradient(135deg, rgba(5,150,105,0.06), rgba(8,145,178,0.03)); border-color: rgba(5,150,105,0.12); }
body.light #aria-logo { filter: none; }
body.light .header::after { background: linear-gradient(90deg, transparent 5%, rgba(5,150,105,0.4) 35%, rgba(8,145,178,0.4) 65%, transparent 95%); }

/* Aria Inbox */
.aria-inbox { position: fixed; bottom: calc(56px + max(8px, var(--safe-bottom))); left: 0; right: 0; z-index: 99; background: rgba(8,8,12,0.97); backdrop-filter: saturate(180%) blur(24px); -webkit-backdrop-filter: saturate(180%) blur(24px); border-top: 1px solid rgba(30,240,161,0.18); padding: 8px 12px; }
@media (min-width: 960px) { .aria-inbox { left: 50%; right: auto; transform: translateX(-50%); width: 960px; border-left: 0.5px solid rgba(255,255,255,0.07); border-right: 0.5px solid rgba(255,255,255,0.07); } }
.inbox-row { display: flex; align-items: flex-end; gap: 8px; }
.inbox-input { flex: 1; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); border-radius: 18px; color: var(--text); font-family: inherit; font-size: 14px; line-height: 1.4; padding: 8px 14px; resize: none; max-height: 100px; overflow-y: auto; outline: none; transition: border-color 0.2s; }
.inbox-input:focus { border-color: rgba(30,240,161,0.35); }
.inbox-input::placeholder { color: var(--text-4); }
.inbox-send { width: 36px; height: 36px; border-radius: 50%; border: none; background: var(--accent); color: #000; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: opacity 0.15s, transform 0.1s; font-size: 14px; }
.inbox-send:active { transform: scale(0.92); opacity: 0.8; }
.inbox-send svg { width: 16px; height: 16px; }
.inbox-feedback { font-size: 11px; color: var(--text-4); text-align: center; padding-top: 4px; height: 16px; transition: color 0.3s; }
.inbox-feedback.ok { color: var(--accent); }
.inbox-feedback.err { color: var(--red); }
/* Effort quick-set — Buttons senden /effort <level> an die Aria-Session */
.effort-row { display: flex; align-items: center; gap: 6px; padding: 0 2px 6px; }
.effort-label { font-size: 10px; color: var(--text-4); text-transform: uppercase; letter-spacing: 0.06em; flex-shrink: 0; }
.effort-chip { flex: 1; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; color: var(--text-3); font-family: inherit; font-size: 11px; padding: 4px 0; cursor: pointer; transition: background 0.15s, border-color 0.15s, color 0.15s; }
.effort-chip:active { transform: scale(0.94); }
.effort-chip.active { background: rgba(30,240,161,0.15); border-color: rgba(30,240,161,0.5); color: var(--accent); }
body.light .effort-chip { background: rgba(0,0,0,0.04); border-color: rgba(0,0,0,0.1); }
body.light .aria-inbox { background: rgba(242,242,247,0.97); border-top-color: rgba(5,150,105,0.2); }
body.light .inbox-input { background: rgba(0,0,0,0.04); border-color: rgba(0,0,0,0.1); color: var(--text); }
body.light .inbox-input:focus { border-color: rgba(5,150,105,0.4); }

/* ====== BRAIN BROWSER ====== */
.brain-layout { display: flex; gap: 0; height: calc(100vh - 56px - 80px); min-height: 480px; }
.brain-sidebar { width: 280px; flex-shrink: 0; overflow-y: auto; border-right: 1px solid var(--separator); padding: 8px 4px 60px 8px; background: var(--bg-card); }
.brain-reader { flex: 1; overflow-y: auto; padding: 18px 22px 80px 22px; min-width: 0; }
.brain-tree-folder { user-select: none; }
.brain-tree-row { display: flex; align-items: center; gap: 6px; padding: 4px 6px; border-radius: 6px; cursor: pointer; font-size: 13px; color: var(--text-2); }
.brain-tree-row:hover { background: rgba(255,255,255,0.05); color: var(--text); }
.brain-tree-row.active { background: rgba(30,240,161,0.12); color: var(--accent); }
.brain-tree-row .caret { width: 12px; display: inline-block; text-align: center; color: var(--text-4); font-size: 10px; transition: transform 0.15s; }
.brain-tree-row.collapsed .caret { transform: rotate(-90deg); }
.brain-tree-row .icon { font-size: 12px; opacity: 0.7; flex-shrink: 0; }
.brain-tree-row .name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.brain-tree-children { padding-left: 14px; border-left: 1px dashed rgba(255,255,255,0.05); margin-left: 8px; }
.brain-tree-children.collapsed { display: none; }

.brain-reader-title { font-size: 22px; font-weight: 700; letter-spacing: -0.4px; margin-bottom: 4px; color: var(--text); }
.brain-reader-meta { font-size: 11px; color: var(--text-4); margin-bottom: 16px; font-family: 'JetBrains Mono', monospace; }
.brain-reader-frontmatter { background: rgba(255,255,255,0.03); border: 1px solid var(--separator); border-radius: 8px; padding: 10px 12px; margin-bottom: 16px; font-size: 11px; font-family: 'JetBrains Mono', monospace; color: var(--text-3); white-space: pre-wrap; }
.brain-reader-content { font-size: 15px; line-height: 1.65; color: var(--text); }
.brain-reader-content h1 { font-size: 22px; font-weight: 700; margin: 24px 0 12px; letter-spacing: -0.3px; }
.brain-reader-content h2 { font-size: 18px; font-weight: 600; margin: 20px 0 10px; color: var(--text); border-bottom: 1px solid var(--separator); padding-bottom: 4px; }
.brain-reader-content h3 { font-size: 15px; font-weight: 600; margin: 16px 0 8px; color: var(--text-2); }
.brain-reader-content h4 { font-size: 14px; font-weight: 600; margin: 12px 0 6px; color: var(--text-2); }
.brain-reader-content p { margin: 8px 0; }
.brain-reader-content ul, .brain-reader-content ol { margin: 8px 0 8px 22px; }
.brain-reader-content li { margin: 4px 0; }
.brain-reader-content code { background: rgba(255,255,255,0.06); padding: 1px 5px; border-radius: 4px; font-family: 'JetBrains Mono', monospace; font-size: 12.5px; color: var(--accent); }
.brain-reader-content pre { background: rgba(0,0,0,0.4); border: 1px solid var(--separator); border-radius: 8px; padding: 12px; margin: 12px 0; overflow-x: auto; }
.brain-reader-content pre code { background: none; padding: 0; color: var(--text); font-size: 12px; }
.brain-reader-content blockquote { border-left: 3px solid var(--accent); padding: 4px 0 4px 14px; margin: 12px 0; color: var(--text-2); font-style: italic; }
.brain-reader-content a { color: var(--cyan); text-decoration: none; border-bottom: 1px dotted rgba(100,210,255,0.4); }
.brain-reader-content a:hover { border-bottom-style: solid; }
.brain-reader-content .wikilink { color: var(--accent); cursor: pointer; border-bottom: 1px dotted rgba(30,240,161,0.4); padding: 1px 3px; border-radius: 3px; }
.brain-reader-content .wikilink:hover { background: rgba(30,240,161,0.1); border-bottom-style: solid; }
.brain-reader-content .wikilink.broken { color: var(--text-4); border-bottom-color: rgba(255,255,255,0.1); }
.brain-reader-content table { border-collapse: collapse; margin: 12px 0; width: 100%; font-size: 13px; }
.brain-reader-content th, .brain-reader-content td { border: 1px solid var(--separator); padding: 6px 10px; text-align: left; }
.brain-reader-content th { background: rgba(255,255,255,0.04); font-weight: 600; }
.brain-reader-content hr { border: none; border-top: 1px solid var(--separator); margin: 18px 0; }
.brain-reader-empty { color: var(--text-4); font-size: 13px; text-align: center; padding: 60px 20px; }

.brain-mobile-toggle { display: none; align-items: center; gap: 8px; position: sticky; top: 0; padding: 8px 12px; background: var(--bg-card-solid); border-bottom: 1px solid var(--separator); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); z-index: 5; }
.brain-mobile-toggle button { background: var(--bg-card); border: 1px solid var(--border); color: var(--text); padding: 6px 12px; border-radius: 6px; font-size: 13px; cursor: pointer; font-family: inherit; }
.brain-mobile-toggle button.active { background: rgba(30,240,161,0.15); border-color: rgba(30,240,161,0.35); color: var(--accent); }
#brain-graph-svg .node { cursor: pointer; transition: r 0.15s; }
#brain-graph-svg .node:hover { stroke: var(--accent); stroke-width: 2px; }
#brain-graph-svg .node.active { stroke: var(--accent); stroke-width: 3px; }
#brain-graph-svg .link { stroke: rgba(255,255,255,0.12); stroke-width: 1px; }
#brain-graph-svg .label { font-size: 10px; fill: var(--text-3); pointer-events: none; font-family: 'IBM Plex Sans', sans-serif; }

@media (max-width: 768px) {
    .brain-layout { flex-direction: column; height: calc(100vh - 56px - 80px - 56px); min-height: 480px; }
    .brain-sidebar { width: 100%; max-height: 50vh; border-right: none; border-bottom: 1px solid var(--separator); display: none; flex-shrink: 0; }
    .brain-sidebar.open { display: block; }
    .brain-reader { padding: 14px 16px 80px 16px; flex: 1; min-height: 0; }
    #brain-graph-view { flex: 1; min-height: 60vh; }
    #brain-graph-svg { min-height: 60vh; }
    .brain-mobile-toggle { display: flex !important; }
}

body.light .brain-sidebar { background: rgba(0,0,0,0.025); }
body.light .brain-sidebar-header { background: rgba(242,242,247,0.97) !important; }
body.light .brain-tree-row { color: var(--text-2); }
body.light .brain-tree-row:hover { background: rgba(0,0,0,0.04); color: var(--text); }
body.light .brain-tree-row.active { background: rgba(5,150,105,0.1); color: var(--accent); }
body.light .brain-reader-content code { background: rgba(0,0,0,0.06); color: #047857; }
body.light .brain-reader-content pre { background: rgba(0,0,0,0.04); }
body.light .brain-reader-content pre code { background: none; color: var(--text); }
body.light #brain-graph-svg .link { stroke: rgba(0,0,0,0.1); }
body.light #brain-graph-svg .label { fill: var(--text-2); }

/* ============================================================
   STATUSLINE — slim, terminal-style header (Home tab top).
   Mirrors Claude Code's /statusline: folder · branch · model · bar.
   ============================================================ */
.statusline {
    margin: 12px 16px 10px;
    padding: 12px 14px 10px;
    background: rgba(255,255,255,0.025);
    border: 1px solid var(--border);
    border-radius: 10px;
    position: relative;
    overflow: hidden;
    font-family: 'JetBrains Mono', monospace;
    font-size: 12.5px;
    line-height: 1.45;
    color: var(--text-2);
}
.statusline::before {
    content: '';
    position: absolute; left: 0; right: 0; top: 0;
    height: 1px;
    background: linear-gradient(90deg, transparent 0%, rgba(30,240,161,0.55) 15%, rgba(30,240,161,0.1) 60%, transparent 100%);
}
.sl-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.sl-row + .sl-row { margin-top: 6px; }
.sl-dot {
    width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
    background: var(--green);
    box-shadow: 0 0 0 4px rgba(30,240,161,0.12);
    animation: pulse 2.4s ease-in-out infinite;
}
.sl-dot.idle    { background: var(--amber); box-shadow: 0 0 0 4px rgba(255,214,10,0.10); animation: none; }
.sl-dot.offline { background: var(--red);   box-shadow: 0 0 0 4px rgba(255,69,58,0.12);  animation: none; }
.sl-folder { color: #5fd5ff; font-weight: 600; }
.sl-branch { color: #ffd56b; font-weight: 600; }
.sl-model  { color: #d896ff; font-weight: 600; white-space: nowrap; }
.sl-sep    { color: var(--text-4); }
.sl-bar    { color: var(--accent); letter-spacing: 0; font-size: 12.5px; white-space: nowrap; }
.sl-bar.warn { color: var(--amber); }
.sl-bar.crit { color: var(--red); }
.sl-pct    { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
.sl-meta   { color: var(--text-3); font-size: 11.5px; }
.sl-meta .sl-key { color: var(--text-4); margin-right: 4px; }
.sl-meta .sl-val { color: var(--text-2); }
.sl-meta .branch-state { color: var(--text-4); margin-left: 4px; }

.sl-today {
    display: flex; gap: 14px;
    padding-top: 8px; margin-top: 8px;
    border-top: 1px dashed var(--separator);
    font-size: 11.5px; color: var(--text-3);
}
.sl-today .sl-key { color: var(--text-4); margin-right: 4px; text-transform: uppercase; letter-spacing: 0.4px; }
.sl-today b { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }

/* Light-mode overrides */
body.light .statusline { background: rgba(0,0,0,0.025); }
body.light .statusline::before { background: linear-gradient(90deg, transparent 0%, rgba(5,150,105,0.5) 15%, rgba(5,150,105,0.1) 60%, transparent 100%); }
body.light .sl-folder { color: #0891b2; }
body.light .sl-branch { color: #b45309; }
body.light .sl-model  { color: #6d28d9; }

/* News (Aria's own) — list style, not card-grid */
.news-list { padding: 0 16px; }
.news-item {
    display: flex; flex-direction: column; gap: 4px;
    padding: 12px 0;
    border-bottom: 1px solid var(--separator);
}
.news-item:last-child { border-bottom: none; }
.news-item-meta {
    display: flex; align-items: center; gap: 8px;
    font-size: 10px; color: var(--text-4);
    text-transform: uppercase; letter-spacing: 0.6px; font-weight: 600;
}
.news-item-meta .news-source { color: var(--accent); }
.news-item-title { font-size: 14px; font-weight: 600; color: var(--text); line-height: 1.35; }
.news-item-summary { font-size: 12.5px; color: var(--text-3); line-height: 1.5; }

/* Footer actions — Restart/Git collapsed */
.footer-actions {
    display: flex; gap: 8px;
    margin: 6px 16px 0;
}
.footer-actions .action-btn {
    flex: 1;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 9px 12px;
    color: var(--text-3);
    font-family: inherit; font-size: 12px; font-weight: 500;
    cursor: pointer;
    transition: border-color 0.2s, color 0.2s;
}
.footer-actions .action-btn:hover { color: var(--text); border-color: rgba(255,255,255,0.18); }
.footer-actions .action-btn.danger:hover { color: var(--red); border-color: rgba(255,69,58,0.3); }

/* Session-log details (collapsible) */
.session-log-block {
    margin: 0 16px 14px;
    border: 1px solid var(--border);
    border-radius: 10px;
    overflow: hidden;
}
.session-log-block summary {
    cursor: pointer;
    padding: 10px 14px;
    font-size: 11px; color: var(--text-3);
    text-transform: uppercase; letter-spacing: 0.7px; font-weight: 600;
    user-select: none;
    list-style: none;
    display: flex; align-items: center; justify-content: space-between;
}
.session-log-block summary::-webkit-details-marker { display: none; }
.session-log-block summary::after {
    content: '+'; color: var(--text-4); font-size: 14px; font-weight: 400;
}
.session-log-block[open] summary::after { content: '−'; }
.session-log-block .log-box { border: none; border-radius: 0; max-height: 320px; }

@media (min-width: 640px) {
    .hero-card { margin: 18px 24px 22px; }
    .news-list { padding: 0 24px; }
    .footer-actions { margin: 6px 24px 0; }
    .session-log-block { margin: 0 24px 14px; }
}
</style>
</head>
<body>
<div class="app">
    <div class="header">
        <div class="header-left">
            <div class="header-icon">
                <!-- Inline SVG mark (no external host) -->
                <svg id="aria-logo" viewBox="0 0 32 32" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent)" aria-label="Aria">
                    <circle cx="16" cy="16" r="13" opacity="0.25"/>
                    <path d="M16 6 L24 23 L8 23 Z"/>
                    <circle cx="16" cy="17" r="2" fill="currentColor" stroke="none"/>
                </svg>
            </div>
            <div>
                <div class="header-title">Aria Control</div>
                <div class="header-status" id="header-status"><span class="status-dot status-dot--check"></span> Prüfe...</div>
            </div>
        </div>
        <div style="display:flex;align-items:center;gap:6px;">
            <span style="font-size:11px;color:var(--text-4);font-variant-numeric:tabular-nums" id="uptime"></span>
            <button id="planer-btn" onclick="openPlaner()" title="Planer öffnen" style="background:none;border:none;cursor:pointer;padding:6px;border-radius:8px;color:var(--text-4);display:flex;align-items:center;justify-content:center;transition:color 0.2s">
                <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="3" y1="10" x2="21" y2="10"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="16" y1="2" x2="16" y2="6"/></svg>
            </button>
            <button id="theme-btn" onclick="toggleTheme()" title="Hell/Dunkel">🌙</button>
        </div>
    </div>

    <!-- ====== STATUS / ÜBERSICHT ====== -->
    <div id="view-home">
        <!-- Statusline (folder · branch · model · context bar) -->
        <section class="statusline" id="statusline">
            <div class="sl-row">
                <span class="sl-dot" id="sl-dot"></span>
                <span class="sl-folder" id="sl-folder">—</span>
                <span class="sl-sep">·</span>
                <span class="sl-branch" id="sl-branch">—</span>
                <span class="sl-sep">·</span>
                <span class="sl-model" id="sl-model">—</span>
            </div>
            <div class="sl-row">
                <span class="sl-bar" id="sl-bar">▱▱▱▱▱▱▱▱▱▱</span>
                <span class="sl-pct" id="sl-pct">—%</span>
                <span class="sl-sep">·</span>
                <span class="sl-meta" id="sl-tokens">— / —</span>
                <span class="sl-sep">·</span>
                <span class="sl-meta" id="sl-updated">—</span>
            </div>
            <div class="sl-row sl-meta">
                <span class="sl-key">cwd</span>
                <span class="sl-val" id="sl-cwd">—</span>
                <span class="branch-state" id="sl-branch-state"></span>
            </div>
            <div class="sl-row sl-meta">
                <span class="sl-key">last</span>
                <span class="sl-val" id="sl-last">—</span>
            </div>
            <div class="sl-today">
                <span><span class="sl-key">heute</span> <b id="today-commits">—</b> commits</span>
                <span><b id="today-tasks">—</b> tasks</span>
                <span><b id="today-telegrams">—</b> telegrams</span>
                <span style="margin-left:auto"><span class="sl-key">uptime</span> <b id="sl-uptime">—</b></span>
            </div>
        </section>

        <!-- Aktive Aria-Tasks -->
        <div id="aria-tasks" class="section" style="margin: 0 16px 12px;"></div>

        <!-- Session-Output (always open) -->
        <div class="section-header">Session-Output</div>
        <div class="section">
            <div class="card-group">
                <div style="padding:12px"><div class="log-box" id="session-log" style="max-height:340px;">Lade…</div></div>
            </div>
        </div>

        <!-- Aria-Erkenntnisse — eigene Funde, in einfachen Worten -->
        <div class="section-header">Aria-Erkenntnisse</div>
        <div class="news-list" id="home-erkenntnisse">
            <div style="padding:14px 0;color:var(--text-4);font-size:12px">Lade…</div>
        </div>

        <!-- KI-News — externe Headlines + Tipps -->
        <div class="section-header">KI-News &amp; Tipps</div>
        <div class="news-list" id="home-news-items">
            <div style="padding:14px 0;color:var(--text-4);font-size:12px">Lade…</div>
        </div>

        <!-- Sekundäre Aktionen (Restart, Git Pull) — bewusst klein -->
        <div class="footer-actions">
            <button class="action-btn danger" id="restart-btn" onclick="restart()">Restart</button>
            <button class="action-btn" onclick="gitPull()">Git Pull</button>
            <button class="action-btn" onclick="loadAll()">Refresh</button>
        </div>

        <div class="action-log" id="action-log"></div>
    </div>

    <!-- ====== INFRA (System + Services + Logs) ====== -->
    <div id="view-infra" style="display:none;">
        <div class="section-header" style="margin-top:12px">Hardware</div>
        <div class="sys-grid">
            <div class="sys-tile"><div class="sys-tile-label">CPU</div><div class="sys-tile-value" id="cpu">-</div><div class="stat-bar"><div class="stat-bar-fill" id="cpu-bar" style="width:0%"></div></div></div>
            <div class="sys-tile"><div class="sys-tile-label">RAM</div><div class="sys-tile-value" id="ram">-</div><div class="stat-bar"><div class="stat-bar-fill" id="ram-bar" style="width:0%"></div></div></div>
            <div class="sys-tile"><div class="sys-tile-label">Disk</div><div class="sys-tile-value" id="disk">-</div><div class="stat-bar"><div class="stat-bar-fill" id="disk-bar" style="width:0%"></div></div></div>
            <div class="sys-tile"><div class="sys-tile-label">Load</div><div class="sys-tile-value" id="load">-</div></div>
        </div>

        <div class="section-header">Max Plan Nutzung</div>
        <div class="sys-grid">
            <div class="sys-tile"><div class="sys-tile-label">Session</div><div class="sys-tile-value" id="usage-current">-</div></div>
            <div class="sys-tile"><div class="sys-tile-label">Uptime</div><div class="sys-tile-value" id="usage-duration">-</div></div>
            <div class="sys-tile">
                <div class="sys-tile-label">Heute</div>
                <div class="sys-tile-value" id="usage-today">-</div>
                <div style="font-size:10px;color:var(--text-4);margin-top:2px">Limit ~900M/Tag</div>
                <div class="stat-bar"><div class="stat-bar-fill" id="usage-today-bar" style="width:0%"></div></div>
            </div>
            <div class="sys-tile"><div class="sys-tile-label">Sessions heute</div><div class="sys-tile-value" id="usage-today-sessions">-</div></div>
            <div class="sys-tile">
                <div class="sys-tile-label">Diese Woche</div>
                <div class="sys-tile-value" id="usage-week">-</div>
                <div style="font-size:10px;color:var(--text-4);margin-top:2px">Limit ~6300M/Wo</div>
                <div class="stat-bar"><div class="stat-bar-fill" id="usage-week-bar" style="width:0%"></div></div>
            </div>
            <div class="sys-tile"><div class="sys-tile-label">Claude CPU</div><div class="sys-tile-value" id="usage-cpu">-</div></div>
            <div class="sys-tile"><div class="sys-tile-label">Claude RAM</div><div class="sys-tile-value" id="usage-ram">-</div></div>
        </div>

        <div class="section-header">Dienste</div>
        <div class="section">
            <div class="card-group" id="services"><div style="padding:16px;color:var(--text-4)">Lade...</div></div>
        </div>

        <div class="section-header">n8n Workflows</div>
        <div class="section"><div class="card-group" id="n8n-active"><div style="padding:16px;color:var(--text-4)">Lade...</div></div></div>
        <div class="section-header">n8n Inaktiv</div>
        <div class="section"><div class="card-group" id="n8n-inactive"></div></div>

        <div class="section-header">Watchdog Log</div>
        <div class="section">
            <div class="card-group"><div style="padding:12px"><div class="log-box" id="watchdog-log" style="max-height:500px;">Lade...</div></div></div>
        </div>

        <div class="section-header">Plugins <span id="capabilities-count-plugins" style="color:var(--text-4);font-weight:400;margin-left:6px"></span></div>
        <div class="section">
            <div class="card-group" id="capabilities-plugins"><div style="padding:16px;color:var(--text-4)">Lade...</div></div>
        </div>
        <div class="section-header">Skills <span id="capabilities-count-skills" style="color:var(--text-4);font-weight:400;margin-left:6px"></span></div>
        <div class="section">
            <div class="card-group" id="capabilities-skills"><div style="padding:16px;color:var(--text-4)">Lade...</div></div>
        </div>
        <div class="section-header">MCP Server <span id="capabilities-count-mcp" style="color:var(--text-4);font-weight:400;margin-left:6px"></span></div>
        <div class="section">
            <div class="card-group" id="capabilities-mcp"><div style="padding:16px;color:var(--text-4)">Lade...</div></div>
        </div>
        <div class="section-header">Inaktive Skill-Ordner</div>
        <div class="section">
            <div class="card-group" id="capabilities-inactive"><div style="padding:16px;color:var(--text-4)">Lade...</div></div>
        </div>

        <div class="section-header">Tool Failures (letzte 24h) <span id="tf-total" style="color:var(--text-4);font-weight:400;margin-left:6px"></span></div>
        <div class="section">
            <div class="card-group" id="tool-failures"><div style="padding:16px;color:var(--text-4)">Lade...</div></div>
        </div>

        <div class="section-header">Cost Overview</div>
        <div class="section">
            <div class="card-group" id="cost-overview"><div style="padding:16px;color:var(--text-4)">Lade...</div></div>
        </div>
    </div>

    <!-- ====== WORK (Kalender + Tasks) ====== -->
    <div id="view-work" style="display:none;">
        <div class="section-header" style="margin-top:12px" id="cal-header">Termine heute</div>
        <div class="section"><div class="card-group" id="calendar-events"><div style="padding:16px;color:var(--text-4)">Lade...</div></div></div>

        <div id="linear-overdue-section" style="display:none;">
            <div class="section-header" style="color:var(--red)">&#9888; Überfällig</div>
            <div class="section"><div class="card-group" id="linear-overdue"></div></div>
        </div>
        <div class="section-header">&#128308; In Arbeit</div>
        <div class="section"><div class="card-group" id="linear-progress"><div style="padding:16px;color:var(--text-4)">Lade...</div></div></div>
        <div class="section-header">&#128993; Offen</div>
        <div class="section"><div class="card-group" id="linear-todo"></div></div>
        <div id="linear-done-section" style="display:none;">
            <div class="section-header" style="color:var(--green)">&#10003; Zuletzt erledigt</div>
            <div class="section"><div class="card-group" id="linear-done"></div></div>
        </div>
    </div>

    <!-- ====== SOCIAL ====== -->
    <div id="view-social" style="display:none;">
        <div class="section-header" style="margin-top:12px">&#9989; Zuletzt gepostet</div>
        <div class="section"><div class="card-group" id="social-posted"><div style="padding:16px;color:var(--text-4)">Lade...</div></div></div>
        <div class="section-header">&#9201; Nächste Posts</div>
        <div class="section"><div class="card-group" id="social-upcoming"></div></div>
    </div>

    <!-- ====== BRAIN BROWSER ====== -->
    <div id="view-brain" style="display:none;">
        <div class="brain-mobile-toggle">
            <button onclick="toggleBrainSidebar()">&#9776; Dateien</button>
            <span style="font-size:11px;color:var(--text-4);margin-left:10px;flex:1" id="brain-current-path">aria-brain</span>
            <button onclick="toggleBrainGraph()" id="brain-graph-toggle" style="margin-left:auto">Graph</button>
        </div>
        <div class="brain-layout">
            <aside class="brain-sidebar" id="brain-sidebar">
                <div class="brain-sidebar-header" style="padding:6px 6px 8px 6px;position:sticky;top:0;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);z-index:2;background:var(--bg-card-solid);border-bottom:1px solid var(--separator)">
                    <input id="brain-search" type="text" placeholder="Suchen oder filtern..."
                        style="width:100%;padding:7px 10px;background:rgba(255,255,255,0.05);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;outline:none"
                        oninput="onBrainSearchInput()">
                    <div id="brain-search-mode" style="font-size:10px;color:var(--text-4);margin-top:4px;display:flex;gap:8px;align-items:center">
                        <span id="brain-stats">- Files</span>
                        <span style="margin-left:auto;display:flex;gap:6px">
                            <button onclick="brainCollapseAll()" title="Alle zuklappen" style="background:rgba(255,255,255,0.04);border:1px solid var(--border);color:var(--text-3);padding:2px 7px;border-radius:4px;font-size:10px;cursor:pointer;font-family:inherit">▸ Close all</button>
                            <button onclick="brainExpandAll()" title="Alle aufklappen" style="background:rgba(255,255,255,0.04);border:1px solid var(--border);color:var(--text-3);padding:2px 7px;border-radius:4px;font-size:10px;cursor:pointer;font-family:inherit">▾ Open all</button>
                        </span>
                    </div>
                </div>
                <div id="brain-tree-root" style="padding:4px 4px;color:var(--text-4);font-size:12px">Lade...</div>
                <div id="brain-search-results" style="padding:4px 4px;display:none"></div>
            </aside>
            <main class="brain-reader" id="brain-reader">
                <div class="brain-reader-empty">Wähle links eine Datei oder suche.</div>
            </main>
            <main class="brain-reader" id="brain-graph-view" style="display:none;padding:0;position:relative">
                <div id="brain-graph-status" style="position:absolute;top:10px;left:14px;font-size:11px;color:var(--text-4);font-family:'JetBrains Mono',monospace;z-index:5">Lade Graph...</div>
                <svg id="brain-graph-svg" style="width:100%;height:100%;display:block;cursor:grab;"></svg>
            </main>
        </div>
    </div>

    <!-- Aria Inbox -->
    <div class="aria-inbox">
        <div class="effort-row" id="effortRow">
            <span class="effort-label">Effort</span>
            <button class="effort-chip" data-level="low" onclick="setEffort('low', this)">Low</button>
            <button class="effort-chip" data-level="medium" onclick="setEffort('medium', this)">Med</button>
            <button class="effort-chip" data-level="high" onclick="setEffort('high', this)">High</button>
            <button class="effort-chip" data-level="xhigh" onclick="setEffort('xhigh', this)">Extra</button>
            <button class="effort-chip" data-level="max" onclick="setEffort('max', this)">Max</button>
        </div>
        <div class="inbox-row">
            <textarea class="inbox-input" id="inboxInput" placeholder="Schreib Aria…" rows="1"
                oninput="resizeInbox(this)" onkeydown="handleInboxKey(event)"></textarea>
            <button class="inbox-send" id="inboxSend" onclick="sendToAria()">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
            </button>
        </div>
        <div class="inbox-feedback" id="inboxFeedback"></div>
    </div>

    <!-- Tab Bar — 4 tabs (Social hidden bis aktiv genutzt; Planer als Header-Icon) -->
    <nav class="tab-bar">
        <button class="tab-item active" onclick="switchTab('home')" id="tab-home">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4l3 3"/></svg>
            <span>Status</span>
        </button>
        <button class="tab-item" onclick="switchTab('work')" id="tab-work">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>
            <span>Arbeit</span>
        </button>
        <button class="tab-item" onclick="switchTab('brain')" id="tab-brain">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z"/><path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z"/></svg>
            <span>Brain</span>
        </button>
        <button class="tab-item" onclick="switchTab('infra')" id="tab-infra">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
            <span>Infra</span>
        </button>
        <!-- ADDITIVE: Team tab (KAR-679) — navigates to separate TEAM_PAGE -->
        <button class="tab-item" onclick="window.location='/team?t='+TOKEN" id="tab-team">
            <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="7" r="3"/><path d="M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/><path d="M21 21v-2a4 4 0 0 0-3-3.87"/></svg>
            <span>Team</span>
        </button>
        <!-- Social-Tab versteckt (Memory: nicht aktiv genutzt, später aktivieren) -->
        <button class="tab-item" onclick="switchTab('social')" id="tab-social" style="display:none">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>
            <span>Social</span>
        </button>
        <button class="tab-item" onclick="openPlaner()" id="tab-planer" style="display:none">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="3" y1="10" x2="21" y2="10"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="15" x2="8" y2="18"/><line x1="12" y1="15" x2="12" y2="18"/></svg>
            <span>Planer</span>
        </button>
    </nav>
</div>

<script>
const TOKEN = new URLSearchParams(window.location.search).get('t') || '';
const TABS = ['home','infra','work','social','brain'];

function switchTab(tab) {
    TABS.forEach(id => {
        const el = document.getElementById('view-' + id);
        if (el) el.style.display = id === tab ? 'block' : 'none';
    });
    document.querySelectorAll('.tab-item').forEach(b => b.classList.remove('active'));
    const btn = document.getElementById('tab-' + tab);
    if (btn) btn.classList.add('active');
    window.scrollTo(0, 0);
    if (tab === 'brain' && !window._brainTreeLoaded) {
        window._brainTreeLoaded = true;
        loadBrainTree();
    }
}

function alog(msg) {
    const el = document.getElementById('action-log');
    el.style.display = 'block';
    el.textContent += new Date().toLocaleTimeString('de') + ' ' + msg + '\\n';
    el.scrollTop = el.scrollHeight;
}

function barColor(pct) {
    if (pct > 85) return 'bar-red';
    if (pct > 60) return 'bar-yellow';
    return 'bar-green';
}

async function api(path) {
    const r = await fetch(path + (path.includes('?') ? '&' : '?') + 't=' + TOKEN);
    return r.json();
}

async function loadStatus() {
    try {
        const d = await api('/status');
        const hs = document.getElementById('header-status');
        const up = document.getElementById('uptime');
        if (d.status === 'online') {
            hs.innerHTML = '<span class="status-dot"></span> Aria Online';
        } else {
            hs.innerHTML = '<span class="status-dot status-dot--off"></span> Aria Offline';
        }
        up.textContent = d.uptime || '';
    } catch(e) {
        document.getElementById('header-status').innerHTML = '<span class="status-dot status-dot--off"></span> Verbindung fehlgeschlagen';
    }
}

async function loadSystem() {
    try {
        const d = await api('/system');
        document.getElementById('cpu').textContent = d.cpu_pct + '%';
        document.getElementById('cpu-bar').style.width = d.cpu_pct + '%';
        document.getElementById('cpu-bar').className = 'stat-bar-fill ' + barColor(d.cpu_pct);

        document.getElementById('ram').textContent = d.ram_used + ' / ' + d.ram_total;
        document.getElementById('ram-bar').style.width = d.ram_pct + '%';
        document.getElementById('ram-bar').className = 'stat-bar-fill ' + barColor(d.ram_pct);

        document.getElementById('disk').textContent = d.disk_used + ' / ' + d.disk_total;
        document.getElementById('disk-bar').style.width = d.disk_pct + '%';
        document.getElementById('disk-bar').className = 'stat-bar-fill ' + barColor(d.disk_pct);

        document.getElementById('load').textContent = d.load;

        // Summary cards retired with the home-tab refactor (10.05.2026); skip if absent.
        const sysVal = document.getElementById('sum-sys-val');
        const sysSub = document.getElementById('sum-sys-sub');
        if (sysVal && sysSub) {
            const sysColor = d.cpu_pct > 85 ? 'var(--red)' : d.cpu_pct > 60 ? 'var(--amber)' : 'var(--green)';
            sysVal.textContent = d.cpu_pct + '%';
            sysVal.style.color = sysColor;
            sysSub.textContent = 'CPU · RAM ' + d.ram_used;
        }
    } catch(e) {}
}

async function loadServices() {
    try {
        const d = await api('/services');
        const el = document.getElementById('services');
        el.innerHTML = d.map(s =>
            '<div class="card-row">' +
            '<div class="card-row-left"><span class="dot ' + (s.active ? 'dot-g' : 'dot-r') + '"></span><span class="card-row-label">' + s.name + '</span></div>' +
            '<span class="badge ' + (s.active ? 'badge-g' : 'badge-r') + '">' +
            (s.info || (s.active ? 'aktiv' : 'inaktiv')) + '</span></div>'
        ).join('');
    } catch(e) {}
}

async function loadLogs() {
    try {
        const d = await api('/logs');
        document.getElementById('watchdog-log').textContent = d.watchdog || 'Keine Eintraege';
    } catch(e) {}
}

async function loadUsage() {
    try {
        const d = await api('/usage');
        document.getElementById('usage-duration').textContent = d.duration || '-';
        document.getElementById('usage-procs').textContent = d.processes || '-';
        document.getElementById('usage-cpu').textContent = d.claude_cpu || '-';
        document.getElementById('usage-ram').textContent = d.claude_ram || '-';
        document.getElementById('usage-today').textContent = d.today_tokens || '-';
        document.getElementById('usage-today-sessions').textContent = d.today_sessions || '-';
        document.getElementById('usage-week').textContent = d.week_tokens || '-';
        // Usage bars (approximate limits: 900M/day, 6300M/week)
        if (d.today_tokens && d.today_tokens !== '-') {
            const todayM = parseFloat(d.today_tokens);
            const todayPct = Math.min(100, Math.round(todayM / 900 * 100));
            const bar = document.getElementById('usage-today-bar');
            if (bar) { bar.style.width = todayPct + '%'; bar.className = 'stat-bar-fill ' + barColor(todayPct); }
        }
        if (d.week_tokens && d.week_tokens !== '-') {
            const weekM = parseFloat(d.week_tokens);
            const weekPct = Math.min(100, Math.round(weekM / 6300 * 100));
            const bar = document.getElementById('usage-week-bar');
            if (bar) { bar.style.width = weekPct + '%'; bar.className = 'stat-bar-fill ' + barColor(weekPct); }
        }
        document.getElementById('usage-week-sessions').textContent = d.week_sessions || '-';
        document.getElementById('usage-current').textContent = d.current_tokens || '-';
        document.getElementById('usage-total').textContent = d.total_tokens || '-';
    } catch(e) {}
}

function renderSession(data, tasksId, logId, pulseId, cardId) {
    if (cardId) {
        const card = document.getElementById(cardId);
        if (!data || !data.session || data.session === 'Keine aktive Session') {
            card.style.display = 'none'; return;
        }
        card.style.display = 'block';
    }
    const tasksEl = document.getElementById(tasksId);
    if (data.tasks && data.tasks.length > 0) {
        tasksEl.innerHTML = data.tasks.map(t => {
            let cls = 'task-pending', icon = '&#9723;';
            if (t.status === 'done') { cls = 'task-done'; icon = '&#10004;'; }
            else if (t.status === 'active') { cls = 'task-active'; icon = '&#9654;'; }
            return '<div class="task-item ' + cls + '"><span class="task-icon">' + icon + '</span>' + t.text + '</div>';
        }).join('');
    } else { tasksEl.innerHTML = ''; }
    document.getElementById(logId).textContent = data.session || 'Keine aktive Session';
    const pulse = document.getElementById(pulseId);
    pulse.style.color = data.thinking ? '#93c5fd' : '#4ade80';

    // Note: prior live-banner (current-task / current-task-icon) was retired in the
    // home-tab refactor. Hero-Session-Card now carries the active state via loadHero().
}

async function loadActivity() {
    try {
        const d = await api('/activity');
        renderSession(d, 'aria-tasks', 'session-log', 'activity-pulse', null);
    } catch(e) {}
}

function fmtTokens(n) {
    if (!n) return '0';
    if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
    if (n >= 1000) return (n / 1000).toFixed(0) + 'k';
    return String(n);
}

function setHeaderStatus(state, label) {
    const hs = document.getElementById('header-status');
    if (!hs) return;
    while (hs.firstChild) hs.removeChild(hs.firstChild);
    const dot = document.createElement('span');
    dot.className = 'status-dot' + (state === 'online' ? '' : (state === 'idle' ? ' status-dot--check' : ' status-dot--off'));
    hs.appendChild(dot);
    hs.appendChild(document.createTextNode(' ' + (label || 'Aria')));
}

function buildBar(pct) {
    // 10-cell unicode bar matching the Claude Code /statusline.
    const filled = Math.max(0, Math.min(10, Math.round((pct || 0) / 10)));
    return '▰'.repeat(filled) + '▱'.repeat(10 - filled);
}

function basenameOf(p) {
    if (!p) return '—';
    const parts = p.replace(/\/+$/, '').split('/');
    return parts[parts.length - 1] || p;
}

async function loadHero() {
    let h;
    try { h = await api('/session-hero'); } catch(e) { return; }

    // State dot
    const dot = document.getElementById('sl-dot');
    if (dot) dot.className = 'sl-dot ' + (h.state || 'offline');

    setHeaderStatus(h.state, h.state_label);

    // Folder · branch · model
    const folderEl = document.getElementById('sl-folder');
    if (folderEl) folderEl.textContent = basenameOf(h.cwd);

    const branchEl = document.getElementById('sl-branch');
    if (branchEl) branchEl.textContent = h.branch || '—';

    const modelEl = document.getElementById('sl-model');
    if (modelEl) modelEl.textContent = h.model_short || h.model_full || '—';

    // Context bar
    const pct = (h.context && h.context.percent) || 0;
    const tokens = (h.context && h.context.tokens) || 0;
    const max = (h.context && h.context.max) || 0;
    const barEl = document.getElementById('sl-bar');
    if (barEl) {
        barEl.textContent = buildBar(pct);
        barEl.className = 'sl-bar' + (pct >= 90 ? ' crit' : (pct >= 70 ? ' warn' : ''));
    }
    const pctEl = document.getElementById('sl-pct');
    if (pctEl) pctEl.textContent = pct.toFixed(0) + '%';
    const tokEl = document.getElementById('sl-tokens');
    if (tokEl) tokEl.textContent = fmtTokens(tokens) + ' / ' + fmtTokens(max);
    const updEl = document.getElementById('sl-updated');
    if (updEl) updEl.textContent = (h.updated_ago && h.updated_ago !== '-') ? h.updated_ago : '—';

    // Meta lines
    const cwdEl = document.getElementById('sl-cwd');
    if (cwdEl) cwdEl.textContent = h.cwd || '—';
    const branchStateEl = document.getElementById('sl-branch-state');
    if (branchStateEl) branchStateEl.textContent = h.branch_state ? '(' + h.branch_state + ')' : '';

    const lastEl = document.getElementById('sl-last');
    if (lastEl) {
        if (h.last_action && h.last_action.text) {
            lastEl.textContent = h.last_action.ago + ' · ' + h.last_action.text;
        } else {
            lastEl.textContent = '—';
        }
    }

    // Today + uptime
    const tc = document.getElementById('today-commits');
    const tt = document.getElementById('today-tasks');
    const tg = document.getElementById('today-telegrams');
    const tu = document.getElementById('sl-uptime');
    if (tc) tc.textContent = (h.today && h.today.commits != null) ? h.today.commits : '0';
    if (tt) tt.textContent = (h.today && h.today.tasks_done) ? h.today.tasks_done : '—';
    if (tg) tg.textContent = (h.today && h.today.telegrams != null) ? h.today.telegrams : '—';
    if (tu) tu.textContent = h.uptime || '—';
}

async function loadLinear() {
    try {
        const d = await api('/linear');
        const renderItems = (items, showActions) => items.length > 0 ? items.map(i => {
            const title = i.title.replace(/^ARI-\d+:\s*/, '');
            const actions = showActions && i.internal_id
                ? '<div class="issue-actions">' +
                  '<button class="issue-btn start" data-lid="' + i.internal_id + '" data-did="' + i.id + '" onclick="handleLinearStart(this)">&#9654; Start</button>' +
                  '<button class="issue-btn done" data-lid="' + i.internal_id + '" data-did="' + i.id + '" onclick="handleLinearDone(this)">&#10003;</button>' +
                  '</div>'
                : '';
            return '<div class="issue-item">' +
            '<div class="issue-row">' +
            '<div class="issue-content">' +
            '<div class="issue-id">' + i.id + '</div>' +
            '<div class="issue-title">' + title + '</div>' +
            (i.extra ? '<div class="issue-sub">&#9888; ' + i.extra + '</div>' : '') +
            '</div>' + actions +
            '</div></div>';
        }).join('') : '<div style="padding:10px 14px;color:var(--text-4);font-size:12px;">Keine Einträge</div>';

        document.getElementById('linear-progress').innerHTML = renderItems(d.in_progress || [], true);
        document.getElementById('linear-todo').innerHTML = renderItems(d.todo || [], true);

        const overdueSection = document.getElementById('linear-overdue-section');
        if (d.overdue && d.overdue.length > 0) {
            overdueSection.style.display = 'block';
            document.getElementById('linear-overdue').innerHTML = renderItems(d.overdue);
        } else {
            overdueSection.style.display = 'none';
        }

        const doneSection = document.getElementById('linear-done-section');
        if (d.done && d.done.length > 0) {
            doneSection.style.display = 'block';
            document.getElementById('linear-done').innerHTML = renderItems(d.done);
        } else {
            doneSection.style.display = 'none';
        }

        // Summary cards retired with the home-tab refactor (10.05.2026); skip if absent.
        const tCount = document.getElementById('sum-tasks-count');
        const tSub = document.getElementById('sum-tasks-sub');
        if (tCount && tSub) {
            const prog = (d.in_progress || []).length;
            const ovd = (d.overdue || []).length;
            tCount.textContent = prog + (ovd > 0 ? ' / ' + ovd + ' !' : '');
            tCount.style.color = ovd > 0 ? 'var(--red)' : 'var(--text)';
            tSub.textContent = prog + ' aktiv' + (ovd > 0 ? ' · ' + ovd + ' überfällig' : '');
        }
    } catch(e) {
        document.getElementById('linear-progress').innerHTML = '<div style="padding:16px;color:var(--red)">Ladefehler</div>';
    }
}

function handleLinearDone(btn) {
    completeLinear(btn.dataset.lid, btn.dataset.did, btn);
}

async function handleLinearStart(btn) {
    btn.disabled = true;
    btn.textContent = '...';
    try {
        const r = await fetch('/api/linear/start', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({token: TOKEN, internal_id: btn.dataset.lid, display_id: btn.dataset.did})
        });
        const d = await r.json();
        if (d.ok) {
            btn.textContent = '▶ Gestartet';
            btn.style.borderColor = 'var(--cyan)';
            setTimeout(() => loadLinear(), 800);
        } else {
            btn.textContent = '!';
            btn.style.color = 'var(--red)';
            setTimeout(() => { btn.disabled = false; btn.textContent = '&#9654; Start'; btn.style.color = ''; }, 2000);
        }
    } catch(e) { btn.disabled = false; btn.textContent = '&#9654; Start'; }
}

async function completeLinear(internalId, displayId, btn) {
    btn.disabled = true;
    btn.textContent = '...';
    try {
        const r = await fetch('/api/linear/complete', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({token: TOKEN, internal_id: internalId, display_id: displayId})
        });
        const d = await r.json();
        if (d.ok) {
            btn.textContent = '✓';
            btn.classList.add('done');
            const item = btn.closest('.issue-item');
            item.style.opacity = '0.4';
            item.style.transition = 'opacity 0.4s';
            setTimeout(() => { item.remove(); loadLinear(); }, 800);
        } else {
            btn.textContent = '!';
            btn.style.borderColor = 'var(--red)';
            btn.style.color = 'var(--red)';
            setTimeout(() => { btn.disabled = false; btn.textContent = '✓'; btn.style.borderColor = ''; btn.style.color = ''; }, 2000);
        }
    } catch(e) {
        btn.disabled = false; btn.textContent = '✓';
    }
}

async function loadCalendar() {
    try {
        const d = await api('/calendar');
        if (d.date) document.getElementById('cal-header').textContent = 'Termine ' + d.date;
        const el = document.getElementById('calendar-events');
        if (d.error) {
            el.innerHTML = '<div style="padding:16px;color:var(--red)">Fehler: ' + d.error.substring(0, 80) + '</div>';
            return;
        }
        if (d.events && d.events.length > 0) {
            el.innerHTML = d.events.map(e =>
                '<div class="issue-item"><div class="issue-row">' +
                '<div class="issue-content">' +
                '<div class="issue-title">' + e.title + '</div>' +
                (e.location ? '<div class="issue-sub" style="color:var(--text-4)">&#128205; ' + e.location.substring(0,40) + '</div>' : '') +
                '</div><span style="font-size:11px;font-weight:700;color:var(--cyan);white-space:nowrap;padding-left:8px">' + e.time + '</span>' +
                '</div></div>'
            ).join('');

            // Summary card retired with the home-tab refactor (10.05.2026); skip if absent.
            const calTime = document.getElementById('sum-cal-time');
            const calTitle = document.getElementById('sum-cal-title');
            if (calTime && calTitle) {
                const next = d.events[0];
                calTime.textContent = next.time || '??:??';
                calTitle.textContent = next.title || '-';
            }
        } else {
            el.innerHTML = '<div style="padding:10px 14px;color:var(--text-4);font-size:12px;">Keine Termine heute</div>';
            const calTime = document.getElementById('sum-cal-time');
            const calTitle = document.getElementById('sum-cal-title');
            if (calTime) calTime.textContent = '--';
            if (calTitle) calTitle.textContent = 'Keine Termine';
        }
    } catch(e) {
        document.getElementById('calendar-events').innerHTML = '<div style="padding:16px;color:var(--red)">Ladefehler</div>';
    }
}

async function loadN8n() {
    try {
        const d = await api('/n8n');
        const render = (items) => items.length > 0 ? items.map(w =>
            '<div class="issue-item"><div class="issue-row"><div class="issue-content">' +
            '<div class="issue-title" style="display:flex;align-items:center;gap:6px">' +
            '<span style="width:7px;height:7px;border-radius:50%;background:' + (w.active ? 'var(--accent)' : 'var(--red)') + ';flex-shrink:0;display:inline-block"></span>' +
            w.name + '</div>' +
            (w.last_run ? '<div class="issue-id" style="margin-top:1px">' + w.last_run + '</div>' : '') +
            '</div></div></div>'
        ).join('') : '<div style="padding:10px 14px;color:var(--text-4);font-size:12px;">Keine</div>';

        const active = (d.workflows || []).filter(w => w.active);
        const inactive = (d.workflows || []).filter(w => !w.active);
        document.getElementById('n8n-active').innerHTML = render(active);
        document.getElementById('n8n-inactive').innerHTML = render(inactive);

        // Summary cards retired with the home-tab refactor (10.05.2026); skip if absent.
        const n8nVal = document.getElementById('sum-n8n-val');
        const n8nSub = document.getElementById('sum-n8n-sub');
        if (n8nVal) n8nVal.textContent = active.length;
        if (n8nSub) n8nSub.textContent = active.length + ' aktiv · ' + inactive.length + ' inaktiv';
    } catch(e) {
        document.getElementById('n8n-active').innerHTML = '<div style="padding:16px;color:var(--red)">Ladefehler</div>';
    }
}

async function loadSocial() {
    try {
        const d = await api('/social');
        const posted = document.getElementById('social-posted');
        const upcoming = document.getElementById('social-upcoming');

        const renderPost = (p) =>
            '<div class="issue-item"><div class="issue-row"><div class="issue-content">' +
            '<div class="issue-id">' + (p.platform || '') + (p.date ? ' · ' + p.date : '') + (p.time ? ' ' + p.time : '') + '</div>' +
            '<div class="issue-title">' + (p.content || '').substring(0, 80) + '</div>' +
            '</div></div></div>';

        posted.innerHTML = (d.posted || []).length > 0
            ? (d.posted || []).map(renderPost).join('')
            : '<div style="padding:16px;color:var(--text-4)">Noch keine Posts</div>';
        upcoming.innerHTML = (d.upcoming || []).length > 0
            ? (d.upcoming || []).map(renderPost).join('')
            : '<div style="padding:16px;color:var(--text-4)">Keine geplanten Posts</div>';
    } catch(e) {
        document.getElementById('social-posted').innerHTML = '<div style="padding:16px;color:var(--red)">Ladefehler</div>';
    }
}

function renderNewsList(elId, items, emptyMsg) {
    const el = document.getElementById(elId);
    if (!el) return;
    while (el.firstChild) el.removeChild(el.firstChild);
    if (!items || items.length === 0) {
        const empty = document.createElement('div');
        empty.style.cssText = 'padding:14px 0;color:var(--text-4);font-size:12px';
        empty.textContent = emptyMsg || '—';
        el.appendChild(empty);
        return;
    }
    for (const n of items) {
        const item = document.createElement('div');
        item.className = 'news-item';
        const meta = document.createElement('div');
        meta.className = 'news-item-meta';
        const src = document.createElement('span');
        src.className = 'news-source';
        src.textContent = n.source || '';
        meta.appendChild(src);
        if (n.date) meta.appendChild(document.createTextNode(' · ' + n.date));
        item.appendChild(meta);
        const title = document.createElement('div');
        title.className = 'news-item-title';
        title.textContent = n.title || '';
        item.appendChild(title);
        if (n.summary) {
            const sum = document.createElement('div');
            sum.className = 'news-item-summary';
            sum.textContent = n.summary;
            item.appendChild(sum);
        }
        el.appendChild(item);
    }
}

async function loadNews() {
    try {
        const d = await api('/news');
        renderNewsList('home-news-items', (d.items || []).slice(0, 5), 'Keine externen Headlines geladen.');
    } catch(e) {}
}

async function loadErkenntnisse() {
    try {
        const d = await api('/erkenntnisse');
        renderNewsList('home-erkenntnisse', (d.items || []).slice(0, 5), 'Noch keine Erkenntnisse.');
    } catch(e) {}
}

function escHtml(s) {
    return String(s || '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}

function badgeHtml(active, labelOn, labelOff) {
    const on = !!active;
    const color = on ? 'var(--green)' : 'rgba(235,235,245,0.3)';
    const bg = on ? 'rgba(48,209,88,0.15)' : 'rgba(120,120,128,0.16)';
    const text = on ? (labelOn || 'aktiv') : (labelOff || 'inaktiv');
    return '<span style="font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;padding:3px 8px;border-radius:999px;color:' + color + ';background:' + bg + '">' + text + '</span>';
}

async function loadCapabilities() {
    try {
        const d = await api('/capabilities');
        const plugins = d.plugins || [];
        const globalSkills = d.global_skills || [];
        const mcps = d.mcp_servers || [];
        const inactive = d.inactive_skill_repos || [];

        // === PLUGINS ===
        const plEl = document.getElementById('capabilities-plugins');
        document.getElementById('capabilities-count-plugins').textContent = plugins.length ? '· ' + plugins.length : '';
        if (plugins.length === 0) {
            plEl.innerHTML = '<div style="padding:16px;color:var(--text-4)">Keine Plugins installiert.</div>';
        } else {
            plEl.innerHTML = plugins.map(p =>
                '<div class="card-row"><div class="card-row-left" style="flex-direction:column;align-items:flex-start;gap:1px;min-width:0;flex:1">' +
                '<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap"><span style="font-weight:600;color:var(--text);font-size:12px">' + escHtml(p.name) + '</span>' + badgeHtml(true, 'aktiv') + '</div>' +
                '<div style="font-size:10px;color:var(--text-4)">' + escHtml(p.marketplace) + ' · v' + escHtml(p.version) + (p.last_updated ? ' · ' + escHtml(p.last_updated) : '') + ' · ' + p.skill_count + ' Skills</div>' +
                '</div></div>'
            ).join('');
        }

        // === SKILLS (flach, alle aktiv geladenen) ===
        const allSkills = [];
        plugins.forEach(p => (p.skills || []).forEach(s => allSkills.push({
            name: s.name, description: s.description, source: p.name + ' (plugin)', active: true
        })));
        globalSkills.forEach(s => allSkills.push({
            name: s.name, description: s.description, source: 'user (~/.claude/skills)', active: true
        }));
        // inaktive Skill-Repos mit reinziehen damit Kais den Gesamt-Überblick hat
        inactive.forEach(r => (r.skills || []).forEach(name => allSkills.push({
            name: name, description: '', source: r.repo + ' (' + r.path + ')', active: false
        })));
        allSkills.sort((a, b) => {
            if (a.active !== b.active) return a.active ? -1 : 1;
            return a.name.localeCompare(b.name);
        });
        const skEl = document.getElementById('capabilities-skills');
        document.getElementById('capabilities-count-skills').textContent = allSkills.length ? '· ' + allSkills.filter(s => s.active).length + ' aktiv / ' + allSkills.length + ' total' : '';
        if (allSkills.length === 0) {
            skEl.innerHTML = '<div style="padding:16px;color:var(--text-4)">Keine Skills gefunden.</div>';
        } else {
            skEl.innerHTML = allSkills.map(s => {
                const dim = s.active ? '' : 'opacity:0.45;';
                return '<div class="card-row" style="' + dim + '"><div class="card-row-left" style="flex-direction:column;align-items:flex-start;gap:1px;flex:1;min-width:0">' +
                    '<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap"><span style="font-weight:600;color:var(--text);font-size:12px">' + escHtml(s.name) + '</span>' + badgeHtml(s.active, 'aktiv', 'inaktiv') + '<span style="font-size:9px;color:var(--text-4)">' + escHtml(s.source) + '</span></div>' +
                    (s.description ? '<div style="font-size:10px;color:var(--text-3);line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden">' + escHtml(s.description) + '</div>' : '') +
                    '</div></div>';
            }).join('');
        }

        // === MCP SERVER ===
        const mcpEl = document.getElementById('capabilities-mcp');
        document.getElementById('capabilities-count-mcp').textContent = mcps.length ? '· ' + mcps.filter(m => m.active).length + ' aktiv / ' + mcps.length + ' total' : '';
        if (mcps.length === 0) {
            mcpEl.innerHTML = '<div style="padding:16px;color:var(--text-4)">Keine MCP-Server konfiguriert.</div>';
        } else {
            mcpEl.innerHTML = mcps.map(m => {
                const dim = m.active ? '' : 'opacity:0.45;';
                const detail = m.url || m.command || m.source;
                return '<div class="card-row" style="' + dim + '"><div class="card-row-left" style="flex-direction:column;align-items:flex-start;gap:1px;flex:1;min-width:0">' +
                    '<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap"><span style="font-weight:600;color:var(--text);font-size:12px">' + escHtml(m.name) + '</span>' + badgeHtml(m.active, 'online', 'offline') + '</div>' +
                    '<div style="font-size:10px;color:var(--text-4);word-break:break-all">' + escHtml(m.type || '') + (detail ? ' · ' + escHtml(detail) : '') + '</div>' +
                    '</div></div>';
            }).join('');
        }

        // === INAKTIVE SKILL-ORDNER ===
        const inaEl = document.getElementById('capabilities-inactive');
        if (inactive.length === 0) {
            inaEl.innerHTML = '<div style="padding:16px;color:var(--text-4)">Keine.</div>';
        } else {
            inaEl.innerHTML = inactive.map(r =>
                '<details style="border-top:1px solid rgba(255,255,255,0.06)"><summary style="padding:12px 16px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;opacity:0.7">' +
                '<div><div style="font-weight:600">' + escHtml(r.repo) + '</div>' +
                '<div style="font-size:11px;color:var(--text-4);margin-top:2px">' + escHtml(r.path) + '</div></div>' +
                '<span style="font-size:11px;color:var(--text-3)">' + r.skill_count + ' Skills</span>' +
                '</summary>' +
                (r.skills && r.skills.length
                    ? '<div style="padding:8px 16px;font-size:12px;color:var(--text-3);line-height:1.7">' + r.skills.map(escHtml).join(', ') + '</div>'
                    : '') +
                '</details>'
            ).join('');
        }
    } catch(e) {
        document.getElementById('capabilities-plugins').innerHTML = '<div style="padding:16px;color:var(--red)">Fehler: ' + escHtml(e.message || e) + '</div>';
    }
}

// Brain modal removed 2026-05-04 — replaced by Brain tab

async function loadToolFailures() {
    try {
        const r = await fetch('/tool-failures?t=' + TOKEN + '&limit=20');
        const d = await r.json();
        const totalEl = document.getElementById('tf-total');
        const wrap = document.getElementById('tool-failures');
        if (!d.ok) {
            if (totalEl) totalEl.textContent = '';
            wrap.innerHTML = '<div style="padding:16px;color:var(--red)">Fehler: ' + escHtml(d.error || 'unknown') + '</div>';
            return;
        }
        const total = d.total_24h || 0;
        if (totalEl) totalEl.textContent = total === 0 ? '(0)' : '(' + total + ')';
        if (total === 0 && (!d.recent || d.recent.length === 0)) {
            wrap.innerHTML = '<div style="padding:16px;color:var(--text-4)">Keine Tool-Failures in den letzten 24h. Sauber.</div>';
            return;
        }
        let html = '';
        if (d.by_tool && d.by_tool.length) {
            html += '<div style="padding:12px 16px;border-bottom:1px solid rgba(255,255,255,0.06);font-size:11px;color:var(--text-3);text-transform:uppercase;letter-spacing:0.05em">Top Tools</div>';
            d.by_tool.slice(0, 8).forEach(function(t) {
                html += '<div class="card-row"><div class="card-row-left"><span class="card-row-label">' + escHtml(t.tool_name) + '</span></div>';
                html += '<div class="card-row-right" style="color:var(--text-3)">' + t.count + 'x</div></div>';
            });
        }
        if (d.recent && d.recent.length) {
            html += '<div style="padding:12px 16px;border-top:1px solid rgba(255,255,255,0.06);border-bottom:1px solid rgba(255,255,255,0.06);font-size:11px;color:var(--text-3);text-transform:uppercase;letter-spacing:0.05em">Letzte Failures</div>';
            d.recent.slice(0, 10).forEach(function(r) {
                const tn = r.tool_name || '?';
                const tt = (r.created_at || '').slice(0, 16).replace('T', ' ');
                const ex = (r.error_excerpt || '').slice(0, 140);
                html += '<div class="card-row" style="flex-direction:column;align-items:flex-start;gap:2px">';
                html += '<div style="display:flex;justify-content:space-between;width:100%"><span class="card-row-label">' + escHtml(tn) + '</span><span style="color:var(--text-4);font-size:10px">' + escHtml(tt) + '</span></div>';
                html += '<div style="font-size:11px;color:var(--text-3);font-family:monospace;width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml(ex) + '</div>';
                html += '</div>';
            });
        }
        wrap.innerHTML = html;
    } catch(e) {
        document.getElementById('tool-failures').innerHTML = '<div style="padding:16px;color:var(--red)">Fehler: ' + escHtml(e.message || e) + '</div>';
    }
}

async function loadCostOverview() {
    try {
        const r = await fetch('/cost-overview?t=' + TOKEN);
        const d = await r.json();
        const wrap = document.getElementById('cost-overview');
        const fmt = function(v) {
            if (v === null || v === undefined) return '-';
            if (typeof v === 'number') return v.toFixed(2) + ' EUR';
            return String(v);
        };
        const s = d.stripe || {};
        const e = d.estimated_costs_per_month || {};
        let html = '';
        // Stripe section
        html += '<div style="padding:12px 16px;border-bottom:1px solid rgba(255,255,255,0.06);font-size:11px;color:var(--text-3);text-transform:uppercase;letter-spacing:0.05em">Stripe (Live)</div>';
        html += '<div class="card-row"><div class="card-row-left"><span class="card-row-label">Aktive Subscriptions</span></div><div class="card-row-right">' + (s.active_subscriptions !== null ? s.active_subscriptions : '-') + '</div></div>';
        html += '<div class="card-row"><div class="card-row-left"><span class="card-row-label">MRR</span></div><div class="card-row-right" style="color:var(--green)">' + fmt(s.active_mrr_eur) + '</div></div>';
        html += '<div class="card-row"><div class="card-row-left"><span class="card-row-label">Available Balance</span></div><div class="card-row-right">' + fmt(s.available_eur) + '</div></div>';
        html += '<div class="card-row"><div class="card-row-left"><span class="card-row-label">Pending Balance</span></div><div class="card-row-right">' + fmt(s.pending_eur) + '</div></div>';
        html += '<div class="card-row"><div class="card-row-left"><span class="card-row-label">Charges 30d (succeeded)</span></div><div class="card-row-right">' + fmt(s.charges_30d_succeeded_eur) + ' / ' + (s.charges_30d_count || 0) + '</div></div>';
        // Costs section
        html += '<div style="padding:12px 16px;border-top:1px solid rgba(255,255,255,0.06);border-bottom:1px solid rgba(255,255,255,0.06);font-size:11px;color:var(--text-3);text-transform:uppercase;letter-spacing:0.05em">Geschaetzte Kosten / Monat</div>';
        html += '<div class="card-row"><div class="card-row-left"><span class="card-row-label">Subscriptions (Software)</span></div><div class="card-row-right" style="color:var(--text-3)">' + fmt(e.subscriptions_total_eur) + '</div></div>';
        html += '<div class="card-row"><div class="card-row-left"><span class="card-row-label">VPS Hostinger</span></div><div class="card-row-right" style="color:var(--text-3)">' + fmt(e.vps_hostinger_eur) + '</div></div>';
        html += '<div class="card-row"><div class="card-row-left"><span class="card-row-label">Anthropic</span></div><div class="card-row-right" style="color:var(--text-4);font-size:10px">' + escHtml(String(e.anthropic_max_plan_eur || '-')) + '</div></div>';
        if (d.errors && d.errors.length) {
            html += '<div style="padding:8px 16px;border-top:1px solid rgba(255,255,255,0.06);color:var(--orange);font-size:10px">Hinweise: ' + escHtml(d.errors.join(' | ')) + '</div>';
        }
        wrap.innerHTML = html;
    } catch(e) {
        document.getElementById('cost-overview').innerHTML = '<div style="padding:16px;color:var(--red)">Fehler: ' + escHtml(e.message || e) + '</div>';
    }
}

async function loadQuickSkills() {
    try {
        const r = await fetch('/quick-skills?t=' + TOKEN);
        const d = await r.json();
        const wrap = document.getElementById('quick-skills');
        if (!d.ok || !d.skills || d.skills.length === 0) {
            wrap.innerHTML = '<div style="padding:16px;color:var(--text-4);grid-column:1/-1">Keine Skills definiert. Datei: /root/.aria-quick-skills.json</div>';
            return;
        }
        let html = '';
        d.skills.forEach(function(s) {
            html += '<button class="qs-btn" data-skill-id="' + escHtml(s.id) + '" onclick="triggerQuickSkill(this.dataset.skillId, this)" title="' + escHtml(s.description || '') + '">';
            html += '<span style="font-size:18px">' + escHtml(s.icon || '⚡') + '</span>';
            html += '<span class="qs-label">' + escHtml(s.label) + '</span>';
            html += '</button>';
        });
        wrap.innerHTML = html;
    } catch(e) {
        document.getElementById('quick-skills').innerHTML = '<div style="padding:16px;color:var(--red);grid-column:1/-1">Fehler: ' + escHtml(e.message || e) + '</div>';
    }
}

async function triggerQuickSkill(id, btn) {
    if (btn) { btn.disabled = true; btn.style.opacity = '0.5'; }
    alog('Quick Skill ' + id + ' getriggert...');
    try {
        const r = await fetch('/api/quick-skill-trigger', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({token: TOKEN, skill_id: id})
        });
        const d = await r.json();
        alog(d.message || 'unbekannt');
    } catch(e) {
        alog('Fehler: ' + (e.message || e));
    } finally {
        if (btn) { setTimeout(function() { btn.disabled = false; btn.style.opacity = '1'; }, 800); }
    }
}

async function loadAll() {
    await Promise.all([loadHero(), loadStatus(), loadSystem(), loadServices(), loadActivity(), loadUsage(), loadLogs(), loadLinear(), loadCalendar(), loadN8n(), loadSocial(), loadNews(), loadErkenntnisse(), loadCapabilities(), loadToolFailures(), loadCostOverview(), loadQuickSkills(), loadEffort()]);
}

async function restart() {
    const btn = document.getElementById('restart-btn');
    btn.disabled = true; btn.textContent = 'Startet...';
    alog('Restart angefordert...');
    try {
        const r = await fetch('/restart', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({token: TOKEN})
        });
        const d = await r.json();
        alog(d.message);
        if (d.ok) {
            alog('Warte auf Aria...');
            setTimeout(async () => {
                await loadAll();
                btn.disabled = false; btn.textContent = 'Restart';
            }, 8000);
        } else {
            btn.disabled = false; btn.textContent = 'Restart';
        }
    } catch(e) {
        alog('Fehler: ' + e.message);
        btn.disabled = false; btn.textContent = 'Restart';
    }
}

async function gitPull() {
    alog('Git Pull aria-brain...');
    try {
        const r = await fetch('/git-pull', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({token: TOKEN})
        });
        const d = await r.json();
        alog(d.message);
    } catch(e) { alog('Fehler: ' + e.message); }
}

function openPlaner() {
    window.location.href = '/planer?t=' + TOKEN;
}

function toggleTheme() {
    const isLight = document.body.classList.toggle('light');
    document.getElementById('theme-btn').textContent = isLight ? '☀️' : '🌙';
    localStorage.setItem('aria-theme', isLight ? 'light' : 'dark');
}
(function() {
    if (localStorage.getItem('aria-theme') === 'light') {
        document.body.classList.add('light');
        document.getElementById('theme-btn').textContent = '☀️';
    }
})();

loadAll();
setInterval(loadHero, 10000);
setInterval(loadActivity, 15000);
setInterval(loadStatus, 30000);
setInterval(loadSystem, 60000);
setInterval(loadUsage, 60000);
setInterval(loadServices, 60000);
setInterval(loadLinear, 120000);
setInterval(loadCalendar, 120000);
setInterval(loadN8n, 120000);
setInterval(loadSocial, 180000);
setInterval(loadNews, 300000);
setInterval(loadErkenntnisse, 300000);

// ===== ARIA INBOX =====
document.addEventListener('click', function(e) {
    const item = e.target.closest('.issue-tappable');
    if (item && item.dataset.inbox) fillInbox(item.dataset.inbox);
});

function fillInbox(text) {
    const input = document.getElementById('inboxInput');
    input.value = text;
    resizeInbox(input);
    input.focus();
    input.setSelectionRange(text.length, text.length);
    // Flash feedback
    input.style.borderColor = 'rgba(30,240,161,0.5)';
    setTimeout(() => { input.style.borderColor = ''; }, 600);
}

function resizeInbox(el) {
    el.style.height = 'auto';
    el.style.height = Math.min(el.scrollHeight, 100) + 'px';
}

function handleInboxKey(e) {
    if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault();
        sendToAria();
    }
}

async function sendToAria() {
    const input = document.getElementById('inboxInput');
    const btn = document.getElementById('inboxSend');
    const fb = document.getElementById('inboxFeedback');
    const text = input.value.trim();
    if (!text) return;

    input.value = '';
    input.style.height = 'auto';
    input.disabled = true;
    btn.style.opacity = '0.5';
    fb.textContent = 'Sende…';
    fb.className = 'inbox-feedback';

    try {
        const r = await fetch('/api/send-message?t=' + TOKEN, {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({text, token: TOKEN})
        });
        const d = await r.json();
        if (d.ok) {
            fb.textContent = '✓ Gesendet an Aria';
            fb.className = 'inbox-feedback ok';
        } else {
            fb.textContent = '✗ ' + (d.message || 'Fehler');
            fb.className = 'inbox-feedback err';
            input.value = text;
        }
    } catch(e) {
        fb.textContent = '✗ Verbindungsfehler';
        fb.className = 'inbox-feedback err';
        input.value = text;
    }

    input.disabled = false;
    btn.style.opacity = '1';
    input.focus();
    setTimeout(() => { fb.textContent = ''; fb.className = 'inbox-feedback'; }, 3000);
}

// ====== EFFORT QUICK-SET ======
function markEffortChip(level) {
    document.querySelectorAll('#effortRow .effort-chip').forEach(function(c) {
        c.classList.toggle('active', c.dataset.level === level);
    });
}

async function setEffort(level, btn) {
    const fb = document.getElementById('inboxFeedback');
    fb.textContent = 'Setze Effort ' + level + '…';
    fb.className = 'inbox-feedback';
    try {
        const r = await fetch('/api/effort?t=' + TOKEN, {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({level: level, token: TOKEN})
        });
        const d = await r.json();
        if (d.ok) {
            fb.textContent = '✓ /effort ' + level + ' an Session gesendet';
            fb.className = 'inbox-feedback ok';
            markEffortChip(level);
        } else {
            fb.textContent = '✗ ' + (d.message || 'Fehler');
            fb.className = 'inbox-feedback err';
        }
    } catch(e) {
        fb.textContent = '✗ Verbindungsfehler';
        fb.className = 'inbox-feedback err';
    }
    setTimeout(() => { fb.textContent = ''; fb.className = 'inbox-feedback'; }, 4000);
}

async function loadEffort() {
    try {
        const r = await fetch('/api/effort?t=' + TOKEN);
        const d = await r.json();
        if (d.ok && d.level) markEffortChip(d.level);
    } catch(e) {}
}

// ====== BRAIN BROWSER ======
window._brainFiles = [];
window._brainCurrentPath = null;

async function loadBrainTree() {
    const root = document.getElementById('brain-tree-root');
    root.innerHTML = '<div style="padding:8px;color:var(--text-4);font-size:12px">Lade Vault...</div>';
    try {
        const r = await fetch('/brain-files-tree?t=' + TOKEN);
        const d = await r.json();
        if (!d.ok) throw new Error(d.error || 'unknown');
        window._brainFiles = [];
        collectBrainFiles(d.tree);
        root.innerHTML = '';
        renderBrainNode(d.tree, root, 0, true);
        const stats = document.getElementById('brain-stats');
        if (stats) stats.textContent = window._brainFiles.length + ' Files';
    } catch (e) {
        root.innerHTML = '<div style="padding:8px;color:var(--red);font-size:12px">Fehler: ' + escHtml(e.message) + '</div>';
    }
}

let _brainSearchTimer = null;
function onBrainSearchInput() {
    const q = (document.getElementById('brain-search').value || '').trim();
    const tree = document.getElementById('brain-tree-root');
    const results = document.getElementById('brain-search-results');
    const modeLbl = document.getElementById('brain-mode-label');
    if (_brainSearchTimer) clearTimeout(_brainSearchTimer);
    if (q.length === 0) {
        tree.style.display = '';
        results.style.display = 'none';
        if (modeLbl) modeLbl.textContent = '';
        return;
    }
    // First: instant client-side filter on file paths/names
    const lq = q.toLowerCase();
    const matches = window._brainFiles.filter(p => p.toLowerCase().includes(lq));
    if (modeLbl) modeLbl.textContent = matches.length + ' Filter';
    tree.style.display = 'none';
    results.style.display = '';
    results.innerHTML = matches.slice(0, 60).map(p => {
        const name = p.split('/').pop().replace(/\\.md$/i, '');
        const dir = p.split('/').slice(0, -1).join('/');
        return '<div class="brain-tree-row" data-path="' + escAttr(p) + '" onclick="openBrainFile(this.dataset.path)" style="padding:5px 8px"><span class="caret"></span><span class="icon">&#128196;</span><span class="name">' + escHtml(name) + '<span style="color:var(--text-4);font-size:10px;display:block;margin-top:2px;font-family:monospace">' + escHtml(dir) + '</span></span></div>';
    }).join('') || '<div style="padding:8px;color:var(--text-4);font-size:11px">Keine Datei-Treffer. Semantic-Suche läuft...</div>';
    // After 400ms idle, also fire semantic search and append below client filter
    _brainSearchTimer = setTimeout(() => runBrainSemantic(q), 400);
}

async function runBrainSemantic(q) {
    if (!q || q.length < 2) return;
    try {
        const r = await fetch('/brain-search?t=' + TOKEN + '&q=' + encodeURIComponent(q));
        const d = await r.json();
        if (!d.ok || !d.hits || !d.hits.length) return;
        const results = document.getElementById('brain-search-results');
        const sep = '<div style="padding:6px 8px;font-size:10px;color:var(--text-4);text-transform:uppercase;letter-spacing:0.05em;border-top:1px dashed rgba(255,255,255,0.06);margin-top:8px">Semantic (' + d.hits.length + (d.fallback ? ' · ' + d.fallback : '') + ')</div>';
        const items = d.hits.map(h => {
            const path = (h.path || '').replace('/root/aria/brain/', '');
            const name = (h.title || path.split('/').pop() || '').replace(/\\.md$/i, '');
            const score = h.score ? Math.round(h.score * 100) + '%' : '';
            const snippet = h.snippet ? '<div style="font-size:10px;color:var(--text-3);line-height:1.35;margin-top:2px;max-height:36px;overflow:hidden">' + escHtml(h.snippet) + '</div>' : '';
            return '<div class="brain-tree-row" data-path="' + escAttr(path) + '" onclick="openBrainFile(this.dataset.path)" style="padding:6px 8px;flex-direction:column;align-items:flex-start"><div style="display:flex;justify-content:space-between;width:100%;gap:6px"><span class="name">' + escHtml(name) + '</span><span style="font-size:10px;color:var(--accent)">' + score + '</span></div>' + snippet + '</div>';
        }).join('');
        results.insertAdjacentHTML('beforeend', sep + items);
    } catch (e) {}
}

function collectBrainFiles(node) {
    if (!node) return;
    if (node.type === 'file') {
        window._brainFiles.push(node.path);
    } else if (node.children) {
        node.children.forEach(collectBrainFiles);
    }
}

function renderBrainNode(node, container, depth, isRoot) {
    if (!node) return;
    if (node.type === 'folder') {
        if (!isRoot) {
            const row = document.createElement('div');
            row.className = 'brain-tree-row brain-tree-folder';
            row.innerHTML = '<span class="caret">&#9662;</span><span class="icon">&#128193;</span><span class="name">' + escHtml(node.name) + '</span>';
            const childrenWrap = document.createElement('div');
            childrenWrap.className = 'brain-tree-children';
            row.onclick = (e) => {
                e.stopPropagation();
                row.classList.toggle('collapsed');
                childrenWrap.classList.toggle('collapsed');
            };
            container.appendChild(row);
            container.appendChild(childrenWrap);
            (node.children || []).forEach(c => renderBrainNode(c, childrenWrap, depth + 1, false));
        } else {
            (node.children || []).forEach(c => renderBrainNode(c, container, depth, false));
        }
    } else if (node.type === 'file') {
        const row = document.createElement('div');
        row.className = 'brain-tree-row';
        row.dataset.path = node.path;
        row.innerHTML = '<span class="caret"></span><span class="icon">&#128196;</span><span class="name">' + escHtml(node.name.replace(/\\.md$/i, '')) + '</span>';
        row.onclick = (e) => { e.stopPropagation(); openBrainFile(node.path); };
        container.appendChild(row);
    }
}

async function openBrainFile(relPath) {
    window._brainCurrentPath = relPath;
    document.querySelectorAll('#brain-tree-root .brain-tree-row').forEach(r => {
        r.classList.toggle('active', r.dataset.path === relPath);
    });
    const reader = document.getElementById('brain-reader');
    reader.innerHTML = '<div class="brain-reader-empty">Lade...</div>';
    const pathEl = document.getElementById('brain-current-path');
    if (pathEl) pathEl.textContent = relPath;
    const sidebar = document.getElementById('brain-sidebar');
    if (sidebar && window.innerWidth <= 768) sidebar.classList.remove('open');
    try {
        const r = await fetch('/brain-file?t=' + TOKEN + '&path=' + encodeURIComponent(relPath));
        const d = await r.json();
        if (!d.ok) throw new Error(d.error || 'unknown');
        renderBrainFile(d, relPath);
    } catch (e) {
        reader.innerHTML = '<div class="brain-reader-empty" style="color:var(--red)">Fehler: ' + escHtml(e.message) + '</div>';
    }
}

function renderBrainFile(d, relPath) {
    const reader = document.getElementById('brain-reader');
    let content = d.content || '';
    let frontmatter = '';
    const fm = content.match(/^---\\n([\\s\\S]*?)\\n---\\n/);
    if (fm) {
        frontmatter = fm[1];
        content = content.slice(fm[0].length);
    }
    const date = d.mtime ? new Date(d.mtime * 1000).toLocaleString('de-DE') : '';
    const html = renderMarkdown(content);
    reader.innerHTML =
        '<div class="brain-reader-title">' + escHtml(d.title || relPath) + '</div>' +
        '<div class="brain-reader-meta">' + escHtml(relPath) + ' &middot; ' + (d.size || 0) + ' Bytes &middot; ' + escHtml(date) + '</div>' +
        (frontmatter ? '<details class="brain-reader-frontmatter"><summary style="cursor:pointer;color:var(--accent);font-size:11px">Frontmatter</summary>' + escHtml(frontmatter) + '</details>' : '') +
        '<div class="brain-reader-content">' + html + '</div>';
    reader.scrollTop = 0;
    reader.querySelectorAll('.wikilink').forEach(link => {
        link.onclick = (e) => {
            e.preventDefault();
            const target = link.dataset.target;
            const match = resolveWikilink(target);
            if (match) openBrainFile(match);
            else alog('Brain: Wikilink "' + target + '" nicht gefunden');
        };
    });
}

function resolveWikilink(name) {
    if (!name) return null;
    const norm = name.toLowerCase().replace(/\\.md$/i, '');
    return window._brainFiles.find(p => {
        const base = p.split('/').pop().toLowerCase().replace(/\\.md$/i, '');
        return base === norm;
    }) || window._brainFiles.find(p => p.toLowerCase().replace(/\\.md$/i, '').endsWith('/' + norm)) || null;
}

function renderMarkdown(src) {
    if (!src) return '';
    // Escape HTML first
    let s = src.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    // Code fences
    s = s.replace(/```([a-zA-Z0-9_-]*)\\n([\\s\\S]*?)```/g, (_, lang, code) =>
        '<pre><code class="lang-' + lang + '">' + code + '</code></pre>'
    );
    // Inline code
    s = s.replace(/`([^`\\n]+)`/g, '<code>$1</code>');
    // Headings
    s = s.replace(/^######\\s+(.+)$/gm, '<h6>$1</h6>');
    s = s.replace(/^#####\\s+(.+)$/gm, '<h5>$1</h5>');
    s = s.replace(/^####\\s+(.+)$/gm, '<h4>$1</h4>');
    s = s.replace(/^###\\s+(.+)$/gm, '<h3>$1</h3>');
    s = s.replace(/^##\\s+(.+)$/gm, '<h2>$1</h2>');
    s = s.replace(/^#\\s+(.+)$/gm, '<h1>$1</h1>');
    // HR
    s = s.replace(/^---+$/gm, '<hr>');
    // Blockquote
    s = s.replace(/^>\\s?(.+)$/gm, '<blockquote>$1</blockquote>');
    // Wikilinks [[Note|alias]] or [[Note]]
    s = s.replace(/\\[\\[([^\\]\\|\\n]+)(?:\\|([^\\]\\n]+))?\\]\\]/g, (_, target, alias) => {
        const label = alias || target;
        return '<span class="wikilink" data-target="' + escAttr(target.trim()) + '">' + label + '</span>';
    });
    // Markdown links [text](url)
    s = s.replace(/\\[([^\\]\\n]+)\\]\\(([^\\)\\n]+)\\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
    // Bold + italic
    s = s.replace(/\\*\\*([^\\*\\n]+)\\*\\*/g, '<strong>$1</strong>');
    s = s.replace(/(^|[^\\*])\\*([^\\*\\n]+)\\*/g, '$1<em>$2</em>');
    // Tables (very simple)
    s = s.replace(/((?:^\\|.+\\|\\s*\\n)+)/gm, (block) => {
        const rows = block.trim().split('\\n').map(r => r.trim());
        if (rows.length < 2) return block;
        const sep = rows[1];
        if (!/^\\|?[\\s\\-\\|:]+\\|?$/.test(sep)) return block;
        const head = rows[0].split('|').slice(1, -1).map(c => '<th>' + c.trim() + '</th>').join('');
        const body = rows.slice(2).map(r => '<tr>' + r.split('|').slice(1, -1).map(c => '<td>' + c.trim() + '</td>').join('') + '</tr>').join('');
        return '<table><thead><tr>' + head + '</tr></thead><tbody>' + body + '</tbody></table>';
    });
    // Lists (unordered)
    s = s.replace(/(^[ \\t]*[-*+]\\s+.+(?:\\n[ \\t]*[-*+]\\s+.+)*)/gm, (block) => {
        const items = block.split('\\n').map(l => l.replace(/^[ \\t]*[-*+]\\s+/, '')).map(l => '<li>' + l + '</li>').join('');
        return '<ul>' + items + '</ul>';
    });
    // Lists (ordered)
    s = s.replace(/(^[ \\t]*\\d+\\.\\s+.+(?:\\n[ \\t]*\\d+\\.\\s+.+)*)/gm, (block) => {
        const items = block.split('\\n').map(l => l.replace(/^[ \\t]*\\d+\\.\\s+/, '')).map(l => '<li>' + l + '</li>').join('');
        return '<ol>' + items + '</ol>';
    });
    // Paragraphs (lines that are not block-level already)
    s = s.split(/\\n{2,}/).map(block => {
        if (/^<(h\\d|ul|ol|li|pre|table|blockquote|hr|p)/.test(block.trim())) return block;
        if (!block.trim()) return '';
        return '<p>' + block.trim().replace(/\\n/g, '<br>') + '</p>';
    }).join('\\n');
    return s;
}

function escAttr(s) {
    return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

function toggleBrainSidebar() {
    const sb = document.getElementById('brain-sidebar');
    if (sb) sb.classList.toggle('open');
}

function brainCollapseAll() {
    const root = document.getElementById('brain-tree-root');
    if (!root) return;
    root.querySelectorAll('.brain-tree-folder').forEach(r => r.classList.add('collapsed'));
    root.querySelectorAll('.brain-tree-children').forEach(c => c.classList.add('collapsed'));
}

function brainExpandAll() {
    const root = document.getElementById('brain-tree-root');
    if (!root) return;
    root.querySelectorAll('.brain-tree-folder').forEach(r => r.classList.remove('collapsed'));
    root.querySelectorAll('.brain-tree-children').forEach(c => c.classList.remove('collapsed'));
}

window._brainGraphLoaded = false;
window._brainGraphMode = false;

async function toggleBrainGraph() {
    const reader = document.getElementById('brain-reader');
    const graphView = document.getElementById('brain-graph-view');
    const btn = document.getElementById('brain-graph-toggle');
    window._brainGraphMode = !window._brainGraphMode;
    if (window._brainGraphMode) {
        reader.style.display = 'none';
        graphView.style.display = 'block';
        btn.classList.add('active');
        if (!window._brainGraphLoaded) {
            await loadD3();
            await loadBrainGraph();
            window._brainGraphLoaded = true;
        }
    } else {
        reader.style.display = '';
        graphView.style.display = 'none';
        btn.classList.remove('active');
    }
}

function loadD3() {
    return new Promise((resolve, reject) => {
        if (window.d3) return resolve();
        const s = document.createElement('script');
        s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
        s.onload = resolve;
        s.onerror = () => reject(new Error('D3 konnte nicht geladen werden'));
        document.head.appendChild(s);
    });
}

async function loadBrainGraph() {
    const status = document.getElementById('brain-graph-status');
    status.textContent = 'Lade Knoten und Kanten...';
    try {
        const r = await fetch('/brain-graph?t=' + TOKEN);
        const d = await r.json();
        if (!d.ok) throw new Error(d.error || 'unknown');
        renderBrainGraph(d.nodes || [], d.edges || []);
        status.textContent = d.nodes.length + ' Notes · ' + d.edges.length + ' Wikilinks';
    } catch (e) {
        status.textContent = 'Fehler: ' + (e.message || e);
        status.style.color = 'var(--red)';
    }
}

function renderBrainGraph(nodes, links) {
    const svg = d3.select('#brain-graph-svg');
    svg.selectAll('*').remove();
    const wrap = document.getElementById('brain-graph-view');
    const w = wrap.clientWidth || 800;
    const h = wrap.clientHeight || 600;
    svg.attr('viewBox', [0, 0, w, h]);

    const g = svg.append('g');
    const zoom = d3.zoom().scaleExtent([0.2, 5]).on('zoom', (event) => g.attr('transform', event.transform));
    svg.call(zoom);

    const groupColors = {};
    const palette = ['#1EF0A1', '#64D2FF', '#FFD60A', '#FF9F0A', '#FF453A', '#BF5AF2', '#5E9FFF', '#5AC8FA', '#FF6E91', '#A8E063'];
    const groups = Array.from(new Set(nodes.map(n => n.group)));
    groups.forEach((g, i) => groupColors[g] = palette[i % palette.length]);

    // Linked nodes have id-based references for d3-force
    const linkData = links.map(l => Object.assign({}, l));

    const link = g.append('g')
        .attr('class', 'links')
        .selectAll('line')
        .data(linkData)
        .enter().append('line')
        .attr('class', 'link');

    const node = g.append('g')
        .attr('class', 'nodes')
        .selectAll('circle')
        .data(nodes)
        .enter().append('circle')
        .attr('class', 'node')
        .attr('r', 4)
        .attr('fill', n => groupColors[n.group] || '#888')
        .attr('stroke', 'rgba(0,0,0,0.4)')
        .attr('stroke-width', 1)
        .on('click', (event, n) => {
            window._brainGraphMode = false;
            document.getElementById('brain-graph-view').style.display = 'none';
            document.getElementById('brain-reader').style.display = '';
            document.getElementById('brain-graph-toggle').classList.remove('active');
            openBrainFile(n.id);
        })
        .call(d3.drag()
            .on('start', (event, d) => { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
            .on('drag', (event, d) => { d.fx = event.x; d.fy = event.y; })
            .on('end', (event, d) => { if (!event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; }));

    node.append('title').text(n => n.label + '  [' + n.group + ']');

    const label = g.append('g')
        .attr('class', 'labels')
        .selectAll('text')
        .data(nodes.filter((_, i) => i % 1 === 0))
        .enter().append('text')
        .attr('class', 'label')
        .attr('dx', 6)
        .attr('dy', 3)
        .text(n => n.label.length > 28 ? n.label.slice(0, 26) + '…' : n.label)
        .style('opacity', 0);

    const simulation = d3.forceSimulation(nodes)
        .force('link', d3.forceLink(linkData).id(d => d.id).distance(40).strength(0.4))
        .force('charge', d3.forceManyBody().strength(-60))
        .force('center', d3.forceCenter(w / 2, h / 2))
        .force('collide', d3.forceCollide(8))
        .on('tick', () => {
            link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
                .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
            node.attr('cx', d => d.x).attr('cy', d => d.y);
            label.attr('x', d => d.x).attr('y', d => d.y);
        });

    // Show labels at higher zoom
    svg.on('wheel.zoom-show-labels', () => {
        const t = d3.zoomTransform(svg.node());
        label.style('opacity', t.k > 1.5 ? 1 : 0);
    });
}

</script>
</body>
</html>"""


class Handler(BaseHTTPRequestHandler):

    def check_token(self, token):
        return token == TOKEN

    def _parse_params(self):
        params = {}
        if '?' in self.path:
            for p in self.path.split('?')[1].split('&'):
                if '=' in p:
                    k, v = p.split('=', 1)
                    params[k] = v
        return params

    def _send_json(self, data, code=200):
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps(data).encode())

    def _send_telegram(self, text):
        """Route an inbox message to the Aria tmux session.

        Phase 0 refactor (2026-05-17): tmux send-keys is the critical path and
        determines success. The Telegram confirmation `📥 Aria erhält: …` runs
        fire-and-forget in a daemon thread — its failures are logged but never
        leak to the browser. Prior version returned `HTTP Error 400: Bad Request`
        to the panel when Telegram's API rejected the confirmation, even though
        the message had already reached Aria.
        """
        import urllib.request
        import re
        import threading
        if not isinstance(text, str):
            return {"ok": False, "message": "invalid input"}
        text = re.sub(r'[\x00-\x08\x0b-\x1f\x7f-\x9f]', '', text)
        if len(text) > 4000:
            text = text[:4000]
        if not text.strip():
            return {"ok": False, "message": "empty after sanitize"}
        # Audit trail
        try:
            ts = datetime.utcnow().isoformat() + 'Z'
            ip = self.client_address[0] if hasattr(self, 'client_address') else '-'
            preview = text[:80].replace('\n', '\\n')
            with open('/var/log/aria-control-send.log', 'a') as f:
                f.write(f"[{ts}] {ip} len={len(text)} preview={preview!r}\n")
        except Exception:
            pass
        # Critical path — tmux send-keys decides success/failure.
        try:
            subprocess.run(["tmux", "send-keys", "-t", "aria", "C-c"], capture_output=True, timeout=2)
            subprocess.run(["tmux", "send-keys", "-t", "aria", "C-u"], capture_output=True, timeout=2)
            subprocess.run(["tmux", "send-keys", "-l", "-t", "aria", text], timeout=5)
            subprocess.run(["tmux", "send-keys", "-t", "aria", "Enter"], capture_output=True, timeout=2)
        except Exception as e:
            return {"ok": False, "message": f"tmux send failed: {e}"}
        # Background — Telegram confirmation, non-blocking.
        if TELEGRAM_BOT_TOKEN:
            preview_t = text[:80] + ("…" if len(text) > 80 else "")
            confirm = f"📥 Aria erhält: {preview_t}"
            def _send_tg_confirm():
                try:
                    payload = json.dumps({"chat_id": OWNER_CHAT_ID, "text": confirm}).encode()
                    req = urllib.request.Request(
                        f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage",
                        data=payload, headers={"Content-Type": "application/json"})
                    urllib.request.urlopen(req, timeout=8)
                except Exception as tg_e:
                    try:
                        with open('/var/log/aria-control-send.log', 'a') as f:
                            f.write(f"[{datetime.utcnow().isoformat()}Z] confirm-fail: {tg_e}\n")
                    except Exception:
                        pass
            threading.Thread(target=_send_tg_confirm, daemon=True).start()
        return {"ok": True}

    # === Effort quick-set (additive) ===
    EFFORT_LEVELS = ("low", "medium", "high", "xhigh", "max")
    EFFORT_STATE_FILE = "/root/aria/state/effort"

    def _get_effort(self):
        """Zuletzt ueber das Panel gesetztes Level — Anzeige-State, kein Live-Read aus Claude."""
        try:
            with open(self.EFFORT_STATE_FILE) as f:
                level = f.read().strip()
            return {"ok": True, "level": level if level in self.EFFORT_LEVELS else None}
        except Exception:
            return {"ok": True, "level": None}

    def _set_effort(self, level):
        """Tippt /effort <level> in die Aria-Session.

        Bewusst OHNE C-c (anders als _send_telegram): ein laufender Turn darf
        durch einen Effort-Wechsel nicht abgebrochen werden. C-u raeumt nur
        die Eingabezeile.
        """
        level = (level or "").strip().lower()
        if level not in self.EFFORT_LEVELS:
            return {"ok": False, "message": f"Ungueltiges Level: {level!r}"}
        try:
            ts = datetime.utcnow().isoformat() + 'Z'
            ip = self.client_address[0] if hasattr(self, 'client_address') else '-'
            with open('/var/log/aria-control-send.log', 'a') as f:
                f.write(f"[{ts}] {ip} effort={level}\n")
        except Exception:
            pass
        try:
            # C-u loescht nur die aktuelle Zeile — bei mehrzeiligen Resten im
            # Input (z.B. haengengebliebener Startup-Prompt) reicht 1x nicht.
            for _ in range(3):
                subprocess.run(["tmux", "send-keys", "-t", "aria", "C-u"], capture_output=True, timeout=2)
            subprocess.run(["tmux", "send-keys", "-l", "-t", "aria", f"/effort {level}"], timeout=5)
            time.sleep(0.3)
            subprocess.run(["tmux", "send-keys", "-t", "aria", "Enter"], capture_output=True, timeout=2)
        except Exception as e:
            return {"ok": False, "message": f"tmux send failed: {e}"}
        try:
            os.makedirs(os.path.dirname(self.EFFORT_STATE_FILE), exist_ok=True)
            with open(self.EFFORT_STATE_FILE, "w") as f:
                f.write(level)
        except Exception:
            pass
        return {"ok": True, "level": level}

    # === KAR-679 Worker-Fleet helpers (additive) ===
    def _safe_read(self, path):
        """Read a single-line state file, return stripped string or ''."""
        try:
            with open(path) as f:
                return f.read().strip()
        except Exception:
            return ''

    def _list_workers(self):
        result = {"ok": False, "workers": []}
        try:
            r = subprocess.run(
                ["tmux", "list-sessions",
                 "-F", "#{session_name}\t#{session_created}\t#{session_activity}"],
                capture_output=True, text=True, timeout=3
            )
            workers = []
            now_ts = time.time()
            for line in r.stdout.splitlines():
                parts = line.split('\t')
                if len(parts) < 3:
                    continue
                session_name, created_ts, activity_ts = parts[0], parts[1], parts[2]
                if not session_name.startswith('aria-'):
                    continue
                worker_name = session_name[5:]  # strip "aria-"
                if not worker_name:
                    continue
                state_dir = os.path.join(WORKERS_STATE, worker_name)
                pane = ''
                try:
                    pr = subprocess.run(
                        ["tmux", "capture-pane", "-t", session_name, "-p", "-J"],
                        capture_output=True, text=True, timeout=2
                    )
                    lines = [l for l in pr.stdout.split('\n') if l.strip()]
                    pane = '\n'.join(lines[-10:])
                except Exception:
                    pass
                ready = '❯' in pane
                try:
                    act_ts = int(activity_ts)
                    ago_s = max(0, int(now_ts - act_ts))
                    if ago_s < 60: ago = f"{ago_s}s"
                    elif ago_s < 3600: ago = f"{ago_s // 60}m"
                    elif ago_s < 86400: ago = f"{ago_s // 3600}h"
                    else: ago = f"{ago_s // 86400}d"
                except Exception:
                    ago = '—'
                    act_ts = 0
                workers.append({
                    "name":               worker_name,
                    "session":            session_name,
                    "role":               self._safe_read(os.path.join(state_dir, "role")),
                    "scope":              self._safe_read(os.path.join(state_dir, "scope")),
                    "workdir":            self._safe_read(os.path.join(state_dir, "workdir")),
                    "mode":               self._safe_read(os.path.join(state_dir, "mode")) or "assist",
                    "alive":              True,
                    "ready":              ready,
                    "last_activity_ts":   act_ts,
                    "last_activity_ago":  ago,
                    "branch":             self._safe_read(os.path.join(state_dir, "branch")),
                    "pid":                self._safe_read(os.path.join(state_dir, "pid")),
                    "pane_snapshot":      pane,
                })
            result["ok"] = True
            result["workers"] = workers
        except Exception as e:
            result["error"] = str(e)
        return result

    def _worker_pane(self, name):
        if not name or not _WORKER_NAME_RE.match(name):
            return {"ok": False, "message": "invalid name"}
        session = f"aria-{name}"
        try:
            r = subprocess.run(
                ["tmux", "capture-pane", "-t", session, "-p", "-J"],
                capture_output=True, text=True, timeout=3
            )
            lines = [l for l in r.stdout.split('\n') if l.strip()]
            return {"ok": True, "name": name, "session": session, "pane": '\n'.join(lines[-200:])}
        except Exception as e:
            return {"ok": False, "message": str(e)}

    def _worker_send(self, name, text):
        import re as _re
        if not name or not _WORKER_NAME_RE.match(name):
            return {"ok": False, "message": "invalid name"}
        session = f"aria-{name}"
        if session == "aria":
            return {"ok": False, "message": "HARD CONSTRAINT: cannot target aria session"}
        if not text:
            return {"ok": False, "message": "empty text"}
        text = _re.sub(r'[\x00-\x08\x0b-\x1f\x7f-\x9f]', '', text)
        if len(text) > 4000:
            text = text[:4000]
        try:
            ts = datetime.utcnow().isoformat() + 'Z'
            with open('/var/log/aria-worker-send.log', 'a') as f:
                f.write(f"[{ts}] {name} len={len(text)} preview={text[:80]!r}\n")
        except Exception:
            pass
        try:
            subprocess.run(["tmux", "send-keys", "-t", session, "C-c"],
                           capture_output=True, timeout=2)
            subprocess.run(["tmux", "send-keys", "-t", session, "C-u"],
                           capture_output=True, timeout=2)
            subprocess.run(["tmux", "send-keys", "-l", "-t", session, text],
                           timeout=5)
            subprocess.run(["tmux", "send-keys", "-t", session, "Enter"],
                           capture_output=True, timeout=2)
            return {"ok": True}
        except Exception as e:
            return {"ok": False, "message": f"tmux send failed: {e}"}

    def _worker_start(self, name, project, role):
        import re as _re
        if not name or not _WORKER_NAME_RE.match(name):
            return {"ok": False, "message": "invalid name"}
        if not project.startswith('/root/'):
            return {"ok": False, "message": "project must be under /root/"}
        if not role or not _re.match(r'^[a-zA-Z0-9][a-zA-Z0-9_-]*$', role):
            return {"ok": False, "message": "invalid role"}
        try:
            import subprocess as _sp
            _sp.Popen(
                ["bash", "/root/aria/scripts/aria-worker.sh", name, project, role],
                stdout=open('/var/log/aria-worker-spawn.log', 'a'),
                stderr=subprocess.STDOUT,
            )
            return {"ok": True, "spawning": True, "session": f"aria-{name}"}
        except Exception as e:
            return {"ok": False, "message": str(e)}

    def _worker_stop(self, name):
        if not name or not _WORKER_NAME_RE.match(name):
            return {"ok": False, "message": "invalid name"}
        session = f"aria-{name}"
        if session == "aria":
            return {"ok": False, "message": "HARD CONSTRAINT"}
        stop_script = "/root/aria/scripts/aria-worker-stop.sh"
        try:
            if os.path.isfile(stop_script):
                r = subprocess.run(
                    ["bash", stop_script, name],
                    capture_output=True, text=True, timeout=30
                )
                ok = r.returncode == 0
                msg = (r.stdout + r.stderr).strip().split('\n')[-1]
            else:
                r = subprocess.run(
                    ["tmux", "kill-session", "-t", session],
                    capture_output=True, text=True, timeout=5
                )
                ok = r.returncode == 0
                msg = "killed (no stop script)"
            return {"ok": ok, "message": msg}
        except subprocess.TimeoutExpired:
            return {"ok": False, "message": "stop timeout (30s)"}
        except Exception as e:
            return {"ok": False, "message": str(e)}

    def _worker_set_mode(self, name, mode):
        if not name or not _WORKER_NAME_RE.match(name):
            return {"ok": False, "message": "invalid name"}
        if mode not in ("assist", "auto", "yolo"):  # CF-7: "off" rejected
            return {"ok": False,
                    "message": "mode must be assist|auto|yolo — to stop, use /api/worker-stop"}
        mode_path = os.path.join(WORKERS_STATE, name, "mode")
        try:
            with open(mode_path, 'w') as f:
                f.write(mode + '\n')
            return {"ok": True, "name": name, "mode": mode}
        except Exception as e:
            return {"ok": False, "message": str(e)}

    def do_GET(self):
        path = self.path.split('?')[0]
        params = self._parse_params()

        if path == '/health':
            status = self._get_status()
            self.send_response(200)
            self.send_header("Content-Type", "text/plain")
            self.end_headers()
            self.wfile.write(status["status"].encode())
            return

        if not self.check_token(params.get('t', '')):
            self._send_json({"error": "Zugriff verweigert"}, 403)
            return

        if path == '/':
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Cache-Control", "no-store, no-cache, must-revalidate")
            self.end_headers()
            self.wfile.write(HTML_PAGE.encode())

        elif path == '/status':
            self._send_json(self._get_status())

        elif path == '/system':
            self._send_json(self._get_system())

        elif path == '/services':
            self._send_json(self._get_services())

        elif path == '/logs':
            self._send_json(self._get_logs())

        elif path == '/activity':
            session_name = params.get('session', 'aria')
            self._send_json(self._get_activity(session_name))

        elif path == '/usage':
            self._send_json(self._get_usage())

        elif path == '/context':
            self._send_json(self._get_context())

        elif path == '/session-hero':
            self._send_json(self._get_session_hero())

        elif path == '/erkenntnisse':
            self._send_json(self._get_erkenntnisse())

        elif path == '/linear':
            self._send_json(self._get_linear())

        elif path == '/calendar':
            self._send_json(self._get_calendar())

        elif path == '/planer':
            html = PLANER_PAGE.replace('__TOKEN__', TOKEN)
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Cache-Control", "no-store, no-cache, must-revalidate")
            self.send_header("Pragma", "no-cache")
            self.send_header("Expires", "0")
            self.end_headers()
            self.wfile.write(html.encode())

        elif path == '/api/tasks':
            self._send_json(self._get_tasks())

        elif path == '/n8n':
            self._send_json(self._get_n8n())

        elif path == '/social':
            self._send_json(self._get_social())

        elif path == '/news':
            self._send_json(self._get_news())

        elif path == '/capabilities':
            self._send_json(self._get_capabilities())

        elif path == '/tool-failures':
            limit = int(params.get('limit', '50'))
            self._send_json(self._get_tool_failures(limit=limit))

        elif path == '/cost-overview':
            self._send_json(self._get_cost_overview())

        elif path == '/brain-search':
            q = params.get('q', '')
            self._send_json(self._get_brain_search(q, top_k=8))

        elif path == '/brain-tree':
            self._send_json(self._get_brain_tree())

        elif path == '/brain-files-tree':
            self._send_json(self._get_brain_files_tree())

        elif path == '/brain-graph':
            self._send_json(self._get_brain_graph())

        elif path == '/brain-file':
            rel = params.get('path', '')
            self._send_json(self._get_brain_file_content(rel))

        elif path == '/quick-skills':
            self._send_json(self._get_quick_skills())

        elif path == '/api/effort':
            self._send_json(self._get_effort())

        elif path == '/team':
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Cache-Control", "no-store")
            self.end_headers()
            self.wfile.write(TEAM_PAGE.replace('__TOKEN__', TOKEN).encode())

        elif path == '/api/workers':
            self._send_json(self._list_workers())

        elif path == '/api/worker-pane':
            name = params.get('name', '').strip()
            self._send_json(self._worker_pane(name))

        else:
            self.send_response(404)
            self.end_headers()

    def do_POST(self):
        path = self.path.split('?')[0]
        params = self._parse_params()
        length = int(self.headers.get('Content-Length', 0))
        body = json.loads(self.rfile.read(length)) if length else {}

        # Accept token from URL param or body
        token = params.get('t', '') or body.get('token', '')
        if not self.check_token(token):
            self._send_json({"ok": False, "message": "Zugriff verweigert"}, 403)
            return

        if path == '/restart':
            self._send_json(self._restart_session())
        elif path == '/git-pull':
            self._send_json(self._git_pull())
        elif path == '/api/tasks':
            self._send_json(self._save_task(body))
        elif path == '/api/tasks/delete':
            self._send_json(self._delete_task(body.get('id', '')))
        elif path == '/api/send-message':
            text = body.get('text', '').strip()
            if not text:
                self._send_json({"ok": False, "message": "Kein Text"})
                return
            self._send_json(self._send_telegram(text))
        elif path == '/api/effort':
            self._send_json(self._set_effort(body.get('level', '')))
        elif path == '/api/quick-skill-trigger':
            skill_id = body.get('skill_id', '').strip()
            if not skill_id:
                self._send_json({"ok": False, "message": "Kein skill_id"})
                return
            self._send_json(self._trigger_quick_skill(skill_id))
        elif path == '/api/linear/complete':
            internal_id = body.get('internal_id', '').strip()
            if not internal_id:
                self._send_json({"ok": False, "message": "Kein internal_id"})
                return
            self._send_json(self._complete_linear_issue(internal_id))
        elif path == '/api/linear/start':
            internal_id = body.get('internal_id', '').strip()
            display_id = body.get('display_id', '').strip()
            if not internal_id:
                self._send_json({"ok": False, "message": "Kein internal_id"})
                return
            self._send_json(self._start_linear_issue(internal_id, display_id))
        elif path == '/api/worker-send':
            name = body.get('name', '').strip()
            text = body.get('text', '').strip()
            self._send_json(self._worker_send(name, text))
        elif path == '/api/worker-start':
            name    = body.get('name', '').strip()
            project = body.get('project', '').strip()
            role    = body.get('role', '').strip()
            self._send_json(self._worker_start(name, project, role))
        elif path == '/api/worker-stop':
            name = body.get('name', '').strip()
            self._send_json(self._worker_stop(name))
        elif path == '/api/worker-mode':
            name = body.get('name', '').strip()
            mode = body.get('mode', '').strip()
            self._send_json(self._worker_set_mode(name, mode))
        else:
            self.send_response(404)
            self.end_headers()

    # --- Data Collectors (alle leichtgewichtig, kein polling) ---

    def _get_session_hero(self):
        """One-shot endpoint for the Hero-Session-Card. Composes data from
        existing helpers (status, context) plus git/cwd/today-stats. Designed
        to be the single call the Home tab makes for the hero card."""
        out = {
            "state": "offline",       # online | idle | offline
            "state_label": "—",
            "uptime": "",
            "model_full": "",
            "model_short": "",
            "context": {"percent": 0.0, "tokens": 0, "max": 0, "input": 0, "output": 0},
            "cwd": "/root",
            "branch": "",
            "branch_state": "",       # clean | "X ahead" | "Y modified" | …
            "last_action": {"text": "", "ago": ""},
            "today": {"commits": 0, "tasks_done": 0, "telegrams": 0},
            "session_id": "",
            "updated_ago": "-",
        }
        try:
            st = self._get_status()
            ctx = self._get_context()
            out["state"] = "online" if st.get("status") == "online" else "offline"
            out["uptime"] = st.get("uptime", "")
            out["model_full"] = ctx.get("model", "")
            out["context"] = {
                "percent": ctx.get("percent", 0.0),
                "tokens": ctx.get("tokens", 0),
                "max": ctx.get("max_tokens", 0),
                "input": ctx.get("input", 0),
                "output": ctx.get("output", 0),
            }
            out["session_id"] = ctx.get("session_id", "")
            out["updated_ago"] = ctx.get("updated_ago", "-")

            # Pretty model label (Opus 4.7 [1M] / Sonnet 4.6 [1M] / Haiku 4.5 [200k])
            m = (out["model_full"] or "").lower()
            label = ""
            if "opus-4-7" in m: label = "Opus 4.7"
            elif "opus-4-6" in m: label = "Opus 4.6"
            elif "sonnet-4-6" in m: label = "Sonnet 4.6"
            elif "sonnet-4-5" in m: label = "Sonnet 4.5"
            elif "haiku-4-5" in m: label = "Haiku 4.5"
            elif m: label = out["model_full"]
            if label and out["context"]["max"]:
                size = "1M" if out["context"]["max"] >= 1_000_000 else f"{out['context']['max']//1000}k"
                label = f"{label} [{size}]"
            out["model_short"] = label

            # State refinement: idle if online but stale (> 5 min)
            if out["state"] == "online" and out["updated_ago"] not in ("-", ""):
                ago = out["updated_ago"]
                # parse "Xs", "Xm", "Xh"
                if ago.endswith("h") or ago.endswith("d"):
                    out["state"] = "idle"
                elif ago.endswith("m"):
                    try:
                        if int(ago[:-1]) >= 5:
                            out["state"] = "idle"
                    except ValueError:
                        pass
            out["state_label"] = {
                "online": "Aria · live",
                "idle":   "Aria · idle",
                "offline":"Aria · offline",
            }.get(out["state"], "—")

            # cwd from active session jsonl path → derive repo dir; fallback to /root
            try:
                import glob
                files = glob.glob("/root/.claude/projects/-root/*.jsonl")
                if files:
                    newest = max(files, key=os.path.getmtime)
                    # The path `-root` encodes /root as cwd. Anything richer needs
                    # parsing the jsonl; for now /root is the right answer.
                    out["cwd"] = "/root"
            except Exception:
                pass

            # Git: branch + ahead-count + modified-count for /root/aria/brain (the working repo)
            for repo in ("/root/aria/brain", "/root/aria-v4", "/root/projekte/Kadi-v2"):
                try:
                    if not os.path.isdir(os.path.join(repo, ".git")):
                        continue
                    br = subprocess.run(["git", "-C", repo, "branch", "--show-current"],
                                        capture_output=True, text=True, timeout=3)
                    branch = (br.stdout or "").strip()
                    if not branch:
                        continue
                    out["cwd"] = repo
                    out["branch"] = branch
                    # ahead/behind vs upstream + dirty count
                    ah = subprocess.run(["git", "-C", repo, "rev-list", "--count", "@{u}..HEAD"],
                                        capture_output=True, text=True, timeout=3)
                    ahead = int((ah.stdout or "0").strip() or 0)
                    st = subprocess.run(["git", "-C", repo, "status", "--porcelain"],
                                        capture_output=True, text=True, timeout=3)
                    dirty = sum(1 for line in (st.stdout or "").splitlines() if line.strip())
                    bits = []
                    if ahead: bits.append(f"{ahead} ahead")
                    if dirty: bits.append(f"{dirty} mod")
                    out["branch_state"] = " · ".join(bits) if bits else "clean"
                    # commits today across this repo
                    cm = subprocess.run(
                        ["git", "-C", repo, "log", "--since=midnight", "--oneline"],
                        capture_output=True, text=True, timeout=3,
                    )
                    out["today"]["commits"] = sum(1 for line in (cm.stdout or "").splitlines() if line.strip())
                    break
                except Exception:
                    continue

            # Last action + today telegram-count from aria_chat_log SQLite.
            try:
                db_path = "/root/.aria-chat.db"
                if os.path.exists(db_path):
                    import sqlite3
                    conn = sqlite3.connect(db_path, timeout=2)
                    cur = conn.cursor()
                    try:
                        cur.execute(
                            "SELECT direction, message_text, created_at "
                            "FROM aria_chat_log ORDER BY id DESC LIMIT 1"
                        )
                        row = cur.fetchone()
                        if row:
                            direction, msg_text, created_at = row
                            text = (msg_text or "").strip().split("\n", 1)[0][:60]
                            ago = "—"
                            try:
                                from datetime import datetime as _dt
                                ts_dt = _dt.strptime(created_at, "%Y-%m-%d %H:%M:%S")
                                ago_s = max(0, int(time.time() - ts_dt.timestamp()))
                                if ago_s < 60: ago = f"{ago_s}s"
                                elif ago_s < 3600: ago = f"{ago_s // 60}m"
                                elif ago_s < 86400: ago = f"{ago_s // 3600}h"
                                else: ago = f"{ago_s // 86400}d"
                            except Exception:
                                pass
                            label = "→ Kais" if direction == "out" else "← Kais"
                            out["last_action"] = {"text": f"{label} · {text}", "ago": ago}

                        # Today's telegram count (created_at midnight comparison)
                        today_str = time.strftime("%Y-%m-%d")
                        cur.execute(
                            "SELECT COUNT(*) FROM aria_chat_log WHERE created_at >= ?",
                            (today_str + " 00:00:00",),
                        )
                        out["today"]["telegrams"] = cur.fetchone()[0] or 0
                    finally:
                        conn.close()
            except Exception:
                pass

        except Exception as e:
            out["error"] = str(e)
        return out

    def _get_status(self):
        try:
            r = subprocess.run(["tmux", "list-sessions"], capture_output=True, text=True, timeout=3)
            if "aria" in r.stdout:
                r2 = subprocess.run(["pgrep", "-x", "claude"], capture_output=True, text=True, timeout=3)
                if r2.returncode == 0:
                    # Uptime aus tmux Session
                    uptime = ""
                    try:
                        parts = r.stdout.split("(created ")[1].split(")")[0] if "(created " in r.stdout else ""
                        if parts:
                            created = datetime.strptime(parts.strip(), "%a %b %d %H:%M:%S %Y")
                            delta = datetime.now() - created
                            h, m = divmod(int(delta.total_seconds()) // 60, 60)
                            d = h // 24
                            h = h % 24
                            if d > 0:
                                uptime = f"{d}d {h}h {m}m"
                            elif h > 0:
                                uptime = f"{h}h {m}m"
                            else:
                                uptime = f"{m}m"
                    except Exception:
                        pass
                    return {"status": "online", "uptime": uptime}
                return {"status": "offline", "uptime": "Session ohne Prozess"}
            return {"status": "offline", "uptime": ""}
        except Exception as e:
            return {"status": "offline", "error": str(e)}

    def _get_system(self):
        result = {"cpu_pct": 0, "ram_used": "-", "ram_total": "-", "ram_pct": 0,
                  "disk_used": "-", "disk_total": "-", "disk_pct": 0, "load": "-"}
        try:
            # CPU (aus /proc/loadavg, guenstig)
            with open("/proc/loadavg") as f:
                load_parts = f.read().split()
                load1 = float(load_parts[0])
                ncpu = os.cpu_count() or 2
                result["load"] = f"{load_parts[0]} / {load_parts[1]} / {load_parts[2]}"
                result["cpu_pct"] = min(100, int(load1 / ncpu * 100))

            # RAM (aus /proc/meminfo, guenstig)
            meminfo = {}
            with open("/proc/meminfo") as f:
                for line in f:
                    parts = line.split()
                    if len(parts) >= 2:
                        meminfo[parts[0].rstrip(':')] = int(parts[1])
            total = meminfo.get("MemTotal", 0)
            avail = meminfo.get("MemAvailable", 0)
            used = total - avail
            result["ram_total"] = f"{total // 1024}M"
            result["ram_used"] = f"{used // 1024}M"
            result["ram_pct"] = int(used / total * 100) if total else 0

            # Disk (aus os.statvfs, kein subprocess)
            st = os.statvfs("/")
            disk_total = st.f_blocks * st.f_frsize
            disk_free = st.f_bavail * st.f_frsize
            disk_used = disk_total - disk_free
            result["disk_total"] = f"{disk_total // (1024**3)}G"
            result["disk_used"] = f"{disk_used // (1024**3)}G"
            result["disk_pct"] = int(disk_used / disk_total * 100) if disk_total else 0

        except Exception:
            pass
        return result

    def _get_services(self):
        services = [
            ("aria-watchdog.timer", "Watchdog Timer"),
            ("aria-api.service", "API (n8n)"),
            ("aria-control.service", "Control Panel"),
            ("aria-health.service", "Health Check"),
            ("aria-git-sync.timer", "Git Sync (5min)"),
        ]
        # Check n8n Docker container
        try:
            r = subprocess.run(
                ["docker", "ps", "--filter", "name=n8n", "--format", "{{.Status}}"],
                capture_output=True, text=True, timeout=5
            )
            n8n_running = "Up" in r.stdout
        except Exception:
            n8n_running = False
        result = []
        for unit, label in services:
            try:
                r = subprocess.run(
                    ["systemctl", "is-active", unit],
                    capture_output=True, text=True, timeout=3
                )
                active = r.stdout.strip() == "active"
            except Exception:
                active = False
            result.append({"name": label, "active": active})
        result.append({"name": "n8n (Docker)", "active": n8n_running})

        # n8n Error Handler Workflow — sucht aktiven Workflow mit "Error Handler" im Namen
        error_handler_active = False
        try:
            api_key = _read_env("N8N_API_KEY")
            if api_key:
                import urllib.request as _ur
                _req = _ur.Request(
                    "http://localhost:5678/api/v1/workflows?active=true",
                    headers={"X-N8N-API-KEY": api_key}
                )
                with _ur.urlopen(_req, timeout=5) as _resp:
                    _wfs = json.loads(_resp.read()).get("data", [])
                    error_handler_active = any(
                        "error handler" in (_wf.get("name", "") or "").lower()
                        for _wf in _wfs
                    )
        except Exception:
            error_handler_active = False
        result.append({"name": "n8n Error Handler", "active": error_handler_active})

        # Supabase Chat-Log (Hook konfiguriert?)
        chat_log_ok = False
        try:
            with open("/root/.claude/settings.json") as _sf:
                _settings = json.load(_sf)
            _hooks = _settings.get("hooks", {})
            chat_log_ok = any(
                "supabase" in str(_h).lower() or "chat_log" in str(_h).lower()
                or "telegram-log" in str(_h).lower()
                for _event_entries in _hooks.values()
                for _h in (_event_entries if isinstance(_event_entries, list) else [])
            )
        except Exception:
            chat_log_ok = False
        result.append({"name": "Supabase Chat-Log", "active": chat_log_ok})

        # n8n Backup — letztes Datum
        import glob as _gl
        backup_date = ""
        try:
            _candidates = []
            for _pattern in ("/root/aria/n8n/backups/*/", "/root/aria-v3/n8n-backup/workflows/*/"):
                _candidates.extend(_gl.glob(_pattern))
            if _candidates:
                _candidates.sort()
                backup_date = os.path.basename(_candidates[-1].rstrip('/'))
        except Exception:
            pass
        result.append({"name": "n8n Backup", "active": bool(backup_date),
                       "info": backup_date or "kein Backup"})

        return result

    def _get_logs(self):
        logs = {}
        # Watchdog Log (letzte 15 Zeilen)
        try:
            r = subprocess.run(
                ["tail", "-15", "/var/log/aria-watchdog.log"],
                capture_output=True, text=True, timeout=3
            )
            logs["watchdog"] = r.stdout.strip() or "Leer"
        except Exception:
            logs["watchdog"] = "Nicht lesbar"

        # tmux Session Output (letzte Zeilen)
        try:
            r = subprocess.run(
                ["tmux", "capture-pane", "-t", "aria", "-p"],
                capture_output=True, text=True, timeout=3
            )
            # Nur nicht-leere Zeilen, letzte 15
            lines = [l for l in r.stdout.split('\n') if l.strip()]
            logs["session"] = '\n'.join(lines[-15:]) or "Leer"
        except Exception:
            logs["session"] = "Keine aktive Session"

        return logs

    def _get_activity(self, session_name="aria"):
        result = {"session": "", "tasks": [], "thinking": False}
        try:
            r = subprocess.run(
                ["tmux", "capture-pane", "-t", session_name, "-p"],
                capture_output=True, text=True, timeout=3
            )
            output = r.stdout
            lines = [l for l in output.split('\n') if l.strip()]

            # Parse tasks from output (checkmarks and bullets)
            tasks = []
            for line in lines:
                stripped = line.strip()
                if stripped.startswith('✔') or stripped.startswith('✓'):
                    tasks.append({"text": stripped.lstrip('✔✓ '), "status": "done"})
                elif stripped.startswith('◼') or stripped.startswith('◻'):
                    status = "active" if '◼' in stripped else "pending"
                    tasks.append({"text": stripped.lstrip('◼◻ '), "status": status})

            result["tasks"] = tasks

            # Check if thinking
            thinking_indicators = ["thinking", "Puttering", "Flibbertigibbeting", "Zigzagging", "Running"]
            result["thinking"] = any(ind in output for ind in thinking_indicators)

            # Session output (last 20 non-empty lines)
            result["session"] = '\n'.join(lines[-20:]) or "Leer"
        except Exception:
            result["session"] = "Keine aktive Session"
        return result

    def _get_context(self):
        # Read real API usage tokens from the newest session .jsonl.
        # Each assistant message carries message.usage — input + cache_creation + cache_read
        # is the true prompt-context size at that turn.
        result = {"tokens": 0, "max_tokens": 1000000, "percent": 0.0,
                  "session_id": "", "model": "", "updated_ago": "-",
                  "input": 0, "cache_read": 0, "cache_create": 0, "output": 0}
        try:
            import glob
            active = "/root/.claude/current_session_id"
            newest = None
            if os.path.exists(active):
                sid = open(active).read().strip()
                cand = f"/root/.claude/projects/-root/{sid}.jsonl"
                if os.path.exists(cand):
                    newest = cand
            if not newest:
                files = glob.glob("/root/.claude/projects/-root/*.jsonl")
                if not files:
                    return result
                newest = max(files, key=os.path.getmtime)

            # Scan backwards for the last assistant message with usage + model
            last_usage = None
            model_name = ""
            with open(newest, "rb") as f:
                f.seek(0, 2)
                size = f.tell()
                chunk = min(size, 64 * 1024)
                f.seek(size - chunk)
                tail = f.read().decode("utf-8", errors="replace")
            for line in reversed(tail.splitlines()):
                line = line.strip()
                if not line:
                    continue
                try:
                    d = json.loads(line)
                except Exception:
                    continue
                msg = d.get("message") or {}
                u = msg.get("usage")
                if u and isinstance(u, dict):
                    last_usage = u
                    model_name = msg.get("model") or ""
                    break

            # Model-aware context window
            model_max = {
                "claude-opus-4-7": 1_000_000,
                "claude-opus-4-6": 1_000_000,
                "claude-sonnet-4-6": 1_000_000,
                "claude-sonnet-4-5": 1_000_000,
                "claude-haiku-4-5": 200_000,
            }
            for key, mx in model_max.items():
                if key in model_name:
                    result["max_tokens"] = mx
                    break
            result["model"] = model_name

            if last_usage:
                inp = int(last_usage.get("input_tokens") or 0)
                cc = int(last_usage.get("cache_creation_input_tokens") or 0)
                cr = int(last_usage.get("cache_read_input_tokens") or 0)
                out = int(last_usage.get("output_tokens") or 0)
                tokens = inp + cc + cr
                result["tokens"] = tokens
                result["input"] = inp
                result["cache_create"] = cc
                result["cache_read"] = cr
                result["output"] = out
                result["percent"] = min(100.0, round(tokens / result["max_tokens"] * 100, 1))

            result["session_id"] = os.path.basename(newest).replace(".jsonl", "")[:8]
            delta = int(time.time() - os.path.getmtime(newest))
            if delta < 60:
                result["updated_ago"] = f"{delta}s"
            elif delta < 3600:
                result["updated_ago"] = f"{delta // 60}m"
            else:
                result["updated_ago"] = f"{delta // 3600}h"
        except Exception as e:
            result["error"] = str(e)
        return result

    def _get_usage(self):
        result = {"duration": "-", "processes": "-", "claude_cpu": "-", "claude_ram": "-",
                  "today_tokens": "-", "today_sessions": "-", "week_tokens": "-",
                  "week_sessions": "-", "current_tokens": "-", "total_tokens": "-"}
        try:
            # Session duration
            r = subprocess.run(["tmux", "list-sessions"], capture_output=True, text=True, timeout=3)
            if "aria" in r.stdout and "(created " in r.stdout:
                parts = r.stdout.split("(created ")[1].split(")")[0]
                created = datetime.strptime(parts.strip(), "%a %b %d %H:%M:%S %Y")
                delta = datetime.now() - created
                h, m = divmod(int(delta.total_seconds()) // 60, 60)
                d = h // 24
                h = h % 24
                if d > 0:
                    result["duration"] = f"{d}d {h}h {m}m"
                elif h > 0:
                    result["duration"] = f"{h}h {m}m"
                else:
                    result["duration"] = f"{m}m"

            # Claude processes
            r = subprocess.run(["pgrep", "-c", "claude"], capture_output=True, text=True, timeout=3)
            result["processes"] = r.stdout.strip() or "0"

            # Claude CPU + RAM usage
            r = subprocess.run(
                ["ps", "-C", "claude", "-o", "%cpu,%mem,rss", "--no-headers"],
                capture_output=True, text=True, timeout=3
            )
            if r.stdout.strip():
                total_cpu = 0.0
                total_ram = 0
                for line in r.stdout.strip().split('\n'):
                    parts = line.split()
                    if len(parts) >= 3:
                        total_cpu += float(parts[0])
                        total_ram += int(parts[2])
                result["claude_cpu"] = f"{total_cpu:.1f}%"
                result["claude_ram"] = f"{total_ram // 1024}M"

            # Token usage from JSONL logs
            try:
                import importlib
                usage_mod = importlib.import_module("aria-usage")

                current = usage_mod.get_current_session_usage()
                if current:
                    result["current_tokens"] = f"{current['total_tokens_k']}k"

                today = usage_mod.aggregate_usage(days=1)
                result["today_tokens"] = f"{today['total_tokens_m']}M"
                result["today_sessions"] = str(today["sessions"])

                week = usage_mod.aggregate_usage(days=7)
                result["week_tokens"] = f"{week['total_tokens_m']}M"
                result["week_sessions"] = str(week["sessions"])

                all_time = usage_mod.aggregate_usage()
                result["total_tokens"] = f"{all_time['total_tokens_m']}M"
            except Exception:
                # Fallback: run as subprocess
                try:
                    r = subprocess.run(
                        ["python3", "-c", """
import json, sys
sys.path.insert(0, '/root/aria/scripts')
exec(open('/root/aria/scripts/aria-usage.py').read())
d = {
    "current": get_current_session_usage(),
    "today": aggregate_usage(days=1),
    "week": aggregate_usage(days=7),
    "total": aggregate_usage()
}
print(json.dumps(d))
"""],
                        capture_output=True, text=True, timeout=10
                    )
                    if r.stdout.strip():
                        d = json.loads(r.stdout.strip())
                        if d.get("current"):
                            result["current_tokens"] = f"{d['current']['total_tokens_k']}k"
                        result["today_tokens"] = f"{d['today']['total_tokens_m']}M"
                        result["today_sessions"] = str(d["today"]["sessions"])
                        result["week_tokens"] = f"{d['week']['total_tokens_m']}M"
                        result["week_sessions"] = str(d["week"]["sessions"])
                        result["total_tokens"] = f"{d['total']['total_tokens_m']}M"
                except Exception:
                    pass
        except Exception:
            pass
        return result

    def _get_linear(self):
        result = {"in_progress": [], "todo": [], "overdue": [], "done": []}
        try:
            import urllib.request as ur
            from datetime import datetime, timezone

            linear_key = _read_env('LINEAR_API_KEY')
            if not linear_key:
                result["error"] = "LINEAR_API_KEY fehlt"
                return result

            query = json.dumps({"query": f"""
{{
  open: issues(filter: {{team: {{key: {{eq: "{LINEAR_TEAM_KEY}"}}}}, state: {{type: {{nin: ["completed","canceled"]}}}}}}, first: 100, orderBy: updatedAt) {{
    nodes {{ id identifier title state {{ name type }} dueDate updatedAt }}
  }}
  recentDone: issues(filter: {{team: {{key: {{eq: "{LINEAR_TEAM_KEY}"}}}}, state: {{type: {{eq: "completed"}}}}}}, first: 15, orderBy: updatedAt) {{
    nodes {{ id identifier title state {{ name type }} dueDate updatedAt }}
  }}
}}"""})

            req = ur.Request(
                'https://api.linear.app/graphql',
                data=query.encode(),
                headers={'Authorization': linear_key, 'Content-Type': 'application/json'}
            )
            with ur.urlopen(req, timeout=8) as resp:
                data = json.loads(resp.read())

            open_issues = data.get('data', {}).get('open', {}).get('nodes', [])
            done_issues = data.get('data', {}).get('recentDone', {}).get('nodes', [])
            issues = open_issues + done_issues
            today = datetime.now(timezone.utc).date()

            # Filter out Linear boilerplate onboarding issues
            skip_ids = {f'{LINEAR_TEAM_KEY}-{i}' for i in range(1, 5)}

            for issue in issues:
                iid = issue['identifier']
                if iid in skip_ids:
                    continue
                title = issue['title']
                stype = issue['state']['type']
                due = issue.get('dueDate')

                is_overdue = False
                if due and stype != 'completed':
                    try:
                        due_date = datetime.fromisoformat(due).date()
                        is_overdue = due_date < today
                    except Exception:
                        pass

                extra = f"Deadline: {due}" if due and stype != 'completed' else ""
                item = {"id": iid, "title": title, "extra": extra, "internal_id": issue.get("id", "")}

                if is_overdue:
                    result["overdue"].append(item)
                elif stype == 'started':
                    result["in_progress"].append(item)
                elif stype in ('unstarted', 'backlog'):
                    result["todo"].append(item)
                elif stype == 'completed':
                    result["done"].append(item)

            # Done: only last 10 (sorted by updatedAt DESC already from query)
            result["done"] = result["done"][:10]

        except Exception as e:
            result["error"] = str(e)
        return result

    def _start_linear_issue(self, internal_id, display_id):
        try:
            import urllib.request as ur
            linear_key = _read_env('LINEAR_API_KEY')
            if not linear_key:
                return {"ok": False, "message": "LINEAR_API_KEY fehlt"}
            # Get the "In Progress" / started state ID for the configured team
            q1 = json.dumps({"query": f'{{ workflowStates(filter: {{team: {{key: {{eq: "{LINEAR_TEAM_KEY}"}}}}, type: {{eq: "started"}}}}) {{ nodes {{ id name }} }} }}'})
            req = ur.Request('https://api.linear.app/graphql', data=q1.encode(),
                             headers={'Authorization': linear_key, 'Content-Type': 'application/json'})
            with ur.urlopen(req, timeout=8) as resp:
                states = json.loads(resp.read())
            nodes = states.get('data', {}).get('workflowStates', {}).get('nodes', [])
            if not nodes:
                return {"ok": False, "message": "In-Progress-State nicht gefunden"}
            started_state_id = nodes[0]['id']
            mutation = json.dumps({"query": f'mutation {{ issueUpdate(id: "{internal_id}", input: {{stateId: "{started_state_id}"}}) {{ success }} }}'})
            req2 = ur.Request('https://api.linear.app/graphql', data=mutation.encode(),
                              headers={'Authorization': linear_key, 'Content-Type': 'application/json'})
            with ur.urlopen(req2, timeout=8) as resp2:
                result = json.loads(resp2.read())
            success = result.get('data', {}).get('issueUpdate', {}).get('success', False)
            if success:
                # Also notify via Telegram
                self._send_telegram(f"▶ Task gestartet: {display_id}\nSchreib mir was ich tun soll oder sag \"mach {display_id}\"")
            return {"ok": success, "message": "Gestartet" if success else "Update fehlgeschlagen"}
        except Exception as e:
            return {"ok": False, "message": str(e)}

    def _complete_linear_issue(self, internal_id):
        try:
            import urllib.request as ur
            linear_key = _read_env('LINEAR_API_KEY')
            if not linear_key:
                return {"ok": False, "message": "LINEAR_API_KEY fehlt"}

            # Step 1: Get the Done state ID for the configured team
            q1 = json.dumps({"query": f'{{ workflowStates(filter: {{team: {{key: {{eq: "{LINEAR_TEAM_KEY}"}}}}, type: {{eq: "completed"}}}}) {{ nodes {{ id name }} }} }}'})
            req = ur.Request('https://api.linear.app/graphql', data=q1.encode(),
                             headers={'Authorization': linear_key, 'Content-Type': 'application/json'})
            with ur.urlopen(req, timeout=8) as resp:
                states = json.loads(resp.read())
            nodes = states.get('data', {}).get('workflowStates', {}).get('nodes', [])
            if not nodes:
                return {"ok": False, "message": "Done-State nicht gefunden"}
            done_state_id = nodes[0]['id']

            # Step 2: Update the issue to Done
            mutation = json.dumps({"query": f'mutation {{ issueUpdate(id: "{internal_id}", input: {{stateId: "{done_state_id}"}}) {{ success }} }}'})
            req2 = ur.Request('https://api.linear.app/graphql', data=mutation.encode(),
                              headers={'Authorization': linear_key, 'Content-Type': 'application/json'})
            with ur.urlopen(req2, timeout=8) as resp2:
                result = json.loads(resp2.read())
            success = result.get('data', {}).get('issueUpdate', {}).get('success', False)
            return {"ok": success, "message": "Done" if success else "Update fehlgeschlagen"}
        except Exception as e:
            return {"ok": False, "message": str(e)}

    def _get_calendar(self):
        result = {"events": [], "date": ""}
        try:
            import urllib.request as ur
            from datetime import datetime, timezone, timedelta

            with open("/root/.google_credentials.json") as f:
                creds = json.load(f)
            access_token = creds.get("access_token", "")

            now = datetime.now(timezone.utc)
            today = now.strftime("%Y-%m-%d")
            time_min = today + "T00:00:00Z"
            time_max = today + "T23:59:59Z"
            result["date"] = today

            url = (
                "https://www.googleapis.com/calendar/v3/calendars/primary/events"
                f"?timeMin={time_min}&timeMax={time_max}&singleEvents=true&orderBy=startTime"
            )
            req = ur.Request(url, headers={"Authorization": f"Bearer {access_token}"})
            with ur.urlopen(req, timeout=8) as resp:
                data = json.loads(resp.read())

            for item in data.get("items", []):
                start = item.get("start", {})
                start_str = start.get("dateTime", start.get("date", ""))
                if "T" in start_str:
                    try:
                        dt = datetime.fromisoformat(start_str.replace("Z", "+00:00"))
                        dt_local = dt + timedelta(hours=2)  # CEST
                        time_fmt = dt_local.strftime("%H:%M")
                    except Exception:
                        time_fmt = start_str[11:16]
                else:
                    time_fmt = "ganztags"

                result["events"].append({
                    "title": item.get("summary", "Kein Titel"),
                    "time": time_fmt,
                    "location": item.get("location", "")
                })

        except Exception as e:
            result["error"] = str(e)
        return result

    def _get_tasks(self):
        try:
            if os.path.exists(PLANER_TASKS_FILE):
                with open(PLANER_TASKS_FILE, encoding="utf-8") as f:
                    return {"tasks": json.load(f)}
        except Exception:
            pass
        return {"tasks": []}

    def _save_task(self, task):
        try:
            tasks = []
            if os.path.exists(PLANER_TASKS_FILE):
                with open(PLANER_TASKS_FILE, encoding="utf-8") as f:
                    tasks = json.load(f)
            # Update or insert
            idx = next((i for i, t in enumerate(tasks) if t.get("id") == task.get("id")), -1)
            if idx >= 0:
                tasks[idx] = task
            else:
                tasks.append(task)
            with open(PLANER_TASKS_FILE, "w", encoding="utf-8") as f:
                json.dump(tasks, f, ensure_ascii=False, indent=2)
            return {"ok": True}
        except Exception as e:
            return {"ok": False, "error": str(e)}

    def _delete_task(self, task_id):
        try:
            tasks = []
            if os.path.exists(PLANER_TASKS_FILE):
                with open(PLANER_TASKS_FILE, encoding="utf-8") as f:
                    tasks = json.load(f)
            tasks = [t for t in tasks if t.get("id") != task_id]
            with open(PLANER_TASKS_FILE, "w", encoding="utf-8") as f:
                json.dump(tasks, f, ensure_ascii=False, indent=2)
            return {"ok": True}
        except Exception as e:
            return {"ok": False, "error": str(e)}

    def _get_n8n(self):
        result = {"workflows": []}
        try:
            api_key = ""
            with open("/root/aria/.env") as f:
                for line in f:
                    line = line.strip().lstrip("export ")
                    if line.startswith("N8N_API_KEY="):
                        api_key = line.split("=", 1)[1].strip().strip('"\'')
                        break
            import urllib.request as ur
            req = ur.Request(
                "https://n8n-vlfw.${VPS_HOST}/api/v1/workflows?limit=50",
                headers={"X-N8N-API-KEY": api_key}
            )
            with ur.urlopen(req, timeout=8) as resp:
                data = json.loads(resp.read())
            for wf in data.get("data", []):
                result["workflows"].append({
                    "name": wf.get("name", ""),
                    "active": wf.get("active", False),
                    "id": wf.get("id", "")
                })
        except Exception as e:
            result["error"] = str(e)
        return result

    def _get_social(self):
        result = {"posted": [], "upcoming": []}
        try:
            import re
            cal_path = "/root/aria/brain/01-Projekte/Social-Media-Content-Kalender.md"
            with open(cal_path, encoding="utf-8") as f:
                content = f.read()
            # Find table rows with status
            for line in content.split('\n'):
                if line.startswith('|') and '---' not in line and 'Datum' not in line and 'Status' not in line:
                    cells = [c.strip() for c in line.split('|') if c.strip()]
                    if len(cells) >= 5:
                        date, time_str, platform, post_num, post_content = cells[0], cells[1], cells[2], cells[3], cells[4]
                        status = cells[5] if len(cells) > 5 else ""
                        if date and platform and ('✅' in status or '⏳' in status or 'Gepostet' in status or 'Ausstehend' in status):
                            entry = {"date": date, "time": time_str, "platform": platform, "content": post_content, "status": status}
                            if '✅' in status or 'Gepostet' in status:
                                result["posted"].append(entry)
                            elif '⏳' in status or 'Ausstehend' in status:
                                result["upcoming"].append(entry)
            # Show last 3 posted, next 5 upcoming
            result["posted"] = result["posted"][-3:]
            result["upcoming"] = result["upcoming"][:5]
        except Exception as e:
            result["error"] = str(e)
        return result

    def _get_news(self):
        """External AI-news headlines from RSS feeds (TechCrunch AI, HuggingFace).
        Aria's own findings live in `_get_erkenntnisse()` instead — Kais wants
        the two streams visually separate (10.05.2026)."""
        result = {"items": []}
        try:
            import urllib.request as ur
            import xml.etree.ElementTree as ET

            feeds = [
                ("TechCrunch AI", "https://techcrunch.com/category/artificial-intelligence/feed/"),
                ("HuggingFace",   "https://huggingface.co/blog/feed.xml"),
            ]
            all_items = []
            for source, url in feeds:
                try:
                    req = ur.Request(url, headers={"User-Agent": "Mozilla/5.0"})
                    with ur.urlopen(req, timeout=6) as resp:
                        xml_data = resp.read()
                    root = ET.fromstring(xml_data)
                    ns = {"atom": "http://www.w3.org/2005/Atom"}
                    items = root.findall('.//item') or root.findall('.//atom:entry', ns) or root.findall('.//entry')
                    for item in items[:4]:
                        title = (item.findtext('title')
                                 or item.findtext('{http://www.w3.org/2005/Atom}title') or '').strip()
                        pub = (item.findtext('pubDate')
                               or item.findtext('published')
                               or item.findtext('{http://www.w3.org/2005/Atom}published') or '')
                        date_str = ''
                        if pub:
                            if pub[0].isdigit():
                                date_str = pub[:10]
                            else:
                                parts = pub.split()
                                if len(parts) >= 4:
                                    months = {"Jan":"01","Feb":"02","Mar":"03","Apr":"04","May":"05","Jun":"06",
                                              "Jul":"07","Aug":"08","Sep":"09","Oct":"10","Nov":"11","Dec":"12"}
                                    mo = months.get(parts[2], parts[2])
                                    day = parts[1].zfill(2)
                                    date_str = f"{parts[3]}-{mo}-{day}"
                        all_items.append({"source": source, "title": title, "date": date_str})
                except Exception:
                    pass

            all_items.sort(key=lambda x: x.get("date", ""), reverse=True)
            result["items"] = all_items[:8]
        except Exception as e:
            result["error"] = str(e)
        return result

    def _get_erkenntnisse(self):
        """Aria's own findings, in plain words. Sources (priority order):
        1. AI-Radar funde — `02-Wissen/ai-radar/funde/YYYY-MM-DD-*.md` with frontmatter
        2. Recent Daily-Logs — last 3 days from `06-Daily/`
        Items have shape: {source, title, summary, date}.
        """
        result = {"items": []}
        items = []
        try:
            import re
            funde_dir = "/root/aria/brain/02-Wissen/ai-radar/funde"
            if os.path.isdir(funde_dir):
                files = []
                for name in os.listdir(funde_dir):
                    if not name.endswith(".md") or name.startswith("_"):
                        continue
                    path = os.path.join(funde_dir, name)
                    files.append((path, os.path.getmtime(path)))
                files.sort(key=lambda x: x[1], reverse=True)
                for path, _mtime in files[:6]:
                    try:
                        with open(path, "r", encoding="utf-8") as f:
                            content = f.read(4096)
                        title = ""
                        date = ""
                        summary = ""
                        if content.startswith("---"):
                            end = content.find("\n---", 3)
                            if end > 0:
                                head = content[3:end]
                                for raw in head.split("\n"):
                                    line = raw.strip()
                                    if line.startswith("title:"):
                                        title = line.split(":", 1)[1].strip().strip('"').strip("'")
                                    elif line.startswith("date:"):
                                        date = line.split(":", 1)[1].strip()
                                    elif line.startswith("summary:") or line.startswith("description:"):
                                        summary = line.split(":", 1)[1].strip().strip('"').strip("'")
                                content = content[end + 4:]
                        if not title:
                            m = re.search(r"^#\s+(.+)$", content, re.MULTILINE)
                            if m:
                                title = m.group(1).strip()
                        if not date:
                            mname = re.match(r"(\d{4}-\d{2}-\d{2})", os.path.basename(path))
                            if mname:
                                date = mname.group(1)
                        if not summary:
                            for line in content.split("\n"):
                                line = line.strip()
                                if line and not line.startswith("#") and not line.startswith("---"):
                                    summary = line[:160]
                                    break
                        if title:
                            items.append({
                                "source": "AI-Radar",
                                "title": title,
                                "summary": summary,
                                "date": date,
                            })
                    except Exception:
                        continue

            if len(items) < 4:
                daily_dir = "/root/aria/brain/06-Daily"
                if os.path.isdir(daily_dir):
                    daily_files = []
                    for name in os.listdir(daily_dir):
                        if name.startswith("Daily-Log-") and name.endswith(".md"):
                            path = os.path.join(daily_dir, name)
                            daily_files.append((path, os.path.getmtime(path)))
                    daily_files.sort(key=lambda x: x[1], reverse=True)
                    for path, _mtime in daily_files[:3]:
                        try:
                            with open(path, "r", encoding="utf-8") as f:
                                content = f.read(2048)
                            base = os.path.basename(path)
                            mname = re.match(r"Daily-Log-(\d{4}-\d{2}-\d{2})", base)
                            date = mname.group(1) if mname else ""
                            body = content
                            if body.startswith("---"):
                                end = body.find("\n---", 3)
                                if end > 0:
                                    body = body[end + 4:]
                            summary = ""
                            for line in body.split("\n"):
                                stripped = line.strip()
                                if stripped and not stripped.startswith("#"):
                                    summary = stripped[:160]
                                    break
                            if summary:
                                items.append({
                                    "source": "Daily-Log",
                                    "title": f"Was lief am {date}",
                                    "summary": summary,
                                    "date": date,
                                })
                        except Exception:
                            continue

            items.sort(key=lambda x: x.get("date", ""), reverse=True)
            result["items"] = items[:6]
            if not result["items"]:
                result["items"] = [{
                    "source": "AI-Radar",
                    "title": "Noch keine Erkenntnisse — KAR-38 Sprint B baut die Pipeline",
                    "summary": "Sobald AI-Radar Funde erzeugt (X / Reddit / GitHub-Trending / HF), erscheinen sie hier.",
                    "date": "",
                }]
        except Exception as e:
            result["error"] = str(e)
        return result

    def _parse_skill_frontmatter(self, path):
        try:
            with open(path, 'r', encoding='utf-8') as f:
                head = f.read(4096)
            if not head.startswith("---"):
                return ""
            end = head.find("\n---", 3)
            if end <= 0:
                return ""
            for raw in head[3:end].split("\n"):
                line = raw.strip()
                if line.startswith("description:"):
                    val = line[len("description:"):].strip()
                    if val.startswith('"') and val.endswith('"'):
                        val = val[1:-1]
                    if val.startswith("'") and val.endswith("'"):
                        val = val[1:-1]
                    return val[:220]
            return ""
        except Exception:
            return ""

    def _get_quick_skills(self):
        """Liest /root/.aria-quick-skills.json. Kais kann Datei direkt editieren."""
        try:
            with open("/root/.aria-quick-skills.json") as f:
                skills = json.load(f)
            return {"ok": True, "skills": skills}
        except Exception as e:
            return {"ok": False, "error": str(e), "skills": []}

    def _trigger_quick_skill(self, skill_id):
        # Disabled 2026-05-04: Headless-claude-Subprocess kollidierte mit Live-Session
        # (Auth-Token-Race -> "Not logged in" -> Live-Session crash).
        return {"ok": False, "message": "Quick Skills sind deaktiviert."}
        try:
            with open("/root/.aria-quick-skills.json") as f:
                skills = json.load(f)
            skill = next((s for s in skills if s.get("id") == skill_id), None)
            if not skill:
                return {"ok": False, "message": f"Skill {skill_id} nicht gefunden"}
            prompt = skill.get("prompt", "")
            if not prompt:
                return {"ok": False, "message": "Kein Prompt definiert"}
            label = skill.get("label", skill_id)

            # Sofort-Bestaetigung an Kais
            ack_text = f"⚡ Quick Skill '{label}' gestartet. Antwort folgt sobald durch."
            self._send_telegram(ack_text)

            # Background-Spawn: claude headless mit dem Prompt
            # Nach Abschluss: Output via Bot-API an Kais
            import subprocess as _sp
            log_dir = "/root/.aria-quick-skill-logs"
            os.makedirs(log_dir, exist_ok=True)
            ts = int(time.time())
            log_file = os.path.join(log_dir, f"{ts}-{skill_id}.log")

            wrapper = f"""#!/bin/bash
export PATH="/root/.local/bin:/usr/local/bin:/usr/bin:/bin:$PATH"
cd /root
LOG="{log_file}"
echo "[$(date -u +%FT%TZ)] starting quick skill {skill_id}" > "$LOG"

# Run claude headless with prompt, capture output
OUT=$(claude --bare -p {self._sh_quote(prompt)} 2>&1 | tee -a "$LOG")
EXIT=$?
echo "[$(date -u +%FT%TZ)] claude exited $EXIT" >> "$LOG"

# Send result to Kais via Bot-API
TG_TOKEN=$(grep -oP '^TELEGRAM_BOT_TOKEN=\\K.*' /root/aria/.env 2>/dev/null)
TG_CHAT=$(grep -oP '^TELEGRAM_CHAT_ID=\\K.*' /root/aria/.env 2>/dev/null)
if [ -n "$TG_TOKEN" ] && [ -n "$TG_CHAT" ]; then
    # Truncate to last 3500 chars to fit Telegram limit
    BODY=$(echo "$OUT" | tail -c 3500)
    MSG="✅ Quick Skill '{label}' fertig (exit $EXIT):

$BODY"
    curl -sS -m 10 -X POST "https://api.telegram.org/bot${{TG_TOKEN}}/sendMessage" \\
        --data-urlencode "chat_id=${{TG_CHAT}}" \\
        --data-urlencode "text=${{MSG}}" >> "$LOG" 2>&1 || true
fi
echo "[$(date -u +%FT%TZ)] done" >> "$LOG"
"""
            wrapper_file = f"/tmp/quick-skill-{ts}.sh"
            with open(wrapper_file, "w") as f:
                f.write(wrapper)
            os.chmod(wrapper_file, 0o755)
            # Spawn background, fully detached
            _sp.Popen(["nohup", "bash", wrapper_file],
                      stdout=open(log_file + ".stdout", "w"),
                      stderr=_sp.STDOUT,
                      stdin=_sp.DEVNULL,
                      preexec_fn=os.setpgrp)

            return {
                "ok": True,
                "message": f"Quick Skill '{label}' gestartet (Headless), Antwort folgt per Telegram",
                "log": log_file,
            }
        except Exception as e:
            return {"ok": False, "message": str(e)}

    def _sh_quote(self, s):
        """Bash single-quote-escape for arbitrary strings."""
        return "'" + s.replace("'", "'\\''") + "'"

    def _get_brain_search(self, query, top_k=8):
        """Semantic search auf aria-brain via aria-brain-mcp (Ollama+sqlite-vec).
        Eingefuehrt 2026-05-04. Direkt-Import des Servers, kein stdio.
        """
        result = {"ok": False, "query": query, "hits": [], "error": None}
        if not query or len(query) < 2:
            result["error"] = "query zu kurz"
            return result
        try:
            import sys as _sys
            if "/root/aria/brain-mcp" not in _sys.path:
                _sys.path.insert(0, "/root/aria/brain-mcp")
            import server as brain_server
            conn = brain_server.get_conn()
            # semantic_search liefert (path, title, snippet, score) Tupel
            try:
                hits = brain_server.search_vault_semantic(conn, query, top_k=top_k)
                result["hits"] = [
                    {
                        "path": str(h.get("path", "")),
                        "title": str(h.get("title", "")),
                        "snippet": str(h.get("text") or h.get("snippet") or "")[:300],
                        "score": float(h.get("score", 0)),
                    }
                    for h in hits
                ]
            except Exception as e_inner:
                # Fallback: grep_vault_content
                try:
                    hits_grep = brain_server.grep_vault_content(query, limit=top_k)
                    result["hits"] = [
                        {
                            "path": str(h.get("path", "")),
                            "title": str(h.get("title", "")),
                            "snippet": str(h.get("line") or h.get("text") or "")[:300],
                            "score": 0.0,
                        }
                        for h in hits_grep
                    ]
                    result["fallback"] = "grep_vault_content"
                except Exception as e_grep:
                    result["error"] = f"semantic_search failed ({e_inner}), grep_vault failed ({e_grep})"
                    return result
            result["ok"] = True
        except Exception as e:
            result["error"] = str(e)
        return result

    def _get_brain_tree(self):
        """Liste der Top-Level-Folders im aria-brain Vault mit Datei-Counts."""
        result = {"ok": False, "folders": [], "total_files": 0, "vault_root": "/root/aria/brain"}
        try:
            vault = "/root/aria/brain"
            if not os.path.isdir(vault):
                result["error"] = "Vault nicht gefunden"
                return result
            entries = []
            total = 0
            for name in sorted(os.listdir(vault)):
                if name.startswith("."):
                    continue
                path = os.path.join(vault, name)
                if os.path.isdir(path):
                    count = 0
                    for root_dir, dirs, files in os.walk(path):
                        # skip hidden
                        dirs[:] = [d for d in dirs if not d.startswith(".")]
                        count += sum(1 for f in files if f.endswith(".md"))
                    entries.append({"name": name, "md_count": count, "type": "folder"})
                    total += count
                elif os.path.isfile(path) and name.endswith(".md"):
                    entries.append({"name": name, "md_count": 1, "type": "file"})
                    total += 1
            result["folders"] = entries
            result["total_files"] = total
            result["ok"] = True
        except Exception as e:
            result["error"] = str(e)
        return result

    def _get_brain_graph(self):
        """Build a graph from Wikilinks across all .md files in /root/aria/brain.
        Nodes = files (path, title, group=top-level folder).
        Edges = wikilink references (source -> target if target file exists).
        """
        import re as _re
        vault = "/root/aria/brain"
        result = {"ok": False, "nodes": [], "edges": []}
        try:
            if not os.path.isdir(vault):
                result["error"] = "Vault nicht gefunden"
                return result
            files = []
            for root_dir, dirs, fnames in os.walk(vault):
                dirs[:] = [d for d in dirs if not d.startswith(".")]
                for fn in fnames:
                    if fn.lower().endswith(".md"):
                        full = os.path.join(root_dir, fn)
                        rel = os.path.relpath(full, vault)
                        files.append((rel, full))
            # Build base name -> rel path lookup (case-insensitive)
            base_lookup = {}
            for rel, _ in files:
                base = os.path.basename(rel).rsplit(".", 1)[0].lower()
                if base not in base_lookup:
                    base_lookup[base] = rel
            wikilink_re = _re.compile(r"\[\[([^\]\|\n]+)(?:\|[^\]\n]+)?\]\]")
            nodes = []
            edges = []
            for rel, full in files:
                top = rel.split("/")[0] if "/" in rel else "ROOT"
                title = os.path.basename(rel).rsplit(".", 1)[0]
                nodes.append({"id": rel, "label": title, "group": top})
                try:
                    with open(full, "r", encoding="utf-8", errors="replace") as f:
                        text = f.read()
                except Exception:
                    continue
                seen = set()
                for m in wikilink_re.finditer(text):
                    target = m.group(1).strip()
                    norm = target.rsplit(".", 1)[0].lower() if target.lower().endswith(".md") else target.lower()
                    target_rel = base_lookup.get(norm)
                    if target_rel and target_rel != rel and (rel, target_rel) not in seen:
                        edges.append({"source": rel, "target": target_rel})
                        seen.add((rel, target_rel))
            result["nodes"] = nodes
            result["edges"] = edges
            result["ok"] = True
        except Exception as e:
            result["error"] = str(e)
        return result

    def _get_brain_files_tree(self):
        """Recursive tree of all .md files under /root/aria/brain.
        Returns nested folders with files. Skips hidden dirs and non-md files.
        """
        vault = "/root/aria/brain"
        result = {"ok": False, "tree": None, "vault_root": vault}
        try:
            if not os.path.isdir(vault):
                result["error"] = "Vault nicht gefunden"
                return result

            def build(path):
                node = {"name": os.path.basename(path) or "aria-brain", "type": "folder", "children": []}
                try:
                    entries = sorted(os.listdir(path))
                except Exception:
                    return node
                for name in entries:
                    if name.startswith("."):
                        continue
                    full = os.path.join(path, name)
                    if os.path.isdir(full):
                        sub = build(full)
                        if sub["children"]:
                            node["children"].append(sub)
                    elif os.path.isfile(full) and name.lower().endswith(".md"):
                        rel = os.path.relpath(full, vault)
                        node["children"].append({"name": name, "type": "file", "path": rel})
                return node

            result["tree"] = build(vault)
            result["ok"] = True
        except Exception as e:
            result["error"] = str(e)
        return result

    def _get_brain_file_content(self, rel_path):
        """Read a markdown file from /root/aria/brain. Path is relative to vault root.
        Strict whitelist: no path traversal, must end .md.
        """
        import urllib.parse as _up
        try:
            rel_path = _up.unquote(rel_path or "")
        except Exception:
            pass
        vault = os.path.realpath("/root/aria/brain")
        result = {"ok": False, "path": rel_path}
        try:
            if not rel_path or ".." in rel_path.split("/"):
                result["error"] = "invalid path"
                return result
            target = os.path.realpath(os.path.join(vault, rel_path))
            if not target.startswith(vault + os.sep) and target != vault:
                result["error"] = "outside vault"
                return result
            if not target.lower().endswith(".md"):
                result["error"] = "not a markdown file"
                return result
            if not os.path.isfile(target):
                result["error"] = "file not found"
                return result
            with open(target, "r", encoding="utf-8", errors="replace") as f:
                content = f.read()
            stat = os.stat(target)
            # Split YAML frontmatter from body so the Markdown renderer doesn't
            # show `title: …` lines as inline body text (Screenshot 17.05.2026).
            # A future Phase 3 step renders the parsed frontmatter as chips
            # above the body — for now it travels as a separate field so the
            # current renderer just sees clean Markdown.
            frontmatter_raw = ""
            body = content
            if content.startswith("---\n"):
                end = content.find("\n---\n", 4)
                if end != -1:
                    frontmatter_raw = content[4:end]
                    body = content[end + 5:].lstrip("\n")
            result["ok"] = True
            result["content"] = body
            result["frontmatter"] = frontmatter_raw
            result["size"] = stat.st_size
            result["mtime"] = stat.st_mtime
            result["title"] = os.path.basename(target).rsplit(".", 1)[0]
        except Exception as e:
            result["error"] = str(e)
        return result

    def _get_tool_failures(self, limit=50):
        """Read recent tool failures from Supabase aria_tool_failures.
        Eingefuehrt 2026-05-04 nach Architektur-Audit + PostToolUseFailure-Hook.
        """
        result = {
            "ok": False,
            "total_24h": 0,
            "by_tool": [],
            "recent": [],
            "error": None,
        }
        try:
            supa_url = _read_env("SUPABASE_URL")
            supa_key = _read_env("SUPABASE_SERVICE_ROLE_KEY")
            if not supa_url or not supa_key:
                result["error"] = "Supabase credentials missing"
                return result
            import urllib.request
            import urllib.parse
            # Recent failures
            q = f"{supa_url}/rest/v1/aria_tool_failures?select=created_at,tool_name,error_excerpt,is_mcp&order=created_at.desc&limit={int(limit)}"
            req = urllib.request.Request(q, headers={
                "apikey": supa_key,
                "Authorization": f"Bearer {supa_key}",
            })
            with urllib.request.urlopen(req, timeout=5) as resp:
                rows = json.loads(resp.read())
                result["recent"] = rows[:limit]
            # Aggregations: last 24h
            from datetime import datetime, timedelta, timezone
            cutoff = (datetime.now(timezone.utc) - timedelta(hours=24)).isoformat()
            agg_q = f"{supa_url}/rest/v1/aria_tool_failures?select=tool_name&created_at=gte.{urllib.parse.quote(cutoff)}"
            agg_req = urllib.request.Request(agg_q, headers={
                "apikey": supa_key,
                "Authorization": f"Bearer {supa_key}",
            })
            with urllib.request.urlopen(agg_req, timeout=5) as resp:
                rows_24h = json.loads(resp.read())
                result["total_24h"] = len(rows_24h)
                counts = {}
                for r in rows_24h:
                    n = r.get("tool_name", "unknown")
                    counts[n] = counts.get(n, 0) + 1
                result["by_tool"] = sorted(
                    [{"tool_name": k, "count": v} for k, v in counts.items()],
                    key=lambda x: -x["count"],
                )[:20]
            result["ok"] = True
        except Exception as e:
            result["error"] = str(e)
        return result

    def _get_cost_overview(self):
        """Aggregate Aria cost + revenue overview.
        Eingefuehrt 2026-05-04 nach Architektur-Audit. Pragmatisch: zeigt was wir aus
        verfuegbaren Quellen ziehen koennen, Anthropic-API-Cost ist N/A wegen Max-Plan.
        """
        result = {
            "currency": "EUR",
            "stripe": {
                "available_eur": None,
                "pending_eur": None,
                "active_subscriptions": None,
                "active_mrr_eur": None,
                "charges_30d_count": None,
                "charges_30d_succeeded_eur": None,
            },
            "estimated_costs_per_month": {
                "subscriptions_total_eur": 174,
                "vps_hostinger_eur": 9,
                "anthropic_max_plan_eur": "Pauschal (nicht trackbar)",
            },
            "notes": [
                "Anthropic Max Plan ist Pauschal, keine per-call Cost-Tracking moeglich.",
                "Subscriptions-Schaetzung 174 EUR aus 08-Finanzen/06-Gewerbe-Software.",
                "Stripe-Daten sind live aus Stripe-API (sofern STRIPE_API_KEY gesetzt).",
            ],
            "errors": [],
        }
        try:
            stripe_key = _read_env("STRIPE_API_KEY")
            if not stripe_key:
                result["errors"].append("STRIPE_API_KEY missing")
                return result
            import urllib.request
            from datetime import datetime, timedelta

            def stripe_get(path, timeout=5):
                req = urllib.request.Request(
                    f"https://api.stripe.com/v1{path}",
                    headers={"Authorization": f"Bearer {stripe_key}"},
                )
                with urllib.request.urlopen(req, timeout=timeout) as resp:
                    return json.loads(resp.read())

            # Balance
            try:
                b = stripe_get("/balance")
                avail = sum(x.get("amount", 0) for x in b.get("available", []))
                pend = sum(x.get("amount", 0) for x in b.get("pending", []))
                result["stripe"]["available_eur"] = avail / 100.0
                result["stripe"]["pending_eur"] = pend / 100.0
            except Exception as e:
                result["errors"].append(f"balance: {e}")

            # Active subscriptions / MRR
            try:
                s = stripe_get("/subscriptions?status=active&limit=100")
                subs = s.get("data", [])
                result["stripe"]["active_subscriptions"] = len(subs)
                mrr = 0
                for sub in subs:
                    for it in sub.get("items", {}).get("data", []):
                        plan = it.get("plan", {}) or {}
                        amount = plan.get("amount", 0)
                        interval = plan.get("interval", "month")
                        if interval == "month":
                            mrr += amount
                        elif interval == "year":
                            mrr += amount / 12
                result["stripe"]["active_mrr_eur"] = round(mrr / 100.0, 2)
            except Exception as e:
                result["errors"].append(f"subscriptions: {e}")

            # Charges 30 Tage
            try:
                created_after = int((datetime.now() - timedelta(days=30)).timestamp())
                ch = stripe_get(f"/charges?created[gte]={created_after}&limit=100")
                charges = ch.get("data", [])
                result["stripe"]["charges_30d_count"] = len(charges)
                # paid + status=succeeded
                succeeded = sum(
                    c.get("amount", 0)
                    for c in charges
                    if c.get("paid") and c.get("status") == "succeeded"
                )
                result["stripe"]["charges_30d_succeeded_eur"] = round(succeeded / 100.0, 2)
            except Exception as e:
                result["errors"].append(f"charges: {e}")

        except Exception as e:
            result["errors"].append(f"outer: {e}")

        return result

    def _get_capabilities(self):
        import glob
        result = {
            "plugins": [],
            "global_skills": [],
            "mcp_servers": [],
            "inactive_skill_repos": [],
        }

        # 1. Installed plugins + their skills
        try:
            with open("/root/.claude/plugins/installed_plugins.json") as f:
                data = json.load(f)
            for key, entries in data.get("plugins", {}).items():
                name = key.split("@")[0]
                marketplace = key.split("@", 1)[1] if "@" in key else ""
                for entry in entries:
                    install_path = entry.get("installPath", "")
                    skills = []
                    skills_dir = os.path.join(install_path, "skills")
                    if os.path.isdir(skills_dir):
                        for skill_name in sorted(os.listdir(skills_dir)):
                            skill_md = os.path.join(skills_dir, skill_name, "SKILL.md")
                            if os.path.isfile(skill_md):
                                skills.append({
                                    "name": skill_name,
                                    "description": self._parse_skill_frontmatter(skill_md),
                                })
                    last_updated = entry.get("lastUpdated", "") or entry.get("installedAt", "")
                    result["plugins"].append({
                        "name": name,
                        "marketplace": marketplace,
                        "version": entry.get("version", "?"),
                        "last_updated": last_updated[:10] if last_updated else "",
                        "skill_count": len(skills),
                        "skills": skills,
                    })
            result["plugins"].sort(key=lambda p: p["name"].lower())
        except Exception as e:
            result["plugins_error"] = str(e)

        # 2. Global / user skills (outside plugins)
        try:
            g = "/root/.claude/skills"
            if os.path.isdir(g):
                for skill_name in sorted(os.listdir(g)):
                    skill_md = os.path.join(g, skill_name, "SKILL.md")
                    if os.path.isfile(skill_md):
                        result["global_skills"].append({
                            "name": skill_name,
                            "description": self._parse_skill_frontmatter(skill_md),
                        })
        except Exception as e:
            result["global_skills_error"] = str(e)

        # 3. MCP servers aus .claude.json + installierten Plugins + Live-Ping
        try:
            mcp_entries = {}

            # (a) .claude.json top-level
            try:
                with open("/root/.claude.json") as f:
                    cfg = json.load(f)
                for key, val in (cfg.get("mcpServers") or {}).items():
                    if not isinstance(val, dict):
                        continue
                    mcp_entries[key] = {
                        "name": key,
                        "type": val.get("type", ""),
                        "url": val.get("url", ""),
                        "command": (val.get("command") or "")[:120],
                        "source": ".claude.json",
                    }
            except Exception:
                pass

            # (b) aus installierten Plugins (Telegram, GitHub, Supabase, Linear, Vercel, Figma, Stripe)
            #     — Plugin-Namen aus installed_plugins.json gelten gleichzeitig als aktive MCPs
            plugin_mcp_hints = ["telegram", "supabase", "vercel", "github", "figma", "linear", "stripe"]
            installed_keys = set()
            try:
                with open("/root/.claude/plugins/installed_plugins.json") as f:
                    pdata = json.load(f)
                for k in pdata.get("plugins", {}).keys():
                    installed_keys.add(k.split("@")[0])
            except Exception:
                pass
            for hint in plugin_mcp_hints:
                if hint in installed_keys:
                    label = f"plugin:{hint}"
                    if label not in mcp_entries:
                        mcp_entries[label] = {
                            "name": label,
                            "type": "plugin",
                            "url": "",
                            "command": "",
                            "source": "plugin",
                        }

            # Ping: http/https via HEAD/POST mit kurzem Timeout, jede Antwort < 500 = reachable
            import urllib.request as ur
            import socket
            for entry in mcp_entries.values():
                active = None
                url = entry.get("url", "")
                if entry.get("source") == "plugin":
                    active = True  # Plugin ist installiert -> geladen
                elif url.startswith("http"):
                    try:
                        req = ur.Request(url, method="POST", data=b'{"jsonrpc":"2.0","id":1,"method":"ping"}',
                                         headers={"Content-Type": "application/json",
                                                  "Accept": "application/json, text/event-stream"})
                        with ur.urlopen(req, timeout=2) as resp:
                            active = resp.status < 500
                    except Exception as e:
                        msg = str(e).lower()
                        if "401" in msg or "403" in msg or "404" in msg:
                            active = True  # Server antwortet, Auth/Route-Problem separat
                        elif "400" in msg or "405" in msg or "406" in msg:
                            active = True
                        else:
                            active = False
                elif entry.get("command"):
                    active = True  # Stdio-MCP: wenn konfiguriert, gilt er als aktiv
                else:
                    active = False
                entry["active"] = bool(active)

            result["mcp_servers"] = sorted(mcp_entries.values(), key=lambda m: m["name"].lower())
        except Exception as e:
            result["mcp_servers_error"] = str(e)

        # 4. Inactive skill repos: .claude/skills/ Ordner außerhalb der aktiven Claude-Config
        try:
            for path in sorted(glob.glob("/root/*/.claude/skills")):
                repo_dir = os.path.dirname(os.path.dirname(path))
                repo = os.path.basename(repo_dir)
                if repo == ".claude":
                    continue
                try:
                    skill_names = sorted(
                        d for d in os.listdir(path)
                        if os.path.isdir(os.path.join(path, d))
                    )
                except Exception:
                    skill_names = []
                result["inactive_skill_repos"].append({
                    "repo": repo,
                    "path": path,
                    "skill_count": len(skill_names),
                    "skills": skill_names[:50],
                })
        except Exception as e:
            result["inactive_skill_repos_error"] = str(e)

        return result

    def _restart_session(self):
        try:
            # aria-restart.sh nutzen - macht alles: kill, cleanup, wrapper, trust
            result = subprocess.run(
                ["bash", os.path.expanduser("~/aria/scripts/aria-restart.sh")],
                capture_output=True, text=True, timeout=120
            )
            if result.returncode == 0:
                return {"ok": True, "message": "Aria neu gestartet"}
            else:
                msg = result.stdout.strip().split('\n')[-1] if result.stdout else result.stderr.strip()
                return {"ok": False, "message": msg or "Restart fehlgeschlagen"}
        except subprocess.TimeoutExpired:
            return {"ok": False, "message": "Restart Timeout (120s)"}
        except Exception as e:
            return {"ok": False, "message": f"Fehler: {e}"}

    def _git_pull(self):
        try:
            r = subprocess.run(
                ["git", "-C", os.path.expanduser("~/aria/brain"), "pull"],
                capture_output=True, text=True, timeout=15
            )
            msg = r.stdout.strip() or r.stderr.strip()
            return {"ok": r.returncode == 0, "message": msg}
        except Exception as e:
            return {"ok": False, "message": str(e)}

    def log_message(self, format, *args):
        pass


class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    daemon_threads = True

if __name__ == "__main__":
    port = 9999
    bind = os.environ.get("ARIA_CONTROL_BIND", "0.0.0.0")
    print(f"Aria Control Panel: {bind}:{port}")
    print(f"Local URL: http://localhost:{port}/?t={TOKEN}")
    ThreadedHTTPServer((bind, port), Handler).serve_forever()
