recall-server 0.1.0

Recall's sync API: SQLite persistence, LLM-assisted merge, and the HTTP surface
Documentation
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Recall admin</title>
<style>
  :root {
    color-scheme: light dark;
    --bg: #ffffff;
    --fg: #1a1a1a;
    --muted: #6b7280;
    --border: #e5e7eb;
    --row-alt: #fafafa;
    --error: #b91c1c;
    --accent: #111827;
  }
  @media (prefers-color-scheme: dark) {
    :root {
      --bg: #14161a;
      --fg: #e8e8e8;
      --muted: #9aa0a6;
      --border: #2a2d33;
      --row-alt: #1b1d22;
      --error: #f87171;
      --accent: #e8e8e8;
    }
  }
  * { box-sizing: border-box; }
  body {
    margin: 0;
    padding: 2.5rem 1.5rem;
    background: var(--bg);
    color: var(--fg);
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  }
  main {
    max-width: 860px;
    margin: 0 auto;
  }
  h1 {
    font-size: 1.25rem;
    font-weight: 600;
    margin: 0 0 1.5rem;
  }
  .token-row {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 1.75rem;
  }
  input[type="password"], input[type="text"] {
    flex: 1;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--fg);
    font-size: 0.9rem;
  }
  button {
    padding: 0.5rem 1rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--accent);
    color: var(--bg);
    font-size: 0.9rem;
    cursor: pointer;
  }
  button:hover { opacity: 0.85; }
  .summary {
    color: var(--muted);
    font-size: 0.9rem;
    margin-bottom: 1.5rem;
    line-height: 1.6;
  }
  .error {
    color: var(--error);
    font-size: 0.9rem;
    margin-bottom: 1.5rem;
  }
  table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.9rem;
  }
  th, td {
    text-align: left;
    padding: 0.6rem 0.75rem;
    border-bottom: 1px solid var(--border);
  }
  th {
    color: var(--muted);
    font-weight: 500;
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
  }
  tbody tr:nth-child(even) { background: var(--row-alt); }
  .empty {
    color: var(--muted);
    font-size: 0.9rem;
  }
</style>
</head>
<body>
<main>
  <h1>Recall admin</h1>
  <div class="token-row">
    <input type="password" id="token" placeholder="Bearer token">
    <button id="load">Load</button>
  </div>
  <div id="summary" class="summary"></div>
  <div id="error" class="error" style="display:none"></div>
  <table id="table" style="display:none">
    <thead>
      <tr>
        <th>Project</th>
        <th>Active files</th>
        <th>Deleted</th>
        <th>Sources</th>
        <th>Last updated</th>
      </tr>
    </thead>
    <tbody id="tbody"></tbody>
  </table>
</main>
<script>
(function () {
  var STORAGE_KEY = "recall_admin_token";
  var tokenInput = document.getElementById("token");
  var loadBtn = document.getElementById("load");
  var summaryEl = document.getElementById("summary");
  var errorEl = document.getElementById("error");
  var tableEl = document.getElementById("table");
  var tbodyEl = document.getElementById("tbody");

  function clearChildren(el) {
    while (el.firstChild) el.removeChild(el.firstChild);
  }

  function showError(message) {
    tableEl.style.display = "none";
    clearChildren(summaryEl);
    errorEl.textContent = message;
    errorEl.style.display = "block";
  }

  function hideError() {
    errorEl.style.display = "none";
    errorEl.textContent = "";
  }

  function renderSummary(data) {
    clearChildren(summaryEl);
    var totals = data.totals || {};
    var parts = [
      "Projects: " + totals.project_count,
      "Active files: " + totals.file_count,
      "Deleted: " + totals.deleted_count,
      "git_commit: " + data.git_commit,
      "last_backup_at: " + (data.last_backup_at || "never"),
    ];
    parts.forEach(function (text, i) {
      if (i > 0) summaryEl.appendChild(document.createTextNode(" ยท "));
      summaryEl.appendChild(document.createTextNode(text));
    });
  }

  function renderTable(projects) {
    clearChildren(tbodyEl);
    if (!projects.length) {
      var tr = document.createElement("tr");
      var td = document.createElement("td");
      td.colSpan = 5;
      td.className = "empty";
      td.textContent = "No projects yet.";
      tr.appendChild(td);
      tbodyEl.appendChild(tr);
      tableEl.style.display = "";
      return;
    }
    projects.forEach(function (p) {
      var tr = document.createElement("tr");

      var tdKey = document.createElement("td");
      tdKey.textContent = p.project_key;
      tr.appendChild(tdKey);

      var tdActive = document.createElement("td");
      tdActive.textContent = String(p.file_count);
      tr.appendChild(tdActive);

      var tdDeleted = document.createElement("td");
      tdDeleted.textContent = String(p.deleted_count);
      tr.appendChild(tdDeleted);

      var tdSources = document.createElement("td");
      tdSources.textContent = (p.sources || []).join(", ");
      tr.appendChild(tdSources);

      var tdUpdated = document.createElement("td");
      tdUpdated.textContent = p.last_updated_at || "";
      tr.appendChild(tdUpdated);

      tbodyEl.appendChild(tr);
    });
    tableEl.style.display = "";
  }

  function load() {
    var token = tokenInput.value.trim();
    if (!token) {
      showError("enter a token");
      return;
    }
    hideError();
    fetch("/admin/stats", { headers: { Authorization: "Bearer " + token } })
      .then(function (resp) {
        if (resp.status === 401) {
          showError("invalid token");
          return null;
        }
        if (!resp.ok) {
          showError("request failed (" + resp.status + ")");
          return null;
        }
        // Only persist a token that's actually been proven valid.
        sessionStorage.setItem(STORAGE_KEY, token);
        return resp.json();
      })
      .then(function (data) {
        if (!data) return;
        renderSummary(data);
        renderTable(data.projects || []);
      })
      .catch(function () {
        showError("request failed");
      });
  }

  loadBtn.addEventListener("click", load);
  tokenInput.addEventListener("keydown", function (e) {
    if (e.key === "Enter") load();
  });

  var stored = sessionStorage.getItem(STORAGE_KEY);
  if (stored) {
    tokenInput.value = stored;
    load();
  }
})();
</script>
</body>
</html>