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

@@ -10,6 +10,15 @@ const cancelBodySchema = z
.object({
reason: z.string().max(2000).optional().nullable(),
notifyRecipients: z.array(z.string().uuid()).max(20).optional(),
/**
* Whether to also DELETE the document from Documenso. `delete` (the
* default) frees the upstream envelope slot - useful for unclogging
* the Documenso log when a draft was abandoned. `keep_remote`
* leaves the envelope intact for audit purposes; only the local
* row is marked `cancelled`. Audit-trail copy on the cancelled-doc
* badge changes accordingly.
*/
cancelMode: z.enum(['delete', 'keep_remote']).optional(),
})
.strict()
.optional();
@@ -17,7 +26,7 @@ const cancelBodySchema = z
export const POST = withAuth(
withPermission('documents', 'edit', async (req, ctx, params) => {
try {
// Body is optional legacy callers POST with `{}`. parseBody returns
// Body is optional - legacy callers POST with `{}`. parseBody returns
// null when the request has no body; default to empty options.
let body: z.infer<typeof cancelBodySchema> = undefined;
try {
@@ -37,6 +46,7 @@ export const POST = withAuth(
{
reason: body?.reason ?? null,
notifyRecipients: body?.notifyRecipients ?? [],
cancelMode: body?.cancelMode ?? 'delete',
},
);
return NextResponse.json({ data: doc });

View File

@@ -8,7 +8,7 @@
* Lookup is keyed off the doc id; the slug embeds the current folder path +
* filename so a forwarded link reads like `Deals 2026/Q1/contract.pdf` even
* though the underlying storage key is a UUID. The slug is rebuilt from
* current state and compared with the supplied path a stale or
* current state and compared with the supplied path - a stale or
* hand-edited URL 404s rather than silently serving the wrong file.
*/

View File

@@ -11,7 +11,7 @@ import { createAuditLog } from '@/lib/audit';
/**
* Per-document move endpoint. Moving a single document is a deliberate
* user action so we DO bump `updated_at` here different semantics from
* user action so we DO bump `updated_at` here - different semantics from
* the bulk soft-rescue in `deleteFolderSoftRescue` where the timestamp
* stays put because reps did not act on the individual documents.
*

View File

@@ -15,7 +15,7 @@ import { getPortDocumensoConfig } from '@/lib/services/port-config';
import { errorResponse, NotFoundError, ValidationError } from '@/lib/errors';
const bodySchema = z.object({
/** Optional defaults to the next pending signer in signing-order. */
/** Optional - defaults to the next pending signer in signing-order. */
recipientId: z.string().optional(),
});
@@ -63,7 +63,7 @@ export const POST = withAuth(
// Self-heal flow when target.signingUrl is null. Two scenarios:
// 1. Envelope was created before the auto-distribute fix shipped
// never distributed, so we must call /envelope/distribute
// - never distributed, so we must call /envelope/distribute
// to mint URLs.
// 2. Envelope WAS auto-distributed at generate time, but the
// response we got didn't carry signingUrls into our DB row
@@ -74,7 +74,7 @@ export const POST = withAuth(
// Defensive flow: try `getEnvelope` FIRST (cheap, always works).
// If recipients carry signingUrls, persist + skip distribute.
// If not, fall through to distribute, but catch 4xx so we don't
// surface a confusing "Documenso upstream error" to the rep
// surface a confusing "Documenso upstream error" to the rep -
// instead we re-fetch via GET one more time and accept whatever
// URLs the envelope has.
if (!target.signingUrl && doc.documensoId) {
@@ -116,7 +116,7 @@ export const POST = withAuth(
recovered = true;
}
} catch {
// ignore fall through to distribute attempt
// ignore - fall through to distribute attempt
}
// Step 2: distribute, only if GET didn't recover URLs.
@@ -125,7 +125,7 @@ export const POST = withAuth(
const distributed = await distributeEnvelopeV2(doc.documensoId, ctx.portId);
await persistUrlsForDocument(distributed.recipients);
} catch {
// Probably "already distributed" last-ditch GET.
// Probably "already distributed" - last-ditch GET.
try {
const fetched = await getDocument(doc.documensoId, ctx.portId);
await persistUrlsForDocument(fetched.recipients);
@@ -146,7 +146,7 @@ export const POST = withAuth(
if (!target.signingUrl) {
throw new ValidationError(
'Signer has no Documenso URL yet try regenerating the EOI; v2 envelopes require distribution before the signing link exists.',
'Signer has no Documenso URL yet - try regenerating the EOI; v2 envelopes require distribution before the signing link exists.',
);
}
@@ -161,7 +161,7 @@ export const POST = withAuth(
documentLabel: DOC_TYPE_LABEL[doc.documentType] ?? 'Expression of Interest',
signerRole: (target.signerRole as SignerRole) ?? 'client',
senderName: docCfg.developerName ?? null,
// Phase 6 surface the per-doc rep-authored note when set so
// Phase 6 - surface the per-doc rep-authored note when set so
// every cascaded invite and any manual resend show the same
// copy. Falls back to the template default when null/empty.
customMessage: doc.invitationMessage,

View File

@@ -6,19 +6,19 @@ import { detectFields } from '@/lib/services/document-field-detector';
import { isPdfMagic } from '@/lib/services/berth-pdf-parser';
/**
* Phase 4 Auto-detect anchor scanner endpoint.
* Phase 4 - Auto-detect anchor scanner endpoint.
*
* POST `/api/v1/documents/auto-detect-fields`
*
* Body: multipart/form-data
* - file: the source PDF the rep just uploaded
*
* Returns: `{ data: { fields: DetectedField[] } }` seed state for the
* Returns: `{ data: { fields: DetectedField[] } }` - seed state for the
* drag-drop overlay. Empty array when the PDF has no extractable text
* (image-only scan) the dialog falls back to manual placement
* (image-only scan) - the dialog falls back to manual placement
* without an error toast.
*
* Permission: documents.send_for_signing the only flow that calls
* Permission: documents.send_for_signing - the only flow that calls
* this endpoint is the upload-for-signing dialog, which already
* requires that bit. Reusing it here means a custom role with the
* upload bit but no send bit can't dry-run the detector to pull

View File

@@ -10,11 +10,11 @@ import { getEoiTemplateSyncReport } from '@/lib/services/documenso-template-sync
*
* Returns the per-port developer + approver defaults the
* UploadForSigningDialog uses to prefill the recipient configurator.
* No secrets are exposed just the display name, email, and the
* No secrets are exposed - just the display name, email, and the
* sendMode flag so the UI can show the right CTA copy ("Send now" vs
* "Save draft, send manually").
*
* Permission: documents.send_for_signing the only caller is the
* Permission: documents.send_for_signing - the only caller is the
* upload-for-signing dialog which already requires this permission to
* complete the flow.
*/
@@ -25,7 +25,7 @@ export const GET = withAuth(
// Signing order resolution chain (highest → lowest priority):
// 1. Cached `documento_eoi_template_sync_report.templateMeta.signingOrder`
// populated by the admin "Sync from Documenso" button and
// - populated by the admin "Sync from Documenso" button and
// represents the live template's bound order. On v2 this is the
// authoritative value because `/template/use` doesn't accept a
// per-call override.
@@ -53,7 +53,7 @@ export const GET = withAuth(
signingOrder,
// Surface where the value came from so the UI tooltip can be
// honest about the source. Helps reps debug "I changed it in
// Documenso but the CRM still says X" they need to re-run
// Documenso but the CRM still says X" - they need to re-run
// Sync to pull the change.
signingOrderSource: syncReport?.templateMeta?.signingOrder
? 'template'