`.forever` was the only way to say three different things — "this is durable",
"I have not answered yet", "I am still looking" — and the census said it was
carrying all three: 17 of 24 live booths (70%, up from 54% the day before).
Three of the four booths in the fleet awaiting an answer had been pinned by
hand as well, and 10 of the 17 were younger than the TTL, so the sentinel had
bought them nothing and was pressed pre-emptively.
Only the first meaning is what `keep` means. The other two are facts the
service already held and did not consult.
KEPT `.forever` present never swept (unchanged)
HELD an open pick, or marks we cannot read never swept (new)
EPHEMERAL everything else 24h (unchanged)
Viewing is activity: a deliberately-served response from a booth's own page
route writes `.viewed`, which is a dotfile and not a `.lock` dotfile, so
`_newest_mtime` already counts it. There is no new arithmetic — `booth_age_seconds`,
`is_expired` and `expires_in` are unchanged. Machine reads are excluded on
purpose: an agent must not be able to hold its own booth open by polling for
the answer it is waiting on.
The hold is unbounded, and what makes that safe is visibility plus two exits
that already existed. Every surface whose chrome the Booth owns says
`held until answered` where the countdown was, and `booth rm` / the UI x /
`DELETE /b/<n>` take a held booth exactly as they take a kept one. A hold is
protection from the timer, never from the operator.
Three cross-frontier panels ran and each found a class the others could not:
* the paraphrase panel found that two reads of one file are not one read of
one state — the contract's `is_held(marks_for(c), read_error(c))` could
resolve to `([], None)`, the pair that deletes. `hold_read` is one read.
* the code-review panel found, 4-of-4, that the booth header's board branch
rendered no lifetime at all; and that five of seven invariant tests passed
under the change that defeats them.
* the bug-hunt panel found four more paths where a failed read still
authorized a delete, and a `record_view` that followed a planted symlink.
`is_held` became `hold_reason`, which returns the reason rather than a bool
beside a string that can disagree with it.
Prediction, to re-count on or after 2026-10-06: the `.forever` rate falls to
the booths that are genuinely durable references. Only 4 booths carry marks at
all, so this rests on both halves of the unit; a null result cannot distinguish
a wrong diagnosis from a habit that outlived its need.
406 tests (341 before). Contract: docs/contracts/u4_derived_lifetime.contract.md
166 lines
8.0 KiB
HTML
166 lines
8.0 KiB
HTML
{% extends "base.html" %}
|
||
{% from "_provenance.html" import provenance %}
|
||
{% from "_lifetime.html" import lifetime %}
|
||
{% block content %}
|
||
<form class="uploader" method="post" action="/upload" enctype="multipart/form-data">
|
||
<label class="drop" for="booth-files">
|
||
<span class="drop-icon">⬆</span>
|
||
<span class="drop-main">Upload files for pickup</span>
|
||
<span class="drop-sub" id="drop-sub">drop here, or click to choose · one pickup id, wiped in {{ ttl_hours }}h</span>
|
||
<input id="booth-files" name="files" type="file" multiple>
|
||
</label>
|
||
<button class="up-go" type="submit">Get pickup id →</button>
|
||
</form>
|
||
|
||
{% if kept %}
|
||
{# Kept boards render FIRST and look different on purpose: they are durable
|
||
operator-facing things (the agent link board, standing reports) and the
|
||
point of the lane is that they cannot be lost in a feed that turns over
|
||
every day. No countdown — they have no expiry to advertise. #}
|
||
<h2 class="lane-head">Kept <span class="lane-note">· no expiry · <code>{{ keep_marker }}</code></span></h2>
|
||
<div class="grid kept-grid">
|
||
{% for b in kept %}
|
||
<article class="card card-kept">
|
||
<a class="thumb" href="/b/{{ b.name_url }}/">
|
||
{% if b.thumb_url %}
|
||
{# A cover blurred inside the booth must be blurred here too, or the
|
||
front page undoes the censoring the booth page applied. #}
|
||
<img class="{{ 'blurred-thumb' if b.thumb_blurred }}" loading="lazy"
|
||
src="/b/{{ b.name_url }}/{{ b.thumb_url }}" alt="">
|
||
{% elif b.has_index %}
|
||
<div class="ph">▦ page</div>
|
||
{% elif b.kinds.video %}
|
||
<div class="ph">▶ video</div>
|
||
{% elif b.kinds.audio %}
|
||
<div class="ph">♪ audio</div>
|
||
{% else %}
|
||
<div class="ph">◆ files</div>
|
||
{% endif %}
|
||
<span class="badge badge-kept">★ kept</span>
|
||
</a>
|
||
<div class="meta">
|
||
<a class="name" href="/b/{{ b.name_url }}/">{{ b.name }}</a>
|
||
<div class="sub">{{ b.count }} item{{ '' if b.count == 1 else 's' }} · {{ lifetime(true, b.hold, b.expires_in) }} · <a class="dl-link" href="/b/{{ b.name_url }}/?download=1" title="download this booth as a zip">⬇ zip</a></div>
|
||
{{ provenance(b.manifest) }}
|
||
</div>
|
||
{# There IS a × here now (operator, 2026-09-21). The old rule was
|
||
release-then-find-it-in-the-other-lane, on the theory that two
|
||
deliberate acts protect durable boards. In practice it protects
|
||
nothing and costs a hunt: the board you just released is loose in a
|
||
feed that turns over, and you have to go find it to finish the job
|
||
you had already decided on.
|
||
|
||
The protection now lives in the CONFIRMATION, not in the number of
|
||
lanes you must traverse — this one names the booth and says the word
|
||
KEPT, where the ephemeral × just asks. A deliberate act, one click,
|
||
reachable.
|
||
|
||
Release still exists and is still the reversible option. Note it
|
||
BUMPS the directory mtime, so the board's age resets and it survives
|
||
another full TTL — unkeep-and-wait is a 24h delay, not a delete,
|
||
which is exactly why a direct × was worth adding. #}
|
||
{# ⚠ BOTH OF THESE WERE position:absolute ON THE SAME CORNER, and `release`
|
||
is the later sibling, so it painted over the × completely: measured
|
||
30x22 px of overlap on a 30px button, and elementFromPoint at the ×'s
|
||
centre returned the release form. The × was unclickable from the day
|
||
it shipped.
|
||
|
||
One flex row, positioned once, instead of two independently guessed
|
||
offsets — so neither control can drift back on top of the other when
|
||
a label changes width. #}
|
||
<div class="kept-actions">
|
||
<form class="release" method="post" action="/b/{{ b.name_url }}/unkeep"
|
||
onsubmit="return confirm('Release \u201c{{ b.name }}\u201d?\n\nIt moves to the ephemeral lane so you can wipe it from there. Nothing is deleted by this step.')">
|
||
<button title="release this board so it can be wiped">release</button>
|
||
</form>
|
||
<form class="wipe wipe-kept" method="post" action="/b/{{ b.name_url }}/delete"
|
||
onsubmit="return confirm('WIPE the KEPT booth \u201c{{ b.name }}\u201d?\n\nThis deletes it and its files immediately. Kept booths are the ones nothing else will clean up, so nobody else is going to do this for you — and nothing brings it back.')">
|
||
<button title="wipe this KEPT booth now" aria-label="wipe kept booth">×</button>
|
||
</form>
|
||
</div>
|
||
</article>
|
||
{% endfor %}
|
||
</div>
|
||
{% if booths %}<h2 class="lane-head">Ephemeral <span class="lane-note">· wiped {{ ttl_hours }}h after last activity</span></h2>{% endif %}
|
||
{% endif %}
|
||
|
||
{% if not booths %}
|
||
{% if not kept %}
|
||
<div class="empty">
|
||
No booths yet. Upload files above, or drop a folder into <code>{{ data_dir }}</code>.
|
||
</div>
|
||
{% endif %}
|
||
{% else %}
|
||
<div class="grid">
|
||
{% for b in booths %}
|
||
<article class="card">
|
||
<a class="thumb" href="/b/{{ b.name_url }}/">
|
||
{% if b.thumb_url %}
|
||
<img class="{{ 'blurred-thumb' if b.thumb_blurred }}" loading="lazy"
|
||
src="/b/{{ b.name_url }}/{{ b.thumb_url }}" alt="">
|
||
{% elif b.has_index %}
|
||
<div class="ph">▦ page</div>
|
||
{% elif b.kinds.video %}
|
||
<div class="ph">▶ video</div>
|
||
{% elif b.kinds.audio %}
|
||
<div class="ph">♪ audio</div>
|
||
{% else %}
|
||
<div class="ph">◆ files</div>
|
||
{% endif %}
|
||
{% if b.uploaded %}<span class="badge">⬆ pickup</span>{% endif %}
|
||
{% if b.marks_open %}<span class="badge badge-mark">? {{ b.marks_open }} open</span>{% endif %}
|
||
</a>
|
||
<div class="meta">
|
||
<a class="name" href="/b/{{ b.name_url }}/">{{ b.name }}</a>
|
||
<div class="sub">{{ b.count }} item{{ '' if b.count == 1 else 's' }} · {{ lifetime(false, b.hold, b.expires_in) }} · <a class="dl-link" href="/b/{{ b.name_url }}/?download=1" title="download this booth as a zip">⬇ zip</a></div>
|
||
{{ provenance(b.manifest) }}
|
||
</div>
|
||
{# Promote to the kept lane. The /keep route and the `booth keep` CLI verb
|
||
both predate this button; until 2026-09-19 the UI could only RELEASE a
|
||
kept booth, never keep an ephemeral one, so the round trip was only
|
||
closed if you had a shell. Reversible, so no confirmation — the × next
|
||
to it is the destructive one and keeps its prompt. #}
|
||
<form class="keepit" method="post" action="/b/{{ b.name_url }}/keep">
|
||
<button title="keep — exempt from the {{ ttl_hours }}h sweep" aria-label="keep booth">★</button>
|
||
</form>
|
||
<form class="wipe" method="post" action="/b/{{ b.name_url }}/delete"
|
||
onsubmit="return confirm('Wipe booth “{{ b.name }}”?')">
|
||
<button title="wipe now" aria-label="wipe booth">×</button>
|
||
</form>
|
||
</article>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<script>
|
||
/* progressive enhancement: reflect chosen files + drag-drop onto the panel.
|
||
With JS off, the native file input + submit still works. */
|
||
(function () {
|
||
var input = document.getElementById('booth-files');
|
||
var sub = document.getElementById('drop-sub');
|
||
var drop = document.querySelector('.drop');
|
||
if (!input) return;
|
||
function show() {
|
||
var n = input.files ? input.files.length : 0;
|
||
if (n) {
|
||
sub.textContent = n + ' file' + (n > 1 ? 's' : '') + ' ready — hit “Get pickup id”';
|
||
drop.classList.add('has');
|
||
}
|
||
}
|
||
input.addEventListener('change', show);
|
||
['dragover', 'dragenter'].forEach(function (e) {
|
||
drop.addEventListener(e, function (ev) { ev.preventDefault(); drop.classList.add('over'); });
|
||
});
|
||
['dragleave', 'drop'].forEach(function (e) {
|
||
drop.addEventListener(e, function (ev) { ev.preventDefault(); drop.classList.remove('over'); });
|
||
});
|
||
drop.addEventListener('drop', function (ev) {
|
||
if (ev.dataTransfer && ev.dataTransfer.files.length) {
|
||
try { input.files = ev.dataTransfer.files; } catch (_) {}
|
||
show();
|
||
}
|
||
});
|
||
})();
|
||
</script>
|
||
{% endblock %}
|