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:
+89
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user