Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions migrations/20260512000000_retention_legal_holds.sql
Original file line number Diff line number Diff line change
@@ -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)
);
77 changes: 77 additions & 0 deletions scripts/fire-drill.sh
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
76 changes: 76 additions & 0 deletions src/lib/raceGate.ts
Original file line number Diff line number Diff line change
@@ -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 }
}
Loading