`/heid-bug-hunt` on U2's diff, four arms, artifact-only. Eight findings were real against live code; a ninth was already closed by v0.2.1 and is recorded as declined. Full triage in persistent-memory.d/2026-09-22-bug-hunt-panel.md. THE LOCK LIFECYCLE (4/4 convergent, and two defects in one place) `_Locked.__exit__` unlinked `.marks.lock` on the no-op path so a booth that had never been marked was left exactly as it was found. `flock` binds to an INODE: unlinking it under a blocked waiter leaves that waiter holding an exclusive lock on a deleted file while the next writer creates a fresh lock and takes it immediately. Two processes then run the read-modify-write concurrently, the later os.replace drops the earlier one's mark, and both obeyed the protocol. The cleanup existed to protect the booth's TTL, and was failing at that too: creating or removing a directory entry bumps the DIRECTORY's mtime, which is what `_newest_mtime` seeds from. The guard's comment reasons about the lock file's own mtime and misses that the directory moved underneath it. One fix: never unlink the lock, exempt `.<name>.lock` dotfiles from `_newest_mtime`, and restore the directory's mtime after creating one. THE READ PATH'S BLAST RADIUS `_clean_text` did `(text or "").replace(...)` and `marks_for` sorts on `(created, id)`, so a stored `text` that was a dict or a `created` that was a number raised out of the read path. `list_booths` reads every booth's marks on every index load, so one hand-edited file returned 500 for `/` and `/healthz` across all 25 booths. Guarded in two layers — a named type check and a `_hydrate_safe` backstop that cannot raise — and an unreadable mark now renders as ⚠ broken rather than as an empty note. ALSO - import_legacy_asks stamped `created` at whole-second resolution, so two sidecars from the same second lost the ordering the importer had just established and re-sorted alphabetically. Microseconds, per the stated `(mtime, name)` rule. - The five mark-write routes ran a blocking flock on the event loop; they now dispatch through run_in_threadpool, asserted structurally like INV-1. - `/answer` 500'd on a non-string `notes` form value where `/note` handled it. - The inline-doc tile had a flag control and no note field. - The marks panel was suppressed on any booth carrying a links.md. - The viewer's arrow keys and Escape threw away a note being typed. CLI `booth marks` printed a traceback and exited 0 on a failed read, and `--wait` emitted a whole JSON document per poll. `booth answer --wait` read a damaged file as "not yet" and spun the full hour. Both now use real exit codes — 0 ok, 1 unanswered/timed-out, 2 no such pick, 3 unreadable — and `--wait` prints once. `marks.read_error()` lets the CLI ask what the page must not: the browser stays lenient, the machine consumer gets the truth. `scripts/booth` had no tests; it has five now, run against the real script under the system python3, which also makes them a live check on INV-1. 275 tests (253 before). Live service restarted, 25/25 booth pages verified 200.
51 lines
2.1 KiB
HTML
51 lines
2.1 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{{ file }} · {{ name }} · The Booth{% endblock %}
|
|
{% block content %}
|
|
<div class="docview">
|
|
<div class="vbar">
|
|
<a class="vbtn vx" href="/b/{{ name_url }}/" title="back to gallery (Esc)">✕</a>
|
|
<span class="vname">{{ file }}</span>
|
|
<span class="vspacer"></span>
|
|
<a class="vbtn" href="{{ file_url }}?dl=1" title="download {{ file }}">⬇</a>
|
|
</div>
|
|
{# Same record, same reason as the image viewer: the sidecar that says what
|
|
this doc IS travels with it to full-page view. #}
|
|
{% if caption %}<div class="doccap">{{ caption }}</div>{% endif %}
|
|
{% if marks %}
|
|
<div class="docmarks">
|
|
{% for m in marks if m.shape == 'note' %}<pre class="vnote">{{ m.text }}</pre>{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
{% if is_html %}
|
|
<article class="markdown-body">{{ body|safe }}</article>
|
|
{% else %}
|
|
<pre class="textview">{{ body }}</pre>
|
|
{% endif %}
|
|
</div>
|
|
<style>
|
|
/* .markdown-body and .textview now live in base.html (shared with the inline
|
|
gallery view). Only the full-page layout wrapper is page-specific. */
|
|
.docview{max-width:52rem;margin:0 auto;padding:0 clamp(12px,3vw,20px) 4rem}
|
|
.docview .textview{overflow-x:auto}
|
|
.doccap{margin:.9rem 0 1.2rem;padding:.6rem .85rem;font-size:.85rem;line-height:1.5;
|
|
color:var(--fg-1);background:var(--rk-surface,rgba(255,255,255,.04));
|
|
border-left:2px solid var(--aus-bright-cyan,#42dcd1);border-radius:0 6px 6px 0;
|
|
white-space:pre-wrap}
|
|
</style>
|
|
<script>
|
|
(function () {
|
|
/* Escape leaves the page, so it must not fire from inside a field someone
|
|
is typing in — the same guard the image viewer carries, stated in both
|
|
places because the handler is on `document` in both. */
|
|
function isEditable(el) {
|
|
return !!(el && (el.isContentEditable ||
|
|
/^(input|textarea|select)$/i.test(el.tagName || '')));
|
|
}
|
|
document.addEventListener('keydown', function (e) {
|
|
if (isEditable(e.target)) return;
|
|
if (e.key === 'Escape') window.location.href = {{ ('/b/' ~ name_url ~ '/')|tojson }};
|
|
});
|
|
})();
|
|
</script>
|
|
{% endblock %}
|