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%
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 < 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