fix(as-S1): the house clock — stamps read 0848, no IPs on the page

The anti-slop run (design-dev, 2026-09-28; operator: "start the fix slices")
found raw ISO stamps with microseconds and offsets, the poster's IP address,
and HH:MM in board rows and <time> tooltips. Operator convention 2026-09-24:
a clock the operator reads is 24-hour local time as four digits, no colon.

- `clock` filter: ISO (any precision, any offset), epoch, or the board's
  `YYYY-MM-DD HH:MM` -> `28 Sep 0848` local, year only when not this year's.
  Never raises; what it cannot read is shown as given. No regex (INV-3).
- `byline` filter: a handle is shown, an IP address is not. Stored `by` and
  `answered_by` are unchanged (u2 still records the client host).
- Applied to the marks' answer and memo lines, the inline ask's state tag
  (so the embed chrome inherits it) and the link board's row time, each in a
  <time> whose datetime= carries the stored value exactly.
- `date_stamp` (the created/updated tooltips) renders `YYYY-MM-DD HHMM`.
Folded from the heid bug-hunt (panel 4/4, thread 01M3MGPFKWBX0SJK5HFE0P3AFM):
clock converts a number inside its guard (an int past float range raised,
Q1); a date or ISO week renders no invented 0000 (Q8); byline also hides
addr:port, [v6]:port, addr/prefix and addresses behind invisible characters
(Q7); the board row's author is bylined (Q5). Refuted: Q3 (default Jinja
Undefined has length 0; the test stays as a StrictUndefined guard).
Accepted with reasons: Q4, Q6.

Contract: docs/contracts/as_antislop.contract.md S1. Falsifiers: antislop.toml 15/15 proved (S1);
all 12 tables 295/295 proved on this tree.
This commit is contained in:
vh
2026-09-28 13:30:31 -07:00
parent 190a75a0e1
commit 09071dcb65
7 changed files with 460 additions and 7 deletions
+89 -1
View File
@@ -37,6 +37,8 @@ import asyncio
import fcntl
import hashlib
import io
import ipaddress
import unicodedata
import json
import math
import os
@@ -262,12 +264,96 @@ def date_iso(epoch: float) -> str:
def date_stamp(epoch: float) -> str:
"""The full stamp for a <time>'s title, in the house clock form
(operator, 2026-09-24): four digits, no colon — `2026-09-28 0848`."""
try:
return time.strftime("%Y-%m-%d %H:%M", time.localtime(epoch))
return time.strftime("%Y-%m-%d %H%M", time.localtime(epoch))
except _BAD_DATE:
return ""
# as_antislop S1 — THE house clock. A clock time the operator reads is local
# 24-hour time as four digits with no colon: `28 Sep 0848`. Marks and answers
# store ISO with microseconds and an offset (`now_stamp`), and `booth link`
# rows store `YYYY-MM-DD HH:MM`; this is the ONE place either becomes visible
# text. The stored value is untouched and rides in the <time>'s `datetime`.
def _as_epoch(value) -> float | None:
if isinstance(value, bool):
return None
if isinstance(value, (int, float)):
# convert INSIDE the guard: `math.isfinite` on an int past float range
# raises OverflowError (heid bug-hunt Q1, 3 of 4 arms)
try:
f = float(value)
except (OverflowError, ValueError):
return None
return f if math.isfinite(f) else None
if not isinstance(value, str):
return None
try:
# 3.11+ reads the board's `YYYY-MM-DD HH:MM` as well as full ISO, so
# no regex: app.py keeps its ONE (INV-3, test_no_regex_touches_author_html).
dt = datetime.fromisoformat(value.strip())
except ValueError:
return None
try:
# naive = local (the board's rows, and any stamp written without a zone)
return dt.timestamp()
except _BAD_DATE:
return None
def clock(value, now: float | None = None) -> str:
"""`28 Sep 0848`, with the year only when it is not this year's
(`6 Sep 2025 2335`). NEVER RAISES: a value it cannot read is returned as
given — never a guess, and never a 500 for a page of rows. Empty is ""."""
if value is None or value == "":
return ""
epoch = _as_epoch(value)
if epoch is None:
return value if isinstance(value, str) else ""
day = date_day(epoch, now)
if isinstance(value, str) and ":" not in value:
# a date (or an ISO week) carries no clock time: never print an invented 0000
return day or value
try:
hhmm = time.strftime("%H%M", time.localtime(epoch))
except _BAD_DATE:
return value if isinstance(value, str) else ""
return f"{day} {hhmm}" if day else (value if isinstance(value, str) else "")
def byline(who) -> str:
"""Who recorded a mark, as the operator should read it: a handle is shown,
an address is not. The u2 record keeps `request.client.host` as it always
has; an IP on the page is noise at best and a leak at worst."""
if not isinstance(who, str):
return ""
# invisible characters (zero-width, bidi, controls) cannot disguise an address
bare = "".join(ch for ch in who if unicodedata.category(ch) not in ("Cf", "Cc")).strip()
if not bare:
return ""
return "" if _is_address(bare) else who
def _is_address(s: str) -> bool:
"""An IP, bare or dressed as `addr:port`, `[v6]:port` or `addr/prefix`."""
candidates = {s, s.split("/", 1)[0]}
if s.startswith("[") and "]" in s:
candidates.add(s[1:s.index("]")])
if s.count(":") == 1:
candidates.add(s.split(":", 1)[0])
for c in candidates:
try:
ipaddress.ip_address(c)
return True
except ValueError:
continue
return False
def date_day(epoch: float, now: float | None = None) -> str:
"""'12 Sep', with the year only when it is not this year's; '' when the
calendar cannot hold it."""
@@ -1089,6 +1175,8 @@ def create_app(
env.filters["stamp"] = date_stamp
env.filters["day"] = date_day
env.filters["ago"] = date_ago
env.filters["clock"] = clock
env.filters["byline"] = byline
templates = Jinja2Templates(env=env)
# embed.js IS READ ONCE, HERE, for exactly the reason above. It is the third