<?php
declare(strict_types=1);

$allowedIp = '10.10.10.3';
if (($_SERVER['REMOTE_ADDR'] ?? '') !== $allowedIp) {
    http_response_code(403);
    header('Content-Type: text/plain; charset=utf-8');
    header('Cache-Control: no-store');
    echo "forbidden\n";
    exit;
}

function no_cache(): void {
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
    header('Pragma: no-cache');
}

function text(string $path, string $default = ''): string {
    $v = @file_get_contents($path);
    return $v === false ? $default : trim($v);
}

function int_text(string $path): int {
    $v = text($path, '0');
    return preg_match('/^-?\d+$/', $v) ? (int)$v : 0;
}

function number_text(string $path): float {
    $v = text($path, '0');
    return preg_match('/^-?\d+$/', $v) ? (float)$v : 0.0;
}

function run_cmd(string $cmd): string {
    $out = @shell_exec($cmd);
    return is_string($out) ? $out : '';
}

function json_out(array $data): void {
    no_cache();
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
    exit;
}

function cpu_percent(): int {
    $line = strtok(text('/proc/stat'), "\n");
    $parts = preg_split('/\s+/', trim((string)$line));
    if (!$parts || $parts[0] !== 'cpu') return 0;

    $nums = array_map('intval', array_slice($parts, 1));
    $idle = ($nums[3] ?? 0) + ($nums[4] ?? 0);
    $total = array_sum($nums);
    $stateFile = '/tmp/php-lite-dashboard-cpu.json';
    $prev = @json_decode((string)@file_get_contents($stateFile), true);
    @file_put_contents($stateFile, json_encode(['total' => $total, 'idle' => $idle]));

    if (!is_array($prev) || !isset($prev['total'], $prev['idle'])) return 0;
    $dt = max(1, $total - (int)$prev['total']);
    $di = max(0, $idle - (int)$prev['idle']);
    $pct = (int)round((1 - ($di / $dt)) * 100);
    return max(0, min(100, $pct));
}

function mem_percent(): int {
    $info = text('/proc/meminfo');
    preg_match('/^MemTotal:\s+(\d+)/m', $info, $total);
    preg_match('/^MemAvailable:\s+(\d+)/m', $info, $available);
    $t = (int)($total[1] ?? 0);
    $a = (int)($available[1] ?? 0);
    if ($t <= 0) return 0;
    return max(0, min(100, (int)round((($t - $a) / $t) * 100)));
}

function fmt_bytes(float $bytes): string {
    $units = ['B', 'KB', 'MB', 'GB', 'TB'];
    $n = max(0.0, $bytes);
    $i = 0;
    while ($n >= 1024 && $i < count($units) - 1) {
        $n /= 1024;
        $i++;
    }
    return $i === 0 ? sprintf('%.0f %s', $n, $units[$i]) : sprintf('%.1f %s', $n, $units[$i]);
}

function seconds_human(int $seconds): string {
    $days = intdiv($seconds, 86400);
    $seconds %= 86400;
    $hours = intdiv($seconds, 3600);
    $seconds %= 3600;
    $minutes = intdiv($seconds, 60);
    if ($days > 0) return "{$days}d {$hours}h";
    if ($hours > 0) return "{$hours}h {$minutes}m";
    return "{$minutes}m";
}

function net_counters(string $ifname): array {
    $base = "/sys/class/net/$ifname/statistics";
    return [
        'rx' => number_text("$base/rx_bytes"),
        'tx' => number_text("$base/tx_bytes"),
    ];
}

function iface_row(string $name): array {
    return [
        'name' => $name,
        'state' => text("/sys/class/net/$name/operstate", '-'),
        'carrier' => text("/sys/class/net/$name/carrier", '-') === '1',
        'speed' => int_text("/sys/class/net/$name/speed"),
        'mac' => text("/sys/class/net/$name/address", '-'),
        'rx' => number_text("/sys/class/net/$name/statistics/rx_bytes"),
        'tx' => number_text("/sys/class/net/$name/statistics/tx_bytes"),
    ];
}

function interfaces(): array {
    $wanted = ['wan', 'br-lan', 'lan1', 'lan2', 'lan3', 'lan4', 'wg1'];
    $rows = [];
    foreach ($wanted as $name) {
        if (is_dir("/sys/class/net/$name")) $rows[] = iface_row($name);
    }
    return $rows;
}

function wan_info(): array {
    $ip = trim(run_cmd("ip -4 addr show dev wan 2>/dev/null | awk '/inet / {print \$2; exit}'"));
    $gw = trim(run_cmd("ip route show default 2>/dev/null | awk '/dev wan/ {print \$3; exit}'"));
    return [
        'ip' => $ip !== '' ? $ip : '-',
        'gateway' => $gw !== '' ? $gw : '-',
        'link' => text('/sys/class/net/wan/speed', '-') . 'M',
    ];
}

function ip_key(string $ip): string {
    $p = array_map('intval', explode('.', $ip));
    return sprintf('%03d%03d%03d%03d', $p[0] ?? 0, $p[1] ?? 0, $p[2] ?? 0, $p[3] ?? 0);
}

function current_devices(): array {
    $leases = [];
    foreach (explode("\n", text('/tmp/dhcp.leases')) as $line) {
        $p = preg_split('/\s+/', trim($line));
        if (count($p) >= 4 && preg_match('/^192\.168\.1\.\d+$/', $p[2])) {
            $leases[$p[2]] = ['mac' => strtolower($p[1]), 'name' => $p[3] !== '*' ? $p[3] : '-'];
        }
    }

    $rows = [
        '192.168.1.1' => ['ip' => '192.168.1.1', 'name' => 'router', 'mac' => '60:cf:84:f4:3f:18', 'state' => 'LOCAL'],
    ];
    $neigh = run_cmd('ip neigh show dev br-lan 2>/dev/null');
    foreach (explode("\n", $neigh) as $line) {
        $line = trim($line);
        if (!preg_match('/^(192\.168\.1\.\d+)\s+/', $line, $m)) continue;
        if (preg_match('/\b(FAILED|INCOMPLETE)\b/', $line)) continue;
        $ip = $m[1];
        preg_match('/lladdr\s+([0-9a-f:]+)/i', $line, $mac);
        preg_match('/\b(REACHABLE|STALE|DELAY|PROBE|PERMANENT|NOARP)\b/', $line, $state);
        $rows[$ip] = [
            'ip' => $ip,
            'name' => $leases[$ip]['name'] ?? '-',
            'mac' => strtolower($mac[1] ?? ($leases[$ip]['mac'] ?? '-')),
            'state' => $state[1] ?? 'seen',
        ];
    }

    $out = array_values($rows);
    usort($out, fn($a, $b) => strcmp(ip_key($a['ip']), ip_key($b['ip'])));
    return $out;
}

function tracked_devices(): array {
    $now = time();
    $active = current_devices();
    $stateFile = '/tmp/php-lite-dashboard-seen.json';
    $seen = @json_decode((string)@file_get_contents($stateFile), true);
    if (!is_array($seen)) $seen = [];

    foreach ($active as $d) {
        $ip = $d['ip'];
        if (!isset($seen[$ip]) || !is_array($seen[$ip])) {
            $seen[$ip] = ['first' => $now];
        }
        $seen[$ip]['last'] = $now;
        $seen[$ip]['name'] = $d['name'];
        $seen[$ip]['mac'] = $d['mac'];
        $seen[$ip]['state'] = $d['state'];
    }

    foreach ($seen as $ip => $d) {
        if (($d['last'] ?? 0) < $now - 604800) unset($seen[$ip]);
    }
    @file_put_contents($stateFile, json_encode($seen));

    $activeIps = array_flip(array_column($active, 'ip'));
    foreach ($active as &$d) {
        $d['online'] = true;
        $d['since'] = seconds_human($now - (int)($seen[$d['ip']]['first'] ?? $now));
    }
    unset($d);

    $offline = [];
    foreach ($seen as $ip => $d) {
        if (isset($activeIps[$ip])) continue;
        $offline[] = [
            'ip' => $ip,
            'name' => $d['name'] ?? '-',
            'mac' => $d['mac'] ?? '-',
            'state' => 'OFFLINE',
            'online' => false,
            'since' => seconds_human($now - (int)($d['first'] ?? $now)),
            'lastSeen' => seconds_human($now - (int)($d['last'] ?? $now)) . ' ago',
        ];
    }
    usort($offline, fn($a, $b) => strcmp(ip_key($a['ip']), ip_key($b['ip'])));
    return ['online' => $active, 'offline' => $offline];
}

function usb_mount(): string {
    $block = run_cmd('block info 2>/dev/null');
    foreach (explode("\n", $block) as $line) {
        if (preg_match('/^\/dev\/sd.*\sMOUNT="([^"]+)"/', $line, $m) && str_starts_with($m[1], '/mnt/')) {
            return $m[1];
        }
    }
    return '';
}

function clean_rel(string $path): string {
    $path = '/' . ltrim($path, '/');
    $path = preg_replace('#/+#', '/', $path) ?: '/';
    if (preg_match('#(^|/)\.\.(/|$)#', $path)) return '/';
    return $path;
}

function safe_usb_path(string $rel): array {
    $mount = usb_mount();
    if ($mount === '') return ['', '', 'no usb mount'];
    $rel = clean_rel($rel);
    $full = realpath($mount . '/' . ltrim($rel, '/'));
    if ($full === false) return ['', $mount, 'not found'];
    if ($full !== $mount && !str_starts_with($full, $mount . '/')) return ['', $mount, 'bad path'];
    return [$full, $mount, ''];
}

function usb_list(string $rel): array {
    [$full, $mount, $err] = safe_usb_path($rel);
    if ($err !== '') return ['mount' => $mount, 'path' => '/', 'error' => $err, 'items' => []];
    if (!is_dir($full)) return ['mount' => $mount, 'path' => clean_rel($rel), 'error' => 'not a dir', 'items' => []];
    $items = [];
    foreach (@scandir($full) ?: [] as $name) {
        if ($name === '.' || $name === '..') continue;
        $p = $full . '/' . $name;
        $items[] = [
            'name' => $name,
            'dir' => is_dir($p),
            'size' => is_file($p) ? filesize($p) : 0,
            'mtime' => filemtime($p) ?: 0,
        ];
    }
    usort($items, fn($a, $b) => ($a['dir'] === $b['dir']) ? strcasecmp($a['name'], $b['name']) : ($a['dir'] ? -1 : 1));
    return ['mount' => $mount, 'path' => clean_rel($rel), 'error' => '', 'items' => $items];
}

function usb_info(): array {
    $mount = usb_mount();
    if ($mount === '') return ['mounted' => false];
    $free = @disk_free_space($mount) ?: 0;
    $total = @disk_total_space($mount) ?: 0;
    return [
        'mounted' => true,
        'mount' => $mount,
        'free' => fmt_bytes((float)$free),
        'total' => fmt_bytes((float)$total),
        'usedPct' => $total > 0 ? max(0, min(100, (int)round((1 - ($free / $total)) * 100))) : 0,
    ];
}

function handle_api(): void {
    $api = $_GET['api'] ?? '';
    if ($api === 'stats') {
        $up = (int)floor((float)explode(' ', text('/proc/uptime', '0'))[0]);
        json_out([
            'time' => date('H:i:s'),
            'cpu' => cpu_percent(),
            'mem' => mem_percent(),
            'load' => text('/proc/loadavg', '-'),
            'uptime' => seconds_human($up),
            'net' => [
                'wan' => net_counters('wan'),
                'lan' => net_counters('br-lan'),
                'wg' => net_counters('wg1'),
            ],
            'wan' => wan_info(),
            'usb' => usb_info(),
            'interfaces' => interfaces(),
        ]);
    }
    if ($api === 'devices') json_out(tracked_devices());
    if ($api === 'files') json_out(usb_list((string)($_GET['path'] ?? '/')));
    if ($api === 'read') {
        [$full, , $err] = safe_usb_path((string)($_GET['path'] ?? '/'));
        if ($err !== '' || !is_file($full)) json_out(['ok' => false, 'error' => $err ?: 'not a file']);
        if (filesize($full) > 1024 * 1024) json_out(['ok' => false, 'error' => 'file is bigger than 1 MB']);
        json_out(['ok' => true, 'path' => clean_rel((string)($_GET['path'] ?? '/')), 'text' => (string)@file_get_contents($full)]);
    }
    if ($api === 'save') {
        [$full, $mount, $err] = safe_usb_path((string)($_GET['path'] ?? '/'));
        if ($err !== '') json_out(['ok' => false, 'error' => $err]);
        if (!is_file($full) || filesize($full) > 1024 * 1024) json_out(['ok' => false, 'error' => 'not editable']);
        $body = file_get_contents('php://input');
        if ($body === false || strlen($body) > 1024 * 1024) json_out(['ok' => false, 'error' => 'too big']);
        $ok = @file_put_contents($full, $body, LOCK_EX);
        json_out(['ok' => $ok !== false, 'mount' => $mount]);
    }
    json_out(['error' => 'bad api']);
}

if (isset($_GET['api'])) handle_api();

if (isset($_GET['download'])) {
    [$full, , $err] = safe_usb_path((string)($_GET['path'] ?? '/'));
    if ($err !== '' || !is_file($full)) {
        http_response_code(404);
        echo "not found\n";
        exit;
    }
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="' . basename($full) . '"');
    header('Content-Length: ' . filesize($full));
    readfile($full);
    exit;
}

no_cache();
header('Content-Type: text/html; charset=utf-8');
?>
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <title>Router</title>
  <style>
    :root{color-scheme:dark;--bg:#0d1218;--panel:#151c24;--line:#263241;--text:#eef4fb;--muted:#93a1b1;--ok:#38c172;--warn:#f2b84b;--bad:#ef5350;--accent:#62a8ff}
    *{box-sizing:border-box}
    body{margin:0;background:var(--bg);color:var(--text);font:14px/1.35 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;letter-spacing:0}
    main{max-width:720px;margin:0 auto;padding:14px 12px 28px}
    header{display:flex;align-items:flex-end;justify-content:space-between;gap:10px;margin:2px 0 12px}
    h1{margin:0;font-size:22px}
    .muted{color:var(--muted)}
    .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
    .card{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:10px;min-width:0}
    .label{font-size:11px;text-transform:uppercase;color:var(--muted);margin-bottom:4px}
    .value{font-size:20px;font-weight:800;white-space:nowrap}
    .bar{height:7px;background:#0a0f14;border-radius:999px;overflow:hidden;margin-top:8px}
    .fill{height:100%;width:0;background:linear-gradient(90deg,var(--ok),var(--accent));transition:width .35s ease}
    canvas{width:100%;height:82px;background:#101720;border:1px solid var(--line);border-radius:8px;margin:8px 0 12px}
    .section{margin-top:12px}
    .section-title{display:flex;justify-content:space-between;align-items:center;margin:0 0 7px}
    .section-title h2{margin:0;font-size:15px}
    .list{display:grid;gap:7px}
    .row{display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:9px 10px}
    .row strong{font-size:16px}
    .sub{font-size:12px;color:var(--muted);overflow:hidden;text-overflow:ellipsis}
    .pill{border-radius:999px;padding:3px 7px;font-size:11px;background:#173b26;color:#8df5b5;white-space:nowrap}
    .pill.off{background:#3a2222;color:#ff9a9a}
    .two{display:grid;grid-template-columns:1fr 1fr;gap:8px}
    button,a.btn{appearance:none;border:1px solid var(--line);background:#1b2530;color:var(--text);border-radius:8px;padding:8px 10px;text-decoration:none;font:inherit;font-weight:700}
    button:active,a.btn:active{transform:translateY(1px)}
    .path{font-size:12px;color:var(--muted);word-break:break-all}
    .file-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
    .actions{display:flex;gap:6px}
    textarea{width:100%;min-height:58vh;resize:vertical;background:#0b1118;color:var(--text);border:1px solid var(--line);border-radius:8px;padding:10px;font:13px/1.35 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
    dialog{width:min(96vw,720px);border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--text);padding:10px}
    dialog::backdrop{background:rgba(0,0,0,.65)}
    @media(max-width:430px){main{padding:12px 10px 24px}.grid{grid-template-columns:repeat(2,1fr)}.value{font-size:18px}.two{grid-template-columns:1fr}.row{padding:8px}.actions button,.actions a{padding:7px 8px;font-size:12px}}
  </style>
</head>
<body>
<main>
  <header>
    <div><h1>Router</h1><div class="muted" id="tick">WG only · <?php echo htmlspecialchars($allowedIp, ENT_QUOTES); ?></div></div>
    <button onclick="loadFiles('/')">USB</button>
  </header>

  <div class="grid">
    <div class="card"><div class="label">CPU</div><div class="value" id="cpu">0%</div><div class="bar"><div class="fill" id="cpuFill"></div></div></div>
    <div class="card"><div class="label">MEM</div><div class="value" id="mem">0%</div><div class="bar"><div class="fill" id="memFill"></div></div></div>
    <div class="card"><div class="label">UPTIME</div><div class="value" id="uptime">-</div></div>
    <div class="card"><div class="label">WAN</div><div class="value" id="wanRate">0 KB/s</div><div class="sub" id="wanInfo">-</div></div>
    <div class="card"><div class="label">WG</div><div class="value" id="wgRate">0 KB/s</div><div class="sub">10.10.10.1</div></div>
    <div class="card"><div class="label">USB</div><div class="value" id="usbPct">-</div><div class="sub" id="usbInfo">-</div></div>
  </div>

  <canvas id="chart" width="680" height="120"></canvas>

  <section class="section">
    <div class="section-title"><h2>Online</h2><span class="muted" id="onlineCount">0</span></div>
    <div class="list" id="online"></div>
    <details class="section"><summary class="muted">Offline <span id="offlineCount">0</span></summary><div class="list" id="offline"></div></details>
  </section>

  <section class="section">
    <div class="section-title"><h2>Interfaces</h2></div>
    <div class="list" id="interfaces"></div>
  </section>

  <section class="section">
    <div class="section-title"><h2>USB files</h2><button id="upBtn">Up</button></div>
    <div class="path" id="path">/</div>
    <div class="list" id="files"></div>
  </section>
</main>

<dialog id="editor">
  <div class="section-title"><h2 id="editTitle">Edit</h2><button id="closeEdit">Close</button></div>
  <textarea id="editText" spellcheck="false"></textarea>
  <div class="section-title" style="margin-top:8px"><span class="muted" id="editState"></span><button id="saveEdit">Save</button></div>
</dialog>

<script>
const $ = id => document.getElementById(id);
const hist = {cpu:[], mem:[], wan:[], wg:[], lan:[]};
let lastNet = null, curPath = '/', editPath = '';

function setText(id, value){ const el=$(id); if(el.textContent!==String(value)) el.textContent=value; }
function setPct(id, pct){ $(id).style.width = Math.max(0, Math.min(100, pct)) + '%'; }
function fmtBytes(b){ const u=['B','KB','MB','GB','TB']; let n=b,i=0; while(n>=1024&&i<u.length-1){n/=1024;i++} return i?`${n.toFixed(1)} ${u[i]}`:`${n|0} ${u[i]}`; }
function esc(s){ return String(s ?? '').replace(/[&<>"']/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
function push(a,v){ a.push(v); while(a.length>70)a.shift(); }

function draw(){
  const c=$('chart'), ctx=c.getContext('2d'), w=c.width, h=c.height;
  ctx.clearRect(0,0,w,h); ctx.fillStyle='#101720'; ctx.fillRect(0,0,w,h);
  const lines=[['cpu','#62a8ff',100],['mem','#38c172',100],['wan','#f2b84b',Math.max(1,...hist.wan)],['wg','#ef5350',Math.max(1,...hist.wg)]];
  ctx.lineWidth=2;
  for(const [key,color,max] of lines){
    const a=hist[key]; if(a.length<2) continue;
    ctx.strokeStyle=color; ctx.beginPath();
    a.forEach((v,i)=>{ const x=i*(w/(69)); const y=h-8-(Math.min(v,max)/max)*(h-18); i?ctx.lineTo(x,y):ctx.moveTo(x,y); });
    ctx.stroke();
  }
}

async function loadStats(){
  const r = await fetch('?api=stats', {cache:'no-store'});
  const s = await r.json();
  setText('tick', `updated ${s.time} · WG only`);
  setText('cpu', `${s.cpu}%`); setPct('cpuFill', s.cpu);
  setText('mem', `${s.mem}%`); setPct('memFill', s.mem);
  setText('uptime', s.uptime);
  setText('wanInfo', `${s.wan.ip} · gw ${s.wan.gateway} · ${s.wan.link}`);
  setText('usbPct', s.usb.mounted ? `${s.usb.usedPct}%` : '-');
  setText('usbInfo', s.usb.mounted ? `${s.usb.free} free / ${s.usb.total}` : 'not mounted');

  const now = Date.now();
  if(lastNet){
    const dt = Math.max(0.1, (now-lastNet.t)/1000);
    const wan = ((s.net.wan.rx+s.net.wan.tx)-(lastNet.wan.rx+lastNet.wan.tx))/dt;
    const wg = ((s.net.wg.rx+s.net.wg.tx)-(lastNet.wg.rx+lastNet.wg.tx))/dt;
    const lan = ((s.net.lan.rx+s.net.lan.tx)-(lastNet.lan.rx+lastNet.lan.tx))/dt;
    setText('wanRate', `${fmtBytes(Math.max(0,wan))}/s`);
    setText('wgRate', `${fmtBytes(Math.max(0,wg))}/s`);
    push(hist.wan, Math.max(0,wan/1024)); push(hist.wg, Math.max(0,wg/1024)); push(hist.lan, Math.max(0,lan/1024));
  }
  lastNet = {t:now, wan:s.net.wan, wg:s.net.wg, lan:s.net.lan};
  push(hist.cpu, s.cpu); push(hist.mem, s.mem); draw();

  $('interfaces').innerHTML = s.interfaces.map(i=>`
    <div class="row"><div><strong>${esc(i.name)}</strong><div class="sub">${esc(i.mac)} · rx ${fmtBytes(i.rx)} · tx ${fmtBytes(i.tx)}</div></div>
    <span class="pill ${i.carrier?'':'off'}">${i.carrier ? (i.speed>0 ? i.speed+'M' : i.state) : 'down'}</span></div>`).join('');
}

function deviceHtml(d){
  return `<div class="row"><div><strong>${esc(d.ip)}</strong><div class="sub">${esc(d.name)} · ${esc(d.mac)} · in net ${esc(d.since)}${d.lastSeen?' · '+esc(d.lastSeen):''}</div></div><span class="pill ${d.online?'':'off'}">${esc(d.state)}</span></div>`;
}
async function loadDevices(){
  const r = await fetch('?api=devices', {cache:'no-store'}), d = await r.json();
  setText('onlineCount', d.online.length); setText('offlineCount', d.offline.length);
  $('online').innerHTML = d.online.map(deviceHtml).join('');
  $('offline').innerHTML = d.offline.map(deviceHtml).join('');
}

async function loadFiles(path=curPath){
  curPath = path || '/';
  const r = await fetch(`?api=files&path=${encodeURIComponent(curPath)}`, {cache:'no-store'});
  const d = await r.json();
  setText('path', d.path || '/');
  if(d.error){ $('files').innerHTML = `<div class="card muted">${esc(d.error)}</div>`; return; }
  $('files').innerHTML = d.items.map(it=>{
    const p = (d.path === '/' ? '/' : d.path + '/') + it.name;
    if(it.dir) return `<div class="row"><div class="file-name"><strong>${esc(it.name)}/</strong></div><button data-dir="${esc(p)}">Open</button></div>`;
    return `<div class="row"><div class="file-name"><strong>${esc(it.name)}</strong><div class="sub">${fmtBytes(it.size)}</div></div><div class="actions"><button data-edit="${esc(p)}">Edit</button><a class="btn" href="?download=1&path=${encodeURIComponent(p)}">Down</a></div></div>`;
  }).join('') || '<div class="card muted">empty</div>';
}

async function openEdit(path){
  editPath = path;
  $('editTitle').textContent = path;
  $('editState').textContent = 'loading';
  $('editText').value = '';
  $('editor').showModal();
  const r = await fetch(`?api=read&path=${encodeURIComponent(path)}`, {cache:'no-store'});
  const d = await r.json();
  if(!d.ok){ $('editState').textContent = d.error || 'error'; return; }
  $('editText').value = d.text;
  $('editState').textContent = 'ready';
}

document.addEventListener('click', e=>{
  const dir = e.target.closest('[data-dir]'); if(dir){ loadFiles(dir.dataset.dir); return; }
  const edit = e.target.closest('[data-edit]'); if(edit){ openEdit(edit.dataset.edit); return; }
});
$('upBtn').onclick = () => loadFiles(curPath.split('/').slice(0,-1).join('/') || '/');
$('closeEdit').onclick = () => $('editor').close();
$('saveEdit').onclick = async () => {
  $('editState').textContent = 'saving';
  const r = await fetch(`?api=save&path=${encodeURIComponent(editPath)}`, {method:'POST', body:$('editText').value});
  const d = await r.json();
  $('editState').textContent = d.ok ? 'saved' : (d.error || 'error');
};

loadStats(); loadDevices(); loadFiles('/');
setInterval(loadStats, 500);
setInterval(loadDevices, 2500);
</script>
</body>
</html>
