From d0274d0b8285a4f930bac257462b59b1b1539d36 Mon Sep 17 00:00:00 2001 From: 0xMegie Date: Mon, 19 Oct 2026 16:28:27 +0100 Subject: [PATCH] Add print styles to App.css, create retention legal holds migration, and implement fire-drill backup script --- .../20260512000000_retention_legal_holds.sql | 43 +++++++++++ scripts/fire-drill.sh | 77 +++++++++++++++++++ src/App.css | 31 ++++++++ src/lib/raceGate.ts | 76 ++++++++++++++++++ 4 files changed, 227 insertions(+) create mode 100644 migrations/20260512000000_retention_legal_holds.sql create mode 100644 scripts/fire-drill.sh create mode 100644 src/lib/raceGate.ts diff --git a/migrations/20260512000000_retention_legal_holds.sql b/migrations/20260512000000_retention_legal_holds.sql new file mode 100644 index 0000000..a1d44bc --- /dev/null +++ b/migrations/20260512000000_retention_legal_holds.sql @@ -0,0 +1,43 @@ +-- Retention lifecycle primitives (ISP-128). +-- Adds the legal-hold, erasure-request and retention-manifest tables that the +-- hold-aware maintenance jobs and erasure workflow will read and write. +-- Creation only: no existing table is altered and no rows are touched, so this +-- does not lock hot writes. + +CREATE TABLE IF NOT EXISTS legal_holds ( + id BIGSERIAL PRIMARY KEY, + scope TEXT NOT NULL, + reason TEXT NOT NULL, + placed_by TEXT NOT NULL, + placed_at TIMESTAMPTZ NOT NULL DEFAULT now(), + released_at TIMESTAMPTZ +); + +-- A hold is active while released_at IS NULL. Partial index keeps the +-- "is this partition held?" check cheap on the maintenance path. +CREATE INDEX IF NOT EXISTS idx_legal_holds_active + ON legal_holds (scope) + WHERE released_at IS NULL; + +CREATE TABLE IF NOT EXISTS erasure_requests ( + id BIGSERIAL PRIMARY KEY, + person_hmac TEXT NOT NULL, + requested_at TIMESTAMPTZ NOT NULL DEFAULT now(), + status TEXT NOT NULL DEFAULT 'pending' + CHECK (status IN ('pending', 'in_progress', 'complete', 'blocked_by_hold')), + proof JSONB +); + +CREATE INDEX IF NOT EXISTS idx_erasure_requests_person + ON erasure_requests (person_hmac); + +CREATE TABLE IF NOT EXISTS retention_manifest ( + id BIGSERIAL PRIMARY KEY, + partition TEXT NOT NULL, + policy TEXT NOT NULL, + hold_ids BIGINT[] NOT NULL DEFAULT '{}', + anonymized_at TIMESTAMPTZ, + dropped_at TIMESTAMPTZ, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (partition) +); diff --git a/scripts/fire-drill.sh b/scripts/fire-drill.sh new file mode 100644 index 0000000..66298c9 --- /dev/null +++ b/scripts/fire-drill.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# ============================================================================== +# fire-drill.sh +# Backup/restore fire-drill harness (ISP-129). Restores into an isolated clone +# and never touches production. This file currently implements the safety +# guards and the ordered plan only; restore steps are not wired yet. +# +# Usage: +# CLONE_DATABASE_URL=postgres://.../helphone_clone scripts/fire-drill.sh --plan +# CLONE_DATABASE_URL=postgres://.../helphone_drill scripts/fire-drill.sh --run --i-understand-this-wipes +# ============================================================================== + +set -euo pipefail + +MODE="plan" +WIPE_ACK="no" + +for arg in "$@"; do + case "$arg" in + --plan) MODE="plan" ;; + --run) MODE="run" ;; + --i-understand-this-wipes) WIPE_ACK="yes" ;; + *) + echo "[fire-drill] unknown argument: ${arg}" >&2 + exit 2 + ;; + esac +done + +CLONE_DATABASE_URL="${CLONE_DATABASE_URL:-}" + +if [ -z "${CLONE_DATABASE_URL}" ]; then + echo "[fire-drill] CLONE_DATABASE_URL is required. Refusing to guess a target." >&2 + exit 1 +fi + +# Database name is the last path segment, without any query string. +DB_NAME="${CLONE_DATABASE_URL%%\?*}" +DB_NAME="${DB_NAME##*/}" + +# Guard 1: the target must be explicitly a clone or drill database. +case "${DB_NAME}" in + *_clone | *_drill) ;; + *) + echo "[fire-drill] refusing target database '${DB_NAME}': name must end in _clone or _drill." >&2 + exit 1 + ;; +esac + +# Guard 2: refuse anything that looks like production, even with a clone suffix. +if printf '%s' "${CLONE_DATABASE_URL}" | grep -qiE '(prod|production|live)'; then + echo "[fire-drill] refusing target: URL contains a production-like marker." >&2 + exit 1 +fi + +# Guard 3: destructive mode needs an explicit acknowledgement flag. +if [ "${MODE}" = "run" ] && [ "${WIPE_ACK}" != "yes" ]; then + echo "[fire-drill] --run wipes the clone. Re-run with --i-understand-this-wipes." >&2 + exit 1 +fi + +echo "[fire-drill] target database: ${DB_NAME} (mode: ${MODE})" +echo "[fire-drill] ordered plan:" +echo " 1. restore PostgreSQL to target time on clone" +echo " 2. reset indexer cursor to restored high-water mark" +echo " 3. restore Redis RDB with backlog quarantine (no refire)" +echo " 4. import field kits (IDB export)" +echo " 5. reconcile contract truth vs PG projections" +echo " 6. emit cutover signal" + +if [ "${MODE}" = "plan" ]; then + echo "[fire-drill] plan only; no changes made." + exit 0 +fi + +echo "[fire-drill] --run is not implemented yet: restore steps 1-6 are pending." >&2 +exit 3 diff --git a/src/App.css b/src/App.css index abb1922..a0b28ef 100644 --- a/src/App.css +++ b/src/App.css @@ -46,3 +46,34 @@ display: none; } } + +/* Print: ink-friendly A4 report output. Everything here is scoped to + @media print so the screen layout is untouched. Elements that must not + appear on paper opt out with .hp-print-hide; cards that must not split + across pages opt in with .hp-print-card. */ +@media print { + @page { + size: A4; + margin: 14mm; + } + + html, + body { + background: #fff !important; + color: #000 !important; + } + + .hp-app-nav, + .hp-print-hide { + display: none !important; + } + + .hp-print-card { + break-inside: avoid; + page-break-inside: avoid; + } + + [dir="rtl"] { + direction: rtl; + } +} diff --git a/src/lib/raceGate.ts b/src/lib/raceGate.ts new file mode 100644 index 0000000..5bb2c54 --- /dev/null +++ b/src/lib/raceGate.ts @@ -0,0 +1,76 @@ +/** + * Multipath race gate (ISP-120). + * + * Pure policy: decides whether a dispatch-critical send may fire a redundant + * second copy. It performs no I/O and does not send anything. Callers feed in + * the current path state and the payload class. Scope is deliberately narrow: + * - P0 only; P1 and bulk never race + * - only after a single-path stall (not always-on) + * - vetoed on metered bearers and on low battery + * - capped races per rolling hour + * The flag is VITE_MULTIPATH_RACE; when it is not "true", nothing races. + */ + +export type PayloadClass = 'P0' | 'P1' | 'bulk' + +export interface PathState { + /** Observed latency of the primary path, in ms. */ + rttMs: number + /** Stall reported by the RTT/heartbeat detector for the primary path. */ + stalled: boolean + /** True when the active bearer is metered (capped data plan). */ + metered: boolean +} + +export interface RaceContext { + payload: PayloadClass + path: PathState + /** Battery level 0..1, or null when the Battery API is unavailable. */ + batteryLevel: number | null + /** Races already fired in the last rolling hour. */ + racesLastHour: number + /** Operator override to allow a metered race for P0 only. */ + operatorOverride: boolean +} + +export type RaceDecision = + | { race: true } + | { race: false; reason: RaceVetoReason } + +export type RaceVetoReason = + | 'flag-off' + | 'not-p0' + | 'path-healthy' + | 'metered-bearer' + | 'low-battery' + | 'hourly-cap' + +export const RACE_MAX_PER_HOUR = 20 +export const RACE_LOW_BATTERY_LEVEL = 0.2 + +/** Mirrors the flag env var; read once by the caller and passed in. */ +export function isRaceFlagOn(rawEnv: string | undefined): boolean { + return rawEnv === 'true' +} + +export function decideRace(flagOn: boolean, ctx: RaceContext): RaceDecision { + if (!flagOn) return { race: false, reason: 'flag-off' } + if (ctx.payload !== 'P0') return { race: false, reason: 'not-p0' } + if (!ctx.path.stalled) return { race: false, reason: 'path-healthy' } + + // Metered bearers are vetoed unless P0 plus an explicit operator override. + // The payload is already P0 here, so the override alone unlocks it. + if (ctx.path.metered && !ctx.operatorOverride) { + return { race: false, reason: 'metered-bearer' } + } + + if (ctx.batteryLevel !== null && ctx.batteryLevel < RACE_LOW_BATTERY_LEVEL) { + return { race: false, reason: 'low-battery' } + } + + if (ctx.racesLastHour >= RACE_MAX_PER_HOUR) { + return { race: false, reason: 'hourly-cap' } + } + + return { race: true } +}