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

@@ -29,7 +29,7 @@ interface ActivityItem {
label: string | null;
userId: string | null;
/** Server-resolved actor display name (from user_profiles). When null,
* the actor row no longer exists render falls back to a "Unknown
* the actor row no longer exists - render falls back to a "Unknown
* user" sentinel rather than the raw UUID prefix. */
actorName: string | null;
fieldChanged: string | null;
@@ -52,6 +52,55 @@ function humanizeFieldName(name: string): string {
.replace(/\b\w/g, (c) => c.toUpperCase());
}
/** Entity type alias map for the feed labels. Most types humanize fine
* via `humanizeFieldName`, but a few read awkwardly ("Residential
* Client" is clearer than the raw enum, notes flatten to their parent). */
const ENTITY_TYPE_LABELS: Record<string, string> = {
residential_client: 'Residential client',
residential_interest: 'Residential interest',
berth_reservation: 'Berth reservation',
berth_maintenance_log: 'Berth maintenance',
berth_recommendation: 'Berth recommendation',
client_note: 'Client note',
yacht_note: 'Yacht note',
company_note: 'Company note',
interest_note: 'Interest note',
interest_qualification: 'Interest qualification',
document_send: 'Document send',
document_folder: 'Document folder',
document_template: 'Document template',
documentTemplate: 'Document template',
form_template: 'Form template',
report_template: 'Report template',
email_account: 'Email account',
email_message: 'Email message',
user_email_change: 'Email change',
custom_field_definition: 'Custom field',
custom_field_values: 'Custom field',
expense_export: 'Expense export',
gdpr_export: 'GDPR export',
qualification_criterion: 'Qualification criterion',
website_submission: 'Website submission',
webhook_inbound: 'Inbound webhook',
webhook_delivery: 'Webhook delivery',
audit_log: 'Audit log',
portal_user: 'Portal user',
portal_session: 'Portal session',
portal_auth_token: 'Portal token',
client_contact: 'Client contact',
clientContact: 'Client contact',
clientAddress: 'Client address',
companyAddress: 'Company address',
clientRelationship: 'Client relationship',
company_membership: 'Company membership',
crm_invite: 'CRM invite',
queue_job: 'Queue job',
super_admin: 'Super admin',
};
function humanizeEntityType(type: string): string {
return ENTITY_TYPE_LABELS[type] ?? humanizeFieldName(type);
}
/** Map enum-typed field values to their canonical human labels. The audit
* log stores raw enum strings (`deposit_10pct`, `lost_other_marina`); the
* feed should read like `10% Deposit`, not the wire value. */
@@ -85,13 +134,13 @@ function normalizeEnumValue(field: string, value: unknown): unknown {
* count; nulls / empty render as em-dash. */
function shortValue(value: unknown, fieldContext?: string): string {
if (fieldContext) value = normalizeEnumValue(fieldContext, value);
if (value === null || value === undefined || value === '') return '';
if (value === null || value === undefined || value === '') return '-';
if (typeof value === 'string') return value;
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
if (Array.isArray(value)) return `${value.length} item${value.length === 1 ? '' : 's'}`;
if (typeof value === 'object') {
const entries = Object.entries(value as Record<string, unknown>);
if (entries.length === 0) return '';
if (entries.length === 0) return '-';
return entries
.slice(0, 3)
.map(
@@ -199,7 +248,7 @@ function ActivityFeedInner() {
// A1: permission_denied rows on the activity feed render as a bare
// action badge with no entity name (they target `admin.X` with empty
// entityId). They're noise for the rep keep them in the audit log
// entityId). They're noise for the rep - keep them in the audit log
// page but hide them from the dashboard feed.
const items = (data ?? []).filter((i) => i.action !== 'permission_denied');
@@ -245,18 +294,23 @@ function ActivityFeedInner() {
space between them. */}
<span className="text-muted-foreground/60 mx-1.5">·</span>
<span className="text-muted-foreground text-xs capitalize">
{item.entityType}
{humanizeEntityType(item.entityType)}
</span>
</>
) : (
<>
<span className="font-medium capitalize">{item.entityType}</span>
{item.entityId && (
<span className="ml-1 text-muted-foreground font-mono text-xs">
{item.entityId.slice(0, 8)}
// No resolvable label - either the entity was
// deleted or the type isn't in the server-side
// resolver yet. Either way we never expose a
// UUID fragment: it reads as noise to the rep
// and leaks an internal identifier.
<span className="font-medium capitalize">
{humanizeEntityType(item.entityType)}
{item.entityId ? (
<span className="ml-1 text-muted-foreground text-xs font-normal">
(removed)
</span>
)}
</>
) : null}
</span>
)}
</p>
{diffLine ? (