chore(autonomous-session): consolidate uncommitted work from prior session

Bundles the prior autonomous-session output that was sitting unstaged:

- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
  never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
  after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
  redirects (ocr to ai, reports to dashboard, invitations to users),
  docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
  flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
  let-reassign), set-state-in-effect disables in CountryFlag and
  UploadForSigning preview-bytes effect, unused 'confirm' destructures in
  interest contract + reservation tabs, unescaped apostrophe in test-template
  card copy
This commit is contained in:
2026-05-23 00:52:59 +02:00
parent 43719b49e9
commit 221ae5784e
749 changed files with 7440 additions and 3118 deletions

View File

@@ -1,13 +1,13 @@
/**
* Pluggable storage backend (Phase 6a see docs/berth-recommender-and-pdf-plan.md §4.7a).
* Pluggable storage backend (Phase 6a - see docs/berth-recommender-and-pdf-plan.md §4.7a).
*
* The CRM stores files (per-berth PDFs, brochures, GDPR exports, etc.) through a
* single `StorageBackend` abstraction. The deployment chooses between an
* S3-compatible store (MinIO / AWS S3 / Backblaze B2 / Cloudflare R2 / Wasabi /
* Tigris) and a local filesystem at runtime via `system_settings.storage_backend`.
*
* Callers should always import from this barrel never from `s3.ts` or
* `filesystem.ts` directly so the factory wiring stays the single source of
* Callers should always import from this barrel - never from `s3.ts` or
* `filesystem.ts` directly - so the factory wiring stays the single source of
* truth.
*/
@@ -24,7 +24,7 @@ export type StorageBackendName = 's3' | 'filesystem';
export interface PutOpts {
contentType: string;
/** Optional pre-computed sha256 hex if absent, the backend computes one. */
/** Optional pre-computed sha256 hex - if absent, the backend computes one. */
sha256?: string;
/** Bytes (for streams that don't expose .length); used for capacity pre-flight. */
sizeBytes?: number;
@@ -43,7 +43,7 @@ export interface PresignOpts {
* verifier asserts the storage key starts with `${portSlug}/` when
* present. S3 backend ignores this field (presigned S3 URLs carry
* their own signature scope). Pass it whenever the issuer is in a
* port-scoped request `generateStorageKey()` already prefixes the
* port-scoped request - `generateStorageKey()` already prefixes the
* slug, so this is the matching enforcement.
*/
portSlug?: string;
@@ -63,7 +63,7 @@ export interface StorageBackend {
/** Existence + size check without reading the full body. Returns null when missing. */
head(key: string): Promise<{ sizeBytes: number; contentType: string } | null>;
/** Delete. Idempotent missing keys must not throw. */
/** Delete. Idempotent - missing keys must not throw. */
delete(key: string): Promise<void>;
/** Generate a short-lived URL the browser can PUT to. */
@@ -129,7 +129,7 @@ async function readGlobalSetting<T = unknown>(key: string): Promise<T | null> {
async function loadStorageConfig(): Promise<StorageConfigSnapshot> {
// Each setting key is a separate row. We read them in parallel.
// `storage_s3_access_key_encrypted` is the modern home for the S3 access
// key (fixes audit S-23 was previously stored plaintext at
// key (fixes audit S-23 - was previously stored plaintext at
// `storage_s3_access_key`). We still read the legacy plaintext key for
// backward compat, but the encrypted form wins when both are present.
const keys = [
@@ -205,7 +205,7 @@ async function loadStorageConfig(): Promise<StorageConfigSnapshot> {
* this comparison is a defense-in-depth backstop rather than the primary
* invalidation path. If you ever change `loadStorageConfig` to read
* additional sensitive material, make sure the rotation flow keeps
* resetting the cache relying on fingerprint diff alone means the old
* resetting the cache - relying on fingerprint diff alone means the old
* client is held in memory until the next mismatch.
*/
function fingerprint(cfg: StorageConfigSnapshot): string {
@@ -250,13 +250,13 @@ async function buildBackend(cfg: StorageConfigSnapshot): Promise<StorageBackend>
// ─── url helpers ────────────────────────────────────────────────────────────
/**
* Convenience wrapper that returns just the presigned download URL the most
* Convenience wrapper that returns just the presigned download URL - the most
* common need at call sites that don't track expiry. Mirrors the legacy
* `getPresignedUrl(key)` helper in `@/lib/minio` but routes through the
* active backend so filesystem-mode deployments work too.
*
* storage-pathing-auditor H2: when `portSlug` is not passed explicitly,
* we attempt to infer it from the key's first path segment every
* we attempt to infer it from the key's first path segment - every
* storage key minted via `buildStoragePath(slug, …)` starts with the
* slug, so the inference is correct for the overwhelming majority of
* callers. This engages the filesystem-proxy port-binding token (`p`)
@@ -287,7 +287,7 @@ export async function presignDownloadUrl(
* A slug is conservatively defined as kebab/alphanumeric (the same
* shape `createPortSchema` enforces). Non-matching first segments
* include UUID-only keys like the legacy `berths/{id}/uploads/...`
* shape those are still served but skip the binding gate.
* shape - those are still served but skip the binding gate.
*/
function inferPortSlugFromKey(key: string): string | undefined {
const slash = key.indexOf('/');