feat(screenshots): cropped shots, sizes for the site, a site check

- shots.py crops to a region (clip) or to what one or more elements cover
  (element, pad), per-shot viewport; capture.py writes the published sizes to
  src/data/screenshots.json so the page reserves the right space.
- anonymize.py no longer empties secrets that netOrk compares with each other
  (Wi-Fi keys on an SSID against the key read from the access point). Emptying
  them invented passphrase "drift" that never existed; a keyed hash keeps equal
  equal, reverses nothing, and its key lives for one run.
- scripts/check/site.py checks the built site in both languages at four widths:
  sideways overflow, one h1, images with alt and size, console errors, requests
  to other origins, links to unknown routes, old-URL redirects, language
  detection, and word counts against the budgets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Christian Manivong
2026-09-29 23:24:49 +02:00
co-authored by Claude Opus 5.5
parent d585f9272f
commit 369f66afdc
18 changed files with 259 additions and 762 deletions
+38 -7
View File
@@ -33,6 +33,7 @@ import ipaddress
import json
import os
import re
import secrets
import sys
from pathlib import Path
@@ -183,9 +184,19 @@ class Mapper:
return ".".join(mapped[::-1]) + ".in-addr.arpa"
# -- whole strings -------------------------------------------------------
@staticmethod
def _secret(m: re.Match) -> str:
name, sep = m.group(1), m.group(2)
if name in SECRET_JSON_KEEP:
return m.group(0)
raw = m.group(0)[m.group(0).index(sep) + len(sep) + 1:-1]
if not raw or not COMPARED_SECRET.match(name):
return f'"{name}"{sep}""'
value = json.loads(f'"{raw}"') # the value as the column would hold it
return f'"{name}"{sep}"{secret_token(value)}"'
def text(self, s: str) -> str:
s = SECRET_JSON.sub(lambda m: m.group(0) if m.group(1) in SECRET_JSON_KEEP
else f'"{m.group(1)}"{m.group(2)}""', s)
s = SECRET_JSON.sub(self._secret, s)
s = REVERSE.sub(self.reverse, s)
s = EMAIL.sub(self.email, s)
if self.domain_re:
@@ -219,6 +230,18 @@ SECRET_JSON = re.compile(
r'"((?:[A-Za-z0-9_]*_)?(?:key|psk|passphrase|password|passwd|secret|token|private_key|ft_key|sae_password))"'
r'(\s*:\s*)"(?:[^"\\]|\\.)*"')
SECRET_JSON_KEEP = {"public_key", "entry_key", "key_type", "is_secret", "ssh_key_id"}
# Secrets netOrk compares with each other (the Wi-Fi key stored on an SSID against
# the key read from the access point). Emptying them would invent drift that never
# existed, so they become a keyed hash instead: equal stays equal, nothing can be
# reversed, and the key lives only for this run.
COMPARED_SECRET = re.compile(r"^(passphrase|psk|ft_key|wpa_key|key|sae_password)$", re.I)
RUN_KEY = secrets.token_hex(32)
def secret_token(value: str) -> str:
"""Same formula as the SQL in scrub_secrets: md5(run key || value)."""
return "demo-" + hashlib.md5((RUN_KEY + value).encode()).hexdigest()[:16]
SECRET_KEEP = {"hashed_password", "token_version", "title_tokens", "disable_password_auth"}
# Whole tables that only hold secrets or personal delivery data.
@@ -284,14 +307,22 @@ async def scrub_secrets(con, dry: bool) -> None:
continue
if dt not in ("text", "character varying", "jsonb", "json", "bytea"):
continue # flags like require_password are booleans
n = await con.fetchval(f'SELECT count(*) FROM "{t}" WHERE "{c}" IS NOT NULL')
if not n:
continue
if COMPARED_SECRET.match(c) and dt in ("text", "character varying"):
print(f" {t}.{c}: {n} replaced by keyed hash")
if not dry:
await con.execute(
f'UPDATE "{t}" SET "{c}" = \'demo-\' || left(md5($1 || "{c}"), 16) '
f'WHERE "{c}" IS NOT NULL AND "{c}" <> \'\'', RUN_KEY)
continue
value = "NULL" if nullable == "YES" else ("'{}'" if dt in ("jsonb", "json") else "''")
if dt == "bytea" and nullable != "YES":
value = "''::bytea"
n = await con.fetchval(f'SELECT count(*) FROM "{t}" WHERE "{c}" IS NOT NULL')
if n:
print(f" {t}.{c}: {n} emptied")
if not dry:
await con.execute(f'UPDATE "{t}" SET "{c}" = {value}')
print(f" {t}.{c}: {n} emptied")
if not dry:
await con.execute(f'UPDATE "{t}" SET "{c}" = {value}')
# Settings flagged secret keep their key, lose their value.
if await con.fetchval("SELECT to_regclass('public.settings') IS NOT NULL"):
n = await con.fetchval("SELECT count(*) FROM settings WHERE is_secret")