SPB Git

spb/localvm-research Public License

Running LLMs larger than memory on a consumer Mac — falsification-driven research: margin-gated deferred refinement, out-of-core verification on Apple Silicon. TR-01 published.

Python 63.2% JavaScript 23.5% CSS 11.8% Shell 0.9% Makefile 0.5%
24.1 KB · 472 lines javascript
Raw Blame History
1// ============================================================================2//  Project   : localvm-research3//  File      : web/server.js4//  Purpose   : Research showcase platform — routes and pages (English, light)5//  Author    : Simon-Pierre Boucher6//  Contact   : contact@spboucher.ai7//  Created   : 2026-08-128//  Modified  : 2026-08-129//  Platform  : macOS / Apple Silicon (arm64) — Node.js (deployed on MacLustr)10//  License   : All rights reserved (research code)11// ============================================================================12"use strict";1314const fs = require("fs");15const path = require("path");16const express = require("express");17const C = require("./lib/content");18const R = require("./lib/render");1920const app = express();21const PORT = process.env.PORT || 8120;2223app.use("/static", express.static(path.join(__dirname, "public"), { maxAge: "1h" }));24app.use(express.urlencoded({ extended: false, limit: "16kb" }));2526// ---- comments storage (data/ is excluded from deploy rsync — persistent) ----27const DATA_DIR = path.join(__dirname, "data");28const COMMENTS_FILE = path.join(DATA_DIR, "comments.json");29fs.mkdirSync(DATA_DIR, { recursive: true });3031function readComments() {32  try {33    return JSON.parse(fs.readFileSync(COMMENTS_FILE, "utf8"));34  } catch {35    return [];36  }37}38function saveComment(entry) {39  const all = readComments();40  all.push(entry);41  fs.writeFileSync(COMMENTS_FILE, JSON.stringify(all, null, 2));42}43const lastPostByIp = new Map();4445function page(res, opts) {46  res.send(R.layout({ ...opts, buildInfo: C.buildInfo() }));47}4849function metaChips(data) {50  if (!data || !Object.keys(data).length) return "";51  const fields = ["document", "author", "created", "modified", "status"];52  const chips = fields53    .filter((f) => data[f])54    .map((f) => `<span class="chip"><span class="chip-k">${f}</span>${R.esc(data[f])}</span>`)55    .join("");56  return chips ? `<div class="chips">${chips}</div>` : "";57}5859// ---------------------------------------------------------------- home60app.get("/", (req, res) => {61  const s = C.stats();62  const chart = C.expHChartData();63  const log = C.parseLogEntries().slice(-3).reverse();64  const phases = [65    ["Phase 1 — Literature sweep (§4.1–4.10)", "done", `${s.notes} theme notes · ${s.sources} sources`],66    ["Phase 2 — State-of-the-art map", C.exists("research/state_of_the_art.md") ? "done" : "pending", "technique taxonomy + overlap analysis"],67    ["Phase 3 — Research gaps", s.gaps ? "done" : "pending", `${s.gaps} falsifiable approaches (G01–G${String(s.gaps).padStart(2, "0")})`],68    ["Phase 4 — Candidate ranking", C.exists("research/candidate_ranking.md") ? "done" : "in progress", "10-axis scoring, 3–5 candidates"],69    ["Phases 5–6 — Framework & micro-experiments", s.experimentsDone ? "in progress" : "pending", `${s.experimentsDone}/${s.experiments} experiments completed`],70    ["Phase 7 — Candidate prototype", C.exists("experiments/candidate_01/analysis.md") ? "in progress" : "pending", "margin-gated deferred refinement, 1.7B + 32B runs"],71    ["Phase 11 — Novelty check", C.exists("research/novelty_check.md") ? "done" : "pending", "adversarial prior-art search, 45 sources"],72  ];73  const phaseHtml = phases74    .map(75      ([name, st, detail]) => `<li class="phase ${st.replace(" ", "-")}">76        <span class="phase-dot"></span><div><strong>${name}</strong><span class="phase-detail">${detail}</span></div>77        <span class="badge badge-${st.replace(" ", "-")}">${st}</span></li>`78    )79    .join("");80  const logHtml = log81    .map((e) => {82      const [when, ...rest] = e.title.split(" — ");83      const excerpt = R.markdownToHtml(e.body.split("\n").slice(0, 6).join("\n"), "research/LOG.md").html;84      return `<article class="log-entry">85      <time class="tl-time">${R.esc(rest.length ? when : "")}</time>86      <h3>${R.esc(rest.join(" — ") || when)}</h3>87      <div class="md md-compact">${excerpt}</div></article>`;88    })89    .join("");9091  const body = `92<section class="hero">93  <p class="kicker">Independent systems + ML research · Apple Silicon</p>94  <h1>Running LLMs larger than memory<br>on a consumer Mac</h1>95  <p class="lede">Can an existing pretrained model be transformed <em>post-training</em> into an execution96  representation whose instantaneous working set is dramatically smaller than the full checkpoint —97  while preserving most of its capabilities? This platform exposes the complete paper trail:98  hypotheses, notes, code, benchmarks, and raw results.</p>99  <p class="lede-eq"><code>total model size ≠ resident size ≠ bytes read per token ≠ parameters required for this token</code></p>100</section>101102<section class="tiles">103  <div class="tile"><span class="tile-value">${s.sources}</span><span class="tile-label">sources reviewed</span></div>104  <div class="tile"><span class="tile-value">${s.gaps}</span><span class="tile-label">research gaps identified</span></div>105  <div class="tile"><span class="tile-value">${s.experimentsDone}<span class="tile-denom">/${s.experiments}</span></span><span class="tile-label">experiments completed</span></div>106  <div class="tile"><span class="tile-value">${s.build.commits || "—"}</span><span class="tile-label">commits</span></div>107</section>108109<section class="split">110  <div class="card">111    <h2>First result — the SSD substrate is not the bottleneck</h2>112    <p>Experiment H measured this Mac's internal NVMe under genuinely cold-cache conditions.113    Random reads reach the ~13.1 GB/s device ceiling at 1 MiB blocks (QD 8), while114    4 KiB single-threaded reads manage only 67 MB/s — a 200× spread that dictates the115    weight-block layout contract: <strong>≥ 256 KiB blocks at QD ≥ 4</strong>. Saturated116    Metal GPU compute costs &lt; 5%.</p>117    ${R.ssdChartSvg(chart)}118    <a class="more" href="/experiments/expH_ssd_feasibility">Experiment H: hypothesis, method, analysis →</a>119  </div>120  <div class="card">121    <h2>Research phases</h2>122    <ul class="phases">${phaseHtml}</ul>123  </div>124</section>125126<section class="card">127  <div class="card-head-row"><h2>Latest from the research log</h2>128  <a class="more" href="/doc/research/LOG.md">Full research log →</a></div>129  <div class="log-grid">${logHtml}</div>130</section>`;131  page(res, { title: "Home", active: "Home", body });132});133134// ---------------------------------------------------------------- research index135app.get("/research", (req, res) => {136  const docs = [137    ["research/LOG.md", "Research log", "Append-only, auditable record of every question, experiment, result, and decision."],138    ["research/state_of_the_art.md", "State of the art", "Phase 2 — technique taxonomy across six families with overlap analysis."],139    ["research/research_gaps.md", "Research gaps", "Phase 3 — falsifiable approaches with kill-numbers, G01–G24."],140    ["research/candidate_ranking.md", "Candidate ranking", "Phase 4 — 10-axis scoring and selected prototype candidates."],141    ["research/bibliography.md", "Bibliography", "Every consulted source with URL and access date."],142    ["research/novelty_check.md", "Novelty check", "Phase 11 — final novelty verification (written last)."],143  ];144  const cards = docs145    .map(([rel, title, desc]) => {146      const ok = C.exists(rel);147      return `<a class="doc-card ${ok ? "" : "disabled"}" href="${ok ? "/doc/" + rel : "#"}">148        <h3>${title}</h3><p>${desc}</p>${ok ? "" : '<span class="badge badge-pending">not yet written</span>'}</a>`;149    })150    .join("");151  const notes = (C.listDir("research/notes") || [])152    .filter((f) => f.name.endsWith(".md") && f.name !== "README.md")153    .map((f) => {154      const md = C.readMarkdown(f.rel);155      const sources = md ? (md.content.match(/^- .*http/gm) || []).length : 0;156      const lines = md ? md.content.split("\n").length : 0;157      return `<a class="doc-card" href="/doc/${f.rel}"><h3>${R.esc(f.name.replace(".md", "").replace(/_/g, " "))}</h3>158        <p>Phase 1 literature notes — ${lines} lines.</p>159        <span class="badge badge-done">${sources} sources</span></a>`;160    })161    .join("");162  const body = `<h1 class="page-title">Research documents</h1>163<div class="doc-grid">${cards}</div>164<h2 class="section-title">Phase 1 literature notes</h2>165<div class="doc-grid">${notes}</div>166<p class="note-line">The project charter itself is public: <a href="/doc/CLAUDE.md">read the full research charter</a>.</p>`;167  page(res, { title: "Research", active: "Research", body });168});169170// ---------------------------------------------------------------- markdown viewer171app.get(/^\/doc\/(.+)$/, (req, res) => {172  const rel = req.params[0];173  const md = rel.endsWith(".md") ? C.readMarkdown(rel) : null;174  if (!md) return notFound(res);175  const crumb = `<p class="crumb"><a href="/research">Research</a> / ${R.esc(rel)}</p>`;176177  if (rel === "research/LOG.md") {178    const entries = C.parseLogEntries();179    const body = `${crumb}180      ${R.docHeader({ ...md.data, document: "Research log" }, rel)}181      <p class="lede-small">Append-only, newest first — every question, experiment, result,182      interpretation, and decision, as required by the charter (§12). ${entries.length} entries.</p>183      ${R.logTimeline(entries, rel)}`;184    return page(res, { title: "Research log", active: "Research", body });185  }186187  const { html, toc } = R.markdownToHtml(md.content, rel);188  const tocBox = R.tocHtml(toc);189  const body = `${crumb}${R.docHeader(md.data, rel)}190  <div class="doc-layout ${tocBox ? "has-toc" : ""}">191    ${tocBox}192    <article class="doc"><div class="md">${html}</div></article>193  </div>`;194  page(res, { title: md.data.document || rel, active: "Research", body });195});196197// ---------------------------------------------------------------- experiments198app.get("/experiments", (req, res) => {199  const exps = C.listExperiments();200  const cards = exps201    .map(202      (e) => `<a class="doc-card" href="/experiments/${e.id}">203      <h3>${R.esc(e.id)}</h3><p>${R.esc(e.purpose)}</p>204      <span class="badge badge-${e.status === "completed" ? "done" : e.status === "has results" ? "in-progress" : "pending"}">${e.status}</span>205      ${e.runs ? `<span class="runs">${e.runs} result run${e.runs > 1 ? "s" : ""}</span>` : ""}</a>`206    )207    .join("");208  const body = `<h1 class="page-title">Experiments</h1>209<p class="lede-small">Every experiment carries a registered hypothesis with an explicit falsification criterion210(seven-field scientific block), a benchmark implementation, raw results, and an analysis. Negative results are kept.</p>211<div class="doc-grid">${cards}</div>`;212  page(res, { title: "Experiments", active: "Experiments", body });213});214215app.get("/experiments/:id", (req, res) => {216  const exps = C.listExperiments();217  const exp = exps.find((e) => e.id === req.params.id);218  if (!exp) return notFound(res);219  const sections = [];220  for (const [file, title] of [["hypothesis.md", "Hypothesis"], ["analysis.md", "Analysis"], ["README.md", "README"]]) {221    const md = C.readMarkdown(path.posix.join(exp.rel, file));222    if (md && md.content.trim().length > 40) {223      sections.push(`<section class="card"><h2>${title}</h2>${metaChips(md.data)}224        <div class="md">${R.markdownToHtml(md.content, path.posix.join(exp.rel, file)).html}</div></section>`);225    }226  }227  const runs = C.listResultRuns().filter((r) => r.experiment === exp.id || r.experiment.startsWith(exp.id + "_"));228  const runsHtml = runs.length229    ? `<section class="card"><h2>Result runs</h2><ul class="file-list">` +230      runs.map((r) => r.files.map((f) => `<li><a href="/results/${f.rel}">${r.timestamp} / ${f.name}</a> <span class="mono-small">${(f.size / 1024).toFixed(1)} KiB</span></li>`).join("")).join("") +231      `</ul></section>`232    : "";233  const codeLink = C.exists(path.posix.join(exp.rel, "benchmark.py"))234    ? `<p><a class="more" href="/file/${exp.rel}/benchmark.py">View benchmark implementation (benchmark.py) →</a></p>`235    : "";236  const body = `<p class="crumb"><a href="/experiments">Experiments</a> / ${R.esc(exp.id)}</p>237<h1 class="page-title">${R.esc(exp.id)}</h1><p class="lede-small">${R.esc(exp.purpose)}</p>238${codeLink}${sections.join("")}${runsHtml}`;239  page(res, { title: exp.id, active: "Experiments", body });240});241242// ---------------------------------------------------------------- results243app.get("/results", (req, res) => {244  const runs = C.listResultRuns();245  const expIds = C.listExperiments().map((e) => e.id);246  const rows = runs247    .map(248      (r) => {249        const target = expIds.find((id) => r.experiment === id || r.experiment.startsWith(id + "_"));250        const expCell = target251          ? `<a href="/experiments/${target}">${r.experiment}</a>`252          : r.experiment;253        return `<tr><td>${expCell}</td>254      <td class="mono-small">${r.timestamp}</td>255      <td>${r.files.map((f) => `<a href="/results/${f.rel}">${f.name}</a>`).join(" · ")}</td></tr>`;256      }257    )258    .join("");259  const body = `<h1 class="page-title">Raw results</h1>260<p class="lede-small">Every result is reproducible from commit hash + config + seed + hardware manifest,261and each JSON embeds the manifest of the exact machine that produced it.</p>262<table class="results-table"><thead><tr><th>Experiment</th><th>Run (UTC)</th><th>Files</th></tr></thead>263<tbody>${rows || '<tr><td colspan="3">No result runs yet.</td></tr>'}</tbody></table>`;264  page(res, { title: "Results", active: "Results", body });265});266267app.get(/^\/results\/(.+)$/, (req, res) => {268  const rel = req.params[0].startsWith("results/") ? req.params[0] : "results/" + req.params[0];269  const raw = C.readText(rel);270  if (raw === null) return notFound(res);271  let bodyContent;272  if (rel.endsWith(".json")) {273    const obj = C.readJson(rel);274    bodyContent = `<pre class="codeblock"><code class="hljs">${R.highlightFile(JSON.stringify(obj, null, 2), "x.json")}</code></pre>`;275  } else {276    bodyContent = `<pre class="codeblock"><code>${R.esc(raw.slice(0, 200000))}</code></pre>`;277  }278  const body = `<p class="crumb"><a href="/results">Results</a> / ${R.esc(rel)}</p><div class="card">${bodyContent}</div>`;279  page(res, { title: rel, active: "Results", body });280});281282// ---------------------------------------------------------------- code browser283const CODE_ROOTS = ["src", "tools", "benchmarks", "experiments", "Makefile", "pyproject.toml", "CITATION.cff"];284app.get("/code", (req, res) => {285  const sections = CODE_ROOTS.map((root) => {286    if (!C.exists(root)) return "";287    const st = C.listDir(root);288    if (st === null) {289      return `<li><a href="/file/${root}">${root}</a></li>`;290    }291    const files = C.walk(root, (f) => C.isTextFile(f.rel) && !f.rel.includes("results/"));292    return `<li class="tree-root"><strong>${root}/</strong><ul>` +293      files.map((f) => `<li><a href="/file/${f.rel}">${f.rel.slice(root.length + 1)}</a></li>`).join("") +294      `</ul></li>`;295  }).join("");296  const body = `<h1 class="page-title">Code</h1>297<p class="lede-small">Core library (<code>src/localvm/</code>), tooling, benchmark harness, and experiment298implementations. Every file carries the project's author header; MLX/Metal is the primary compute path — CUDA299is never a core dependency.</p>300<ul class="tree">${sections}</ul>`;301  page(res, { title: "Code", active: "Code", body });302});303304app.get(/^\/file\/(.+)$/, (req, res) => {305  const rel = req.params[0];306  if (!C.isTextFile(rel)) return notFound(res);307  const raw = C.readText(rel);308  if (raw === null) return notFound(res);309  const name = rel.split("/").pop();310  const body = `<p class="crumb"><a href="/code">Code</a> / ${R.esc(rel)}</p>311<div class="card file-card"><div class="file-head"><span class="mono-small">${R.esc(rel)}</span>312<span class="mono-small">${raw.split("\n").length} lines</span></div>313<pre class="codeblock"><code class="hljs">${R.highlightFile(raw.slice(0, 400000), name)}</code></pre></div>`;314  page(res, { title: name, active: "Code", body });315});316317// ---------------------------------------------------------------- about318app.get("/about", (req, res) => {319  const chart = C.expHChartData();320  const m = (chart && chart.manifest) || {};321  const chip = m.chip || {}, mem = m.memory || {}, ssd = m.ssd || {}, os = m.os || {}, sw = m.software || {};322  const hw = `323<table class="kv-table">324<tr><th>Chip</th><td>${R.esc(chip.brand || "Apple M5 Max")} — ${chip.cores_performance || 6}P + ${chip.cores_efficiency || 12}E CPU cores, ${chip.gpu_cores || 40} GPU cores</td></tr>325<tr><th>Unified memory</th><td>${mem.unified_gb || 48} GB (16 KiB pages)</td></tr>326<tr><th>Storage</th><td>${R.esc(ssd.model || "APPLE SSD AP2048Z")} ${R.esc(ssd.size || "2 TB")} — measured ceiling ≈ 13.1 GB/s (iostat-validated)</td></tr>327<tr><th>OS</th><td>macOS ${R.esc(os.version || "27.0")} (${R.esc(os.build || "")})</td></tr>328<tr><th>Stack</th><td>Python ${R.esc(sw.python || "3.14")}, MLX ${R.esc(sw.mlx || "0.32")}, PyTorch ${R.esc(sw.torch || "2.12")} (MPS), Metal</td></tr>329</table>`;330  const body = `<h1 class="page-title">About this project</h1>331<div class="card md">332<p><strong>localvm-research</strong> is an independent research project by333<strong>Simon-Pierre Boucher</strong> (<a href="mailto:contact@spboucher.ai">contact@spboucher.ai</a>) investigating334whether pretrained large language models that normally do not fit in a consumer Mac's memory can be transformed —335strictly <em>post-training</em> — into execution representations with dramatically smaller instantaneous working sets.</p>336<p>The methodology is deliberately strict: an append-only research log, registered hypotheses with explicit337falsification criteria before any experiment runs, no results reported from uncommitted code, hardware manifests338embedded in every result file, and negative results kept and published. The full methodology is codified in the339<a href="/doc/CLAUDE.md">research charter</a>.</p>340<h2>Primary research hardware</h2>${hw}341<h2>What would count as a breakthrough</h2>342<ul>343<li>A model significantly larger than unified memory running locally with acceptable interactive latency.</li>344<li>Bytes transferred per token substantially smaller than the compressed checkpoint.</li>345<li>Evidence that only a small token-dependent fraction of model information is required during typical inference.</li>346<li>A progressive/conditional execution mechanism that preserves quality while avoiding most weight loading.</li>347<li>A post-training representation with a qualitatively better storage/RAM/quality tradeoff than fixed quantization.</li>348</ul>349<p>Failure is an acceptable outcome — the project's charter defines explicit failure criteria, and the log records350why an approach died, not just what survived.</p>351</div>`;352  page(res, { title: "About", active: "About", body });353});354355// ---------------------------------------------------------------- publications356app.get("/publications", (req, res) => {357  const pubs = (C.listDir("docs/publications") || [])358    .filter((f) => f.name.endsWith(".md"))359    .map((f) => {360      const md = C.readMarkdown(f.rel);361      const abstract = (md.content.split(/## Abstract/i)[1] || "").trim().split("\n\n")[0] || "";362      return { rel: f.rel, data: md.data, abstract };363    })364    .sort((a, b) => String(b.data.created).localeCompare(String(a.data.created)));365  const cards = pubs.length ? pubs.map((p) => `366    <a class="pub-card" href="/doc/${p.rel}">367      <div class="pub-meta"><span class="badge badge-done">${R.esc(p.data.number || "report")}</span>368      <span class="mono-small">${R.esc(p.data.created || "")}</span>369      <span class="badge badge-${p.data.status === "final" ? "done" : "in-progress"}">${R.esc(p.data.status || "draft")}</span></div>370      <h2>${R.esc(p.data.title || p.rel)}</h2>371      <p class="pub-authors">${R.esc(p.data.author || "")}</p>372      <p class="pub-abstract">${R.esc(p.abstract.slice(0, 420))}${p.abstract.length > 420 ? "…" : ""}</p>373      <span class="more">Read the full report →</span>374    </a>`).join("") : "<p class='lede-small'>No publications yet.</p>";375  const body = `<h1 class="page-title">Publications</h1>376<p class="lede-small">Official write-ups of the project's results to date — every number traceable377to a committed result file with hardware manifest, every figure regenerated from raw data.</p>378<div class="pub-list">${cards}</div>`;379  page(res, { title: "Publications", active: "Publications", body });380});381382// raw asset serving (figures referenced by publications)383app.get(/^\/raw\/(.+)$/, (req, res) => {384  const rel = req.params[0];385  const ext = path.extname(rel).toLowerCase();386  const types = { ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg" };387  const abs = C.safeResolve(rel);388  if (!abs || !types[ext] || !require("fs").existsSync(abs)) return notFound(res);389  res.type(types[ext]).sendFile(abs);390});391392// ---------------------------------------------------------------- comments393app.get("/comments", (req, res) => {394  const AVATAR_COLORS = ["#2a78d6", "#eb6834", "#1baf7a", "#4a3aa7", "#e87ba4", "#008300"];395  const comments = readComments().slice().reverse();396  const list = comments.length397    ? comments.map((c) => {398        const initial = (c.name || "?").trim()[0].toUpperCase();399        const hue = AVATAR_COLORS[[...String(c.name)].reduce((a, ch) => a + ch.charCodeAt(0), 0) % AVATAR_COLORS.length];400        return `<article class="comment">401        <span class="avatar" style="background:${hue}">${R.esc(initial)}</span>402        <div class="comment-main">403          <div class="comment-head"><span class="comment-name">${R.esc(c.name)}</span>404          <time class="comment-time">${R.esc((c.ts || "").slice(0, 16).replace("T", " "))} UTC</time></div>405          <p class="comment-body">${R.esc(c.message)}</p>406        </div>407      </article>`;408      }).join("")409    : `<div class="empty-state">410        <span class="empty-icon">💬</span>411        <p>No comments yet — be the first to leave one.</p>412      </div>`;413  const posted = req.query.posted ? `<div class="flash">Thanks — your comment is posted.</div>` : "";414  const err = req.query.err ? `<div class="flash flash-err">${R.esc(String(req.query.err))}</div>` : "";415  const body = `<div class="comments-wrap">416<h1 class="page-title">Discussion</h1>417<p class="lede-small">Questions, critiques, pointers to prior art we missed, replication reports —418all welcome. Comments are public; no account, no tracking.</p>419${posted}${err}420<form class="comment-form" method="POST" action="/comments">421  <div class="cf-head">Leave a comment</div>422  <div class="cf-body">423    <label class="field">424      <span class="field-label">Name</span>425      <input name="name" maxlength="60" required placeholder="Your name" autocomplete="name">426    </label>427    <label class="field">428      <span class="field-label">Comment</span>429      <textarea name="message" maxlength="2000" rows="5" required430        placeholder="Feedback, prior art, questions…"></textarea>431    </label>432    <input type="text" name="website" class="hp" tabindex="-1" autocomplete="off" aria-hidden="true">433    <div class="cf-foot">434      <span class="cf-hint">Stored on this server only · moderated after the fact</span>435      <button type="submit" class="btn">Post comment</button>436    </div>437  </div>438</form>439<section class="comment-list">440  <div class="cl-head">441    <h2>${comments.length} comment${comments.length === 1 ? "" : "s"}</h2>442  </div>443  ${list}444</section>445</div>`;446  page(res, { title: "Comments", active: "Comments", body });447});448449app.post("/comments", (req, res) => {450  const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress || "?";451  const name = String(req.body.name || "").trim().slice(0, 60);452  const message = String(req.body.message || "").trim().slice(0, 2000);453  if (String(req.body.website || "").length) return res.redirect("/comments?posted=1"); // honeypot454  if (!name || message.length < 3) return res.redirect("/comments?err=Name and comment are required.");455  const last = lastPostByIp.get(ip) || 0;456  if (Date.now() - last < 30_000) return res.redirect("/comments?err=Please wait a moment between comments.");457  lastPostByIp.set(ip, Date.now());458  saveComment({ name, message, ts: new Date().toISOString() });459  res.redirect("/comments?posted=1");460});461462// ---------------------------------------------------------------- misc463app.get("/health", (req, res) => res.json({ ok: true, app: "localvm-web", author: "Simon-Pierre Boucher" }));464465function notFound(res) {466  res.status(404);467  page(res, { title: "Not found", active: "", body: `<div class="card"><h1>404</h1><p>That page does not exist. <a href="/">Back to home</a>.</p></div>` });468}469app.use((req, res) => notFound(res));470471app.listen(PORT, () => console.log(`localvm-web listening on :${PORT}`));472