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

@@ -5,7 +5,7 @@ import type { SettingEntry } from './types';
/**
* Central registry of every tenant-configurable setting. One entry per setting,
* consumed by the resolver, the admin form generator, the validator, and the
* encryption helper. Adding a new integration is a registry entry no new
* encryption helper. Adding a new integration is a registry entry - no new
* schema, no new resolver, no new admin page wiring.
*
* Do NOT register boot-time / build-time secrets here (DATABASE_URL,
@@ -28,7 +28,7 @@ export const REGISTRY: SettingEntry[] = [
section: 'documenso.api',
label: 'API URL',
description:
'Bare host only never include /api/v1. The client appends versioned paths based on the API version below.',
'Bare host only - never include /api/v1. The client appends versioned paths based on the API version below.',
type: 'url',
scope: 'port',
envFallback: 'DOCUMENSO_API_URL',
@@ -53,8 +53,8 @@ export const REGISTRY: SettingEntry[] = [
'v1 = Documenso 1.13.x stable. v2 = Documenso 2.x with the envelope model. Test the connection after switching.',
type: 'select',
options: [
{ value: 'v1', label: 'v1 Documenso 1.13.x (default, stable)' },
{ value: 'v2', label: 'v2 Documenso 2.x (envelope, recommended for new ports)' },
{ value: 'v1', label: 'v1 - Documenso 1.13.x (default, stable)' },
{ value: 'v2', label: 'v2 - Documenso 2.x (envelope, recommended for new ports)' },
],
scope: 'port',
envFallback: 'DOCUMENSO_API_VERSION',
@@ -78,7 +78,7 @@ export const REGISTRY: SettingEntry[] = [
{
key: 'documenso_developer_name',
section: 'documenso.signers',
label: 'Developer signer name',
label: 'Developer signer - name',
description:
"Override the name on the developer recipient slot. Leave blank to use whatever's set on the Documenso template.",
type: 'string',
@@ -87,7 +87,7 @@ export const REGISTRY: SettingEntry[] = [
{
key: 'documenso_developer_email',
section: 'documenso.signers',
label: 'Developer signer email',
label: 'Developer signer - email',
description:
"Override the email on the developer recipient slot. Leave blank to use whatever's set on the Documenso template.",
type: 'email',
@@ -96,7 +96,7 @@ export const REGISTRY: SettingEntry[] = [
{
key: 'documenso_developer_label',
section: 'documenso.signers',
label: 'Developer signer label',
label: 'Developer signer - label',
description: 'Display label shown on the signing screen (defaults to "Developer").',
type: 'string',
scope: 'port',
@@ -107,7 +107,7 @@ export const REGISTRY: SettingEntry[] = [
section: 'documenso.signers',
label: 'Developer Documenso recipient ID',
description:
'Numeric Documenso recipient slot ID for the developer signer. Set automatically by "Sync from Documenso" you rarely set this by hand.',
'Numeric Documenso recipient slot ID for the developer signer. Set automatically by "Sync from Documenso" - you rarely set this by hand.',
type: 'number',
scope: 'port',
envFallback: 'DOCUMENSO_DEVELOPER_RECIPIENT_ID',
@@ -115,16 +115,16 @@ export const REGISTRY: SettingEntry[] = [
{
key: 'documenso_developer_user_id',
section: 'documenso.signers',
label: 'Developer signer linked CRM user (optional)',
label: 'Developer signer - linked CRM user (optional)',
description:
"Project Director RBAC binding. When set, the webhook handler fires an in-CRM notification for this user when it's their turn to sign alongside the branded email. Leave blank if the developer slot doesn't map to a CRM user (e.g. external developer).",
"Project Director RBAC binding. When set, the webhook handler fires an in-CRM notification for this user when it's their turn to sign - alongside the branded email. Leave blank if the developer slot doesn't map to a CRM user (e.g. external developer).",
type: 'user-select',
scope: 'port',
},
{
key: 'documenso_approver_name',
section: 'documenso.signers',
label: 'Approver signer name',
label: 'Approver signer - name',
description:
"Override the name on the approver recipient slot. Leave blank to use whatever's set on the Documenso template.",
type: 'string',
@@ -133,7 +133,7 @@ export const REGISTRY: SettingEntry[] = [
{
key: 'documenso_approver_email',
section: 'documenso.signers',
label: 'Approver signer email',
label: 'Approver signer - email',
description:
"Override the email on the approver recipient slot. Leave blank to use whatever's set on the Documenso template.",
type: 'email',
@@ -142,7 +142,7 @@ export const REGISTRY: SettingEntry[] = [
{
key: 'documenso_approver_label',
section: 'documenso.signers',
label: 'Approver signer label',
label: 'Approver signer - label',
description: 'Display label shown on the signing screen (defaults to "Approver").',
type: 'string',
scope: 'port',
@@ -153,7 +153,7 @@ export const REGISTRY: SettingEntry[] = [
section: 'documenso.signers',
label: 'Approver Documenso recipient ID',
description:
'Numeric Documenso recipient slot ID for the approver. Set automatically by "Sync from Documenso" you rarely set this by hand.',
'Numeric Documenso recipient slot ID for the approver. Set automatically by "Sync from Documenso" - you rarely set this by hand.',
type: 'number',
scope: 'port',
envFallback: 'DOCUMENSO_APPROVAL_RECIPIENT_ID',
@@ -161,9 +161,9 @@ export const REGISTRY: SettingEntry[] = [
{
key: 'documenso_approver_user_id',
section: 'documenso.signers',
label: 'Approver linked CRM user (optional)',
label: 'Approver - linked CRM user (optional)',
description:
"Same as developer's linked user when set, fires an in-CRM notification when it's the approver's turn to sign.",
"Same as developer's linked user - when set, fires an in-CRM notification when it's the approver's turn to sign.",
type: 'user-select',
scope: 'port',
},
@@ -241,11 +241,11 @@ export const REGISTRY: SettingEntry[] = [
section: 'documenso.behavior',
label: 'Signing order',
description:
'PARALLEL = all recipients can sign at once. SEQUENTIAL = each waits for the previous (v2 only v1 always parallel).',
'PARALLEL = all recipients can sign at once. SEQUENTIAL = each waits for the previous (v2 only - v1 always parallel).',
type: 'select',
options: [
{ value: 'PARALLEL', label: 'Parallel all recipients sign concurrently' },
{ value: 'SEQUENTIAL', label: 'Sequential order matters (v2 only)' },
{ value: 'PARALLEL', label: 'Parallel - all recipients sign concurrently' },
{ value: 'SEQUENTIAL', label: 'Sequential - order matters (v2 only)' },
],
scope: 'port',
defaultValue: 'PARALLEL',
@@ -263,7 +263,7 @@ export const REGISTRY: SettingEntry[] = [
// JSON map keyed by trigger name; value is one of 'auto' | 'suggest' |
// 'off'. Read by `getStageAdvanceMode` in port-config.ts. The registry
// entry uses the generic `string` type because the form generator's
// schemas don't have a JSON variant the admin UI is a dedicated page
// schemas don't have a JSON variant - the admin UI is a dedicated page
// (/admin/pipeline-rules) that renders 3-way toggles per trigger.
{
key: 'stage_advance_rules',
@@ -316,15 +316,15 @@ export const REGISTRY: SettingEntry[] = [
description: 'Used when a feature does not specify an explicit model.',
type: 'select',
options: [
{ value: 'gpt-4o-mini', label: 'gpt-4o-mini cheap, fast, vision-capable' },
{ value: 'gpt-4o', label: 'gpt-4o full-strength multimodal' },
{ value: 'gpt-4-turbo', label: 'gpt-4-turbo legacy text reasoning' },
{ value: 'gpt-4o-mini', label: 'gpt-4o-mini - cheap, fast, vision-capable' },
{ value: 'gpt-4o', label: 'gpt-4o - full-strength multimodal' },
{ value: 'gpt-4-turbo', label: 'gpt-4-turbo - legacy text reasoning' },
],
scope: 'port',
defaultValue: 'gpt-4o-mini',
},
// ─── Email From / Reply-To ──────────────────────────────────────────────
// ─── Email - From / Reply-To ──────────────────────────────────────────────
{
key: 'email_from_name',
section: 'email.from',
@@ -364,7 +364,7 @@ export const REGISTRY: SettingEntry[] = [
placeholder: 'https://portnimara.com/supplemental',
},
// ─── Email SMTP overrides ───────────────────────────────────────────────
// ─── Email - SMTP overrides ───────────────────────────────────────────────
{
key: 'smtp_host_override',
section: 'email.smtp',
@@ -406,7 +406,7 @@ export const REGISTRY: SettingEntry[] = [
envFallback: 'SMTP_PASS',
},
// ─── Storage S3 / MinIO ─────────────────────────────────────────────────
// ─── Storage - S3 / MinIO ─────────────────────────────────────────────────
{
key: 'storage_s3_endpoint',
section: 'storage.s3',
@@ -445,7 +445,7 @@ export const REGISTRY: SettingEntry[] = [
section: 'storage.s3',
label: 'S3 access key',
description:
'IAM access key id. AES-encrypted at rest (was previously stored plaintext fixed in this migration).',
'IAM access key id. AES-encrypted at rest (was previously stored plaintext - fixed in this migration).',
type: 'password',
scope: 'global',
encrypted: true,
@@ -474,13 +474,13 @@ export const REGISTRY: SettingEntry[] = [
defaultValue: false,
},
// ─── Storage Filesystem (single-node only) ──────────────────────────────
// ─── Storage - Filesystem (single-node only) ──────────────────────────────
{
key: 'storage_filesystem_root',
section: 'storage.filesystem',
label: 'Filesystem root path',
description:
'Absolute directory where files land when the active backend is filesystem. Single-node deployments only multi-node MUST use S3.',
'Absolute directory where files land when the active backend is filesystem. Single-node deployments only - multi-node MUST use S3.',
type: 'string',
scope: 'global',
placeholder: '/var/lib/pn-crm/files',
@@ -552,7 +552,7 @@ export const REGISTRY: SettingEntry[] = [
section: 'pulse',
label: 'Signal: document declined (risk)',
description:
'Default on. Strongest cooling signal client refused to sign an EOI / contract / reservation. Requires the risk-data wiring shipped alongside Phase 2 to populate.',
'Default on. Strongest cooling signal - client refused to sign an EOI / contract / reservation. Requires the risk-data wiring shipped alongside Phase 2 to populate.',
type: 'boolean',
scope: 'port',
},
@@ -600,7 +600,7 @@ export const REGISTRY: SettingEntry[] = [
placeholder: 'Cold',
},
// ─── Residential partner forwarding ──────────────────────────────────────
// ─── Residential - partner forwarding ──────────────────────────────────────
{
key: 'residential_partner_recipients',
section: 'residential.partner',

View File

@@ -12,7 +12,7 @@ import type { ResolvedSetting, SettingEntry, SettingSource } from './types';
/**
* Stored shape for encrypted JSONB values. The encrypt() helper returns a
* JSON string of this shape we wrap it in the JSONB column verbatim.
* JSON string of this shape - we wrap it in the JSONB column verbatim.
*/
interface EncryptedEnvelope {
iv: string;
@@ -32,7 +32,7 @@ function isEncryptedEnvelope(value: unknown): value is EncryptedEnvelope {
/**
* Validator inferred from the entry type when no explicit `validator` is set.
* Keeps the registry concise only override when standard rules don't fit.
* Keeps the registry concise - only override when standard rules don't fit.
*/
function defaultValidator(entry: SettingEntry): z.ZodTypeAny {
if (entry.validator) return entry.validator;
@@ -163,7 +163,7 @@ export async function getSetting<T = unknown>(
}
/**
* Batch resolver efficient for the admin form which needs every field in a
* Batch resolver - efficient for the admin form which needs every field in a
* section. Returns a map keyed by setting key.
*/
export async function resolveSettings(
@@ -222,7 +222,7 @@ export async function writeSetting(
// Empty value on a settable field == delete the row (revert to fallback).
// Sensitive/encrypted: empty input means "don't change" rather than
// "revert" UI shows ••• placeholder so an unchanged save shouldn't
// "revert" - UI shows ••• placeholder so an unchanged save shouldn't
// wipe the stored ciphertext. The dedicated DELETE endpoint exists for
// explicit reverts.
if (rawValue === '' || rawValue == null) {
@@ -276,7 +276,7 @@ export async function writeSetting(
},
});
// Audit-log with redaction for sensitive / encrypted fields fixes AU-02
// Audit-log with redaction for sensitive / encrypted fields - fixes AU-02
// (encrypted ciphertext stored in audit_logs.new_value).
const isSecret = entry.encrypted || entry.sensitive;
void createAuditLog({

View File

@@ -32,11 +32,11 @@ export interface SettingEntry {
section: string;
/** UI label. */
label: string;
/** UI description (plain text can include backticks for inline code). */
/** UI description (plain text - can include backticks for inline code). */
description: string;
/** Drives both validation and form input rendering. */
type: SettingType;
/** select-only list of option choices. */
/** select-only - list of option choices. */
options?: SettingOption[];
/** Optional Zod schema. When omitted, a type-appropriate default is used. */
validator?: z.ZodTypeAny;
@@ -53,7 +53,7 @@ export interface SettingEntry {
/**
* Sensitive: never surface cleartext via admin API. Encrypted fields are
* sensitive by default; non-encrypted fields may still be marked sensitive
* (e.g. an externally-shared secret) the API emits `<key>IsSet: boolean`.
* (e.g. an externally-shared secret) - the API emits `<key>IsSet: boolean`.
*/
sensitive?: boolean;
/** Placeholder text shown in the input. */
@@ -64,7 +64,7 @@ export interface SettingEntry {
* Resolved-value response shape returned to the admin UI.
*
* - `source` tells the UI whether the value came from the port row, the
* global row, the env fallback, or the registry default which drives
* global row, the env fallback, or the registry default - which drives
* the "Using env fallback" badge.
* - `value` carries the resolved cleartext for non-sensitive fields. For
* sensitive / encrypted fields it is omitted and only `isSet` is exposed.