H15: new applyView({filters,sort}) atomic mutator (one URL write) restores a
saved view's sort, threaded through all six list components instead of being
discarded. H14: a guarded effect resyncs page/sort/filters FROM the URL on
Back/Forward; the resync setStates carry a scoped, justified
set-state-in-effect disable (loop-guarded external-URL sync).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
createPayment/updatePayment now store refunds as a negative magnitude, and
every financial reader (sumPaymentsInRange, getRevenueByMonth, getCashFlow)
subtracts refund magnitude regardless of stored sign — fixing both new rows
and legacy positive-stored refunds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extracts transferOwnershipTx (close open yacht_ownership_history row + open
a new one + update denormalized owner) from transferOwnership, and uses it in
client-archive + client-restore instead of writing only the denormalized
columns — which left the ledger showing the old owner as current and let the
next real transfer close the wrong row.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Layer 1: createReportRun rejects a user-triggered run whose coverBrandPortId
is a port the triggering user can't access (userCanAccessPort: super-admin or
userPortRoles membership). Layer 2: renderReportRun only honors the override
when it equals run.portId or the run's user is a member, else falls back to
the source port's branding — so a forged/scheduled config can't leak another
tenant's logo/name.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds sanitizeCsvCell() (prefixes a quote when a cell starts with = + - @
tab/CR) and applies it to the audit-export escape() and the user-controlled
free-text columns of the expense export before Papa.unparse.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Applies withRateLimit('ai') to all three AI routes (mirroring scan-receipt)
and adds a checkBudget gate before the OpenAI call in generateEmailDraft,
falling back to the template draft when the per-port budget is exhausted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
H4: reservation_agreement completion fired the contract_signed berth rule,
flipping the berth to 'sold' one-to-two stages early. Add a dedicated
reservation_signed berth trigger (defaults to under_offer) and fire it.
H13: the manual signed-EOI upload path advanced only to 'eoi' via the
ungated helper while the Documenso-webhook path advanced to 'reservation';
both now use advanceStageIfBehindGated(..., 'reservation', 'eoi_signed') so
manually- and webhook-signed deals reach the same stage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge now re-points the loser's payments, company memberships (deduped
against unique_cm_exact), polymorphic yacht ownership, and polymorphic
invoice billing-entity to the winner inside the same transaction, before
archiving the loser. H2: the winner no longer silently loses those rows.
H3: because payments (notNull onDelete:cascade) are moved off the loser, a
later hard-delete of the archived loser can no longer cascade-delete the
winner's financial history. Counts wired into the merge result + audit row.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
H8: enabling the residentialAccess flag grants the full residential CRUD
set, so a non-super-admin caller must now hold those leaves themselves to
grant it — closes the escalation back door around the role-superset check.
M12: an admin can no longer change their OWN isActive / roleId /
residentialAccess (self-lockout / self-escalation), mirroring the
permission-override route's self-target block.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
H1: webhook delivery fetch now uses redirect:'manual' and refuses to read
or expose a redirected (un-revalidated) response, closing the SSRF read
primitive. H6: dashboard report queries matched title-case 'Sold'/'Under
offer' that never match the lowercase canonical, silently reporting 0 sold
/ understated occupancy — now lowercase. H7: NotesList maps the entityType
discriminator to its REST path (residential_* -> residential/clients|
interests) instead of interpolating the raw underscore, which 404'd every
residential notes request.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds assertResidentialModuleEnabled(ctx.portId) as the first statement in
every residential v1 handler (24 handlers across 13 files), mirroring the
Tenancies pattern. Previously the disabled-module state was enforced only
in the page layout, so a disabled module still accepted API writes
(including partner-forward emails on residential interest creation).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
C1: getDepositTotalForInterest now filters to the interest's
depositExpectedCurrency for the auto-advance gate, so a wrong-currency
payment can no longer satisfy the deposit expectation (and mark the berth
Sold). C2: setInterestOutcome fires interest_completed only for 'won';
lost/cancelled fire a new 'deal_lost' rule that frees the berth instead of
flipping it to 'sold'. C4: add '/q/' to proxy PUBLIC_PATHS so tracked
links in outbound mail reach external recipients.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 bite-sized TDD tasks: parseOperationalFilters (unit-tested), Area
filter threaded through the operational service + route, hasData
existence flags on all three report routes, shared ReportEmptyState
component, and per-client wiring. Verification + tracker update in the
final task.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Locked decisions from brainstorming: report-level empty states across
Sales/Operational/Financial gated on a window-independent hasData flag;
Operational gains an Area-only berth-scope filter (Status dropped as a
light filter in this report); rep/source confirmed not applicable to
Operational.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sales/Operational/Financial are built + verified; Marketing is blocked
on the website cutover (launch-readiness Init 1b), not on code. Rather
than hide the whole reports surface behind a module toggle, keep it live
for beta and 404 the one unbuilt kind so a hand-typed /reports/marketing
URL can't reach the "in development" placeholder. The landing page
already advertises only the three live reports + Custom.
Remove the UNAVAILABLE_NEW_KINDS entry when the Marketing report ships.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds the Financial report on the canonical payments + expenses tables
(the CRM records money received; it does not invoice — invoices module
is off, dev DB has zero invoice rows). The invoice-centric spec is
reframed onto the payments model: "outstanding AR" → expected-deposit
shortfall on active deals; "AR aging" → outstanding deposits bucketed by
deal age.
Service (financial.service.ts):
- 7 KPIs: revenue collected (net of refunds), deposits, balance,
pipeline expected, outstanding deposits, expenses, net contribution
- 6 chart datasets: revenue by month (deposit/balance), collection
funnel (EOI→deposit→contract→won), expected-deposit aging, cash flow
(inflow vs outflow), expense breakdown by category
- 4 tables: outstanding deposits, recent payments, refund log, expense
ledger
- every money figure normalised to port currency via a shared
resolvePortCurrency/normalizeAmount helper (new reports/currency.ts)
UI (financial-report-client.tsx): KPI strip + recharts (stacked bar /
horizontal bar / line / donut) + month/quarter/year toggle + branded
empty states; DateRangePicker + Templates + Export wired. Un-hidden the
Financial card on the reports landing.
Plumbing: added '1y' (trailing 12mo) preset to the shared range system
(financial trends want a year); added 'financial'/'marketing' to the
report-template kind enum for template parity.
TDD: 6 financial-math unit tests (aging buckets, month keys/range, net
contribution). tsc clean; full unit suite green except pre-existing
Redis/storage-dependent integration tests. Browser-verified against live
data: API 200, KPIs correct ($5,849 expenses / -$5,849 net, $0 revenue
correct given 0 payment rows), expense ledger + breakdown populate,
payment-derived sections show graceful empty states.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the two cross-cutting filter gaps in launch-readiness (rep
multi-select + source multi-select). The Sales detail tables can now be
narrowed by assigned rep and lead source alongside the existing stage /
lead-category / outcome filters.
- service: thread `assignedTo` + `sources` through the 5 filtered Sales
queries (rep-performance, stalled, closing-this-month, recent-wins,
lost-reason); add `getRepFilterOptions` for the rep dropdown's stable
option list (distinct assigned reps port-wide, window-independent).
- route: extract param parsing into a pure, unit-tested
`parseSalesFilters` helper (source allowlisted against SOURCES;
assignedTo passed through as free user-id list); return `repOptions`
in the payload.
- ui: static Source filter (SOURCES) + dynamic "Assigned to" filter
(from payload repOptions, hidden until loaded); decouple the query
builder from dynamic options via a stable FILTER_KEYS list.
TDD: 8 new parseSalesFilters unit tests (allowlist drop, free-list
passthrough, combine). tsc clean; 12/12 reports unit tests; browser-
verified both filters fire `source=`/`assignedTo=` → 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Third importer increment — the write path, fully testable without UI.
- commit.ts: commitBatch streams classified rows, applies insert/update per
the conflict policy via the adapter (each row in its own try/catch so valid
rows still land), records every action in import_batch_rows, and keeps live
counts on the batch header. undoBatch hard-deletes a batch's inserted rows
(port-scoped); a delete blocked by a dependent FK is reported, not forced,
and the batch flips to `undone` only when every inserted row was removed.
- import worker: replaced the no-op placeholder with the real processor —
loads the batch, re-reads the uploaded file from storage, parses, and runs
commitBatch under the batch's mapping + policy. Marks the batch failed on
error. Concurrency 1 so imports don't race each other's dedup lookups.
Tests: commit (skip/insert/error counts + per-row ledger + real inserted
entity), undo (removes exactly the inserted row, flips status), and
update-matches overwrite. 2 passing.
Engine is now functional end-to-end at the service layer: parse → map →
dry-run → commit → undo. Remaining: 4 FK adapters, API routes + permission,
wizard UI + history.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First increment of the importer (docs/superpowers/specs/2026-06-01-bulk-import-design.md):
the three port-scoped tables, no changes to entity tables.
- import_batches — one row per run: entity_type, filename, storage_key,
status, conflict_policy, mapping_json, live counts, created_by, timestamps.
- import_batch_rows — per-row action ledger (inserted/updated/skipped/errored)
with entity_id + error; partial index on inserted rows powers Undo.
- import_mappings — saved column mappings, unique per (port, entity, name).
Migration 0090 applied via psql; schema re-exported from the index.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three polish items so the legacy seed is one-shot and complete:
- backfill-documents: recover the ~10 pre-Documenso "LOI process" EOIs
whose signed PDF lives only as a NocoDB attachment in the `database`
MinIO bucket (the pipeline keys EOI-doc creation off documensoID, so it
never created rows for them). Reads EOI_Document attachment metadata
from the local nocodb_legacy dump, pulls the PDF (read-only) from the
`database` bucket, and CREATES the document + file + folder, linking the
signed PDF. Idempotent via a `nocodb_eoi_document` ledger entry.
- connect-berth-links: refactored into an exported connectBerthLinks()
and folded into migrate-from-nocodb --apply (best-effort; skips with a
warning if the local dump isn't restored) so the multi-berth junction is
reconnected as part of the one-shot seed, not a separate manual step.
- migration-apply: contactless legacy clients (no email/phone across the
whole dedup cluster) get a per-port "Needs contact info" tag so staff
can filter + chase them, instead of being dropped.
The current dev DB's 29 contactless clients were tagged via a one-off
mirroring the pipeline logic. EOI recovery code is ready but the actual
run needs LEGACY_MINIO_* read creds supplied at the command line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both berth-detail surfaces were stubbed/hidden behind a comment in
berth-tabs.tsx. Their backing schema already existed; this wires the UI
and fills the service gaps.
Maintenance Log (was ~60% built: schema/migration/add+get service/route):
- new edit + delete: updateMaintenanceLog / deleteMaintenanceLog service
(port-scoped tenant guard), PATCH/DELETE at maintenance/[logId], plus
updateMaintenanceLogSchema. add schema now accepts null for cost /
responsibleParty so the shared add+edit dialog sends one body shape.
- BerthMaintenanceTab: list (newest first) + add/edit dialog + delete
confirm, realtime invalidation. New berth:maintenanceUpdated/Removed
socket events.
Waiting List (un-hide the orphaned manager + next-in-line notify):
- getWaitingList now left-joins the client so the queue renders names,
not raw ids.
- WaitingListManager rewritten: ClientPicker instead of free-text id,
client names, manage_waiting_list gating on add/reorder/remove, and a
"Next in line" marker on position 1.
- notifyWaitlistNextInLine: when a berth transitions to available,
surface the #1 client to staff who hold berths.manage_waiting_list
(mirrors the interest-based notifyNextInLine; dedupeKey-suppressed).
Hooked into updateBerthStatus on any -> available transition.
Tests: maintenance add/get/update/delete + cross-port guard; waitlist
notify recipient-resolution / payload / empty + no-permission no-ops.
Verified end-to-end in the browser (create/render/delete for both).
Also adds scripts/dev-reset-admin-pw.ts (reset a synthetic user's
password via the better-auth hasher after a dev reseed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Reservation and Contract tabs reused ExternalEoiUploadDialog, but the
service hard-coded the EOI document type, status columns, stage target, and
berth rule. A signed contract uploaded from the Contract tab filed as an
`eoi`, flipped `eoi_status`, and advanced the stage to `eoi` - wrong doc
kind, wrong sub-state, wrong stage.
- external-eoi.service: UPLOAD_CONFIG keyed off docType (eoi | reservation
| contract) parameterises documentType, file category, storage prefix,
doc-status column, signed-date column, target stage, advance-from set,
and berth rule. eoi_status is written only for docType=eoi.
- route: parse docType from the form (default eoi).
- dialog: docType prop; generalised copy; EOI-only UI (active-EOI replace
banner, public-map flip, cancelActiveDocumentId) gated to docType=eoi.
- reservation/contract tabs: pass docType; drop the coming-soon comments.
- test: docType routing cases (reservation -> reservation_agreement +
reservation cols; contract -> contract + contract cols; eoi_status stays
null on both; contract idempotent at/past contract stage).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
reconcile-migration.ts: read-only cross-check of EVERY migrated record vs its
legacy source (via the ledger) — coverage (nothing dropped), field fidelity
(independently re-derives stage/eoiStatus/documensoId/berth/email), and
relationship integrity (orphans, dangling FKs).
connect-berth-links.ts: the dedup pipeline migrated only the single per-interest
Berth Number text field and missed the legacy _nc_m2m_Berths_Interests junction
(multi-berth deals) — 57 deals were missing links. Reads the junction from the
nocodb_legacy snapshot, resolves interest + berth via the ledger, inserts the
missing interest_berths rows (idempotent; respects the one-primary partial
unique index). Inserted 74 links, 51 new primaries.
After the fix: reconciliation = 0 discrepancies across all 255 deals, 165
expenses, 45 residential.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Read-only audit of migrated data:
- EOI PDF ↔ person: extracts each attached signed-EOI PDF text (unpdf), confirms
the linked client name appears, flags any PDF where a different client name
appears. Result: 35/35 strong match, 0 mismatches (visually spot-checked 2).
- Berth PDF ↔ mooring: soft text check; moorings render as graphics so the
filename→mooring attachment is authoritative (113/113; A1 visually confirmed).
- Per-person completeness: 0 deals missing stage, 0 clients without a deal,
29 clients without contact info (inherited legacy data gaps).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
backfill-documents.ts pulls signed EOI PDFs + berth spec PDFs from the legacy
MinIO (client-portal bucket; read-only via dedicated LEGACY_MINIO_* creds) and
deposits them into the CRM (getStorageBackend), linking:
- berth PDFs → berth_pdf_versions + berths.current_pdf_version_id (mooring from
filename; 113/113 matched)
- signed EOIs → documents.signed_file_id + status=completed + a files row filed
into the client folder (exact name + conservative lev<=2 fuzzy; 33 linked)
Idempotent (skips when signedFileId / current_pdf_version_id already set).
Strictly prod-READ-only; all writes local (dev storage_backend=filesystem).
Unmatched EOIs reported (mostly in-flight deals w/ no signed PDF yet + old-LOI
docs in the NocoDB attachment bucket).
Adds probe-minio.ts (read-only bucket inventory).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A single idempotent --apply now seeds the full legacy dataset:
- Expenses: fetch the separate "Expenses" NocoDB base (mxfcefkk4dqs6uq),
transform (price→amount+currency, payment status, receipt marker), apply to
the expenses table under a new nocodb_expenses ledger tag.
- Interest EOI display state: set interests.eoiStatus/eoiDocStatus from the
legacy EOI Status / LOI process so deals show signed / awaiting-signature
(in-flight) state, not only a separate documents row.
- Runner reports expenses + tags createdBy with the seeded super-admin id.
Validated via --apply on the dev DB: 239 clients (multi-deal grouping intact),
255 interests (qualified 171/eoi 51/nurturing 30/reservation 2/contract 1),
48 signed + 3 in-flight EOIs, 165 expenses (5 currencies), 41 docs + 119
signers, 45 residential. tsc clean; 67 dedup unit tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 2026-05-03 migration pipeline (src/lib/dedup/*) predates the 9→7
pipeline-stage refactor; its STAGE_MAP emitted invalid stages
(open/details_sent/eoi_sent/…) that would write bad pipeline_stage values
on --apply. Remap to the current PIPELINE_STAGES (enquiry/qualified/
nurturing/eoi/reservation/deposit_paid/contract) + a deposit-received →
deposit_paid override. Frozen-fixture test expectations updated (17/17 pass).
Validated: live --dry-run = 239 clients / 255 interests / 41 EOI docs
(matches independent snapshot analysis; pipeline is more conservative and
flags 3 borderline pairs for review).
Adds the migration design spec (source map, scope lock to Port Nimara +
Expenses bases, EOI coverage 48/48, in-flight Documenso state, remaining
gaps: interest eoiStatus, expenses, doc-blob backfill).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ship-what's-done prep ahead of the prod cutover (launch ~today):
- Hide Financial + Marketing report cards from the reports landing
(both were "Builder in development" placeholders gated on unbuilt
data sources). Sales/Operational/Custom + templates/scheduling/
exports remain live.
- Trim the Custom-report card copy to match the shipped basic builder
(no group-by/filters yet; the builder page header was already honest).
- Hide the Bulk Import mockup from search-nav-catalog + the admin
sections browser; /admin/import is now unreachable from the UI.
- Correct client-facing doc over-claims (waiting-list "next-in-line
notification", Import) in features-list.md + new-system-feature-summary.md.
- Un-stale BACKLOG.md (Documenso phases 2-7 confirmed shipped).
- Log decisions + deferred work (full importer, full custom-builder,
waiting-list, maintenance-log, paper-upload bug) to launch-readiness.md.
Deferred-importer design spec added at
docs/superpowers/specs/2026-06-01-bulk-import-design.md.
Verified: tsc --noEmit clean, eslint clean on changed files,
1512/1519 vitest pass (7 failures are Redis-down, unrelated).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a "Compare to prior period" toggle to the Sales report header.
When on, the API recomputes the KPI window for the equal-length window
immediately preceding the selected range (previousPeriodBounds) behind
`?compare=1`, and the five window-derived KPI tiles (Won, Lost, Win
rate, Avg time-to-close, New leads) render colour-correct "vs prior"
deltas. Point-in-time tiles (Active interests, Pipeline value) have no
prior-window analogue and intentionally show no delta. The prior-window
query runs in parallel with the main batch and resolves to null when the
toggle is off (zero cost). Toggle state persists in the saved-template
config.
Closes the spec's "period comparison on every report" gap for Sales;
Operational already rendered period-start deltas.
Pure helpers TDD'd: previousPeriodBounds (range.ts) +
computeSalesKpiComparison (sales-comparison.ts), 7 unit tests. tsc +
lint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a `residential_module_enabled` port setting (default ON) that
hides/disables the entire Residential surface when an admin turns it
off, mirroring the Tenancies / Invoices / Expenses module-toggle
pattern. Disabling is a soft hide — residential clients/interests are
preserved and reappear on re-enable.
Surfaces gated:
- Route guard: new residential/layout.tsx renders ModuleDisabledPage
(covers all 5 residential pages)
- Sidebar "Residential" section + mobile more-sheet tile (SSR-resolved
residentialModuleByPort threaded layout → app-shell → sidebar)
- Global search: residential client/interest buckets early-return at
the shared chokepoint so disabled-port records don't dead-end
- Public intake: /api/public/residential-inquiries 404s when off
- Admin Switch in settings-manager (writes via settings PUT)
Service TDD'd (residential-module.test.ts, 6 tests) plus a
disabled-port rejection test on the public endpoint. tsc + lint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bundles the rest of the in-flight work from this UAT round into one
checkpoint. Each sub-area is independent; see the headings below.
UAT polish (drained 11 findings from active-uat.md):
- Dialog primitive default bumped sm:max-w-xl/lg:max-w-3xl →
sm:max-w-2xl/lg:max-w-4xl so multi-field forms + PDF previews
aren't cramped at 1440-1920px.
- Notes tab badge aggregation: new countFor{Client,Yacht,Company}
Aggregated helpers in notes.service mirror the listFor*Aggregated
symmetric-reach joins. yacht-tabs + company-tabs render the
badge; client-tabs already had badge support.
- Supplemental-info form polish bundle: BrandedAuthShell gains a
`width: 'sm' | 'md'` prop (md uses min-h-dvh scroll instead of
fixed inset-0 pin so long forms scroll naturally). Form picks up
port branding (logoUrl + backgroundUrl + appName) via
loadByToken. Address fields completed (street + city + region +
postal + country). Port name eyebrow + success-state copy added.
- new-document-menu Upload-file landing toast: per-file completion
emits toast.success with action link to the destination entity
or folder.
- interest-tabs OverviewTab "from client" pill on Email + Phone
rows via new EditableRow `inheritedFrom` prop.
- create-document-wizard subject picker → segmented button strip
(5 types visible at once).
Launch infra:
- UTM column wiring (Init 1b step 4): migration
0089_website_submissions_utm.sql adds utm_source/medium/campaign/
term/content + composite index (port_id, utm_source, received_at)
for per-campaign rollups. website-inquiries intake accepts the
five fields. Residential intake intentionally untouched per audit
scope.
- Invoicing module gate (Init 1c spike): new
invoices-module.service + invoices layout guard + registry entry
invoices_module_enabled (default false). Audit conclusion in
launch-readiness.md: payments table is canonical money path;
/invoices flow is parallel infrastructure now hidden by default.
Smart-back navigation refactor:
- Replaced breadcrumb component with history-aware Back button.
New route-labels.ts + use-smart-back hook +
navigation-history-tracker so back falls through to the parent
route when there's no prior page in history.
- Sidebar / topbar / mobile-topbar adopt the new pattern; old
breadcrumb-store kept for back-compat consumers but the
breadcrumbs component is gone.
- 6 detail pages (admin/errors per-id + codes, invoices/
upload-receipts, reports kind, tenancies detail, analytics
metric, client detail) migrated.
Trackers + docs:
- docs/launch-readiness.md — master pre-launch tracker. Includes
the reports gap audit (cross-cutting filter set, Marketing +
Financial blockers, custom builder remaining entities, scheduled
CSV/XLSX, template scope picker).
- docs/superpowers/audits/active-uat.md — 15 findings flipped
OPEN → SHIPPED locally with fix-applied notes; 4 OPEN remaining
(each blocked on user input or cross-repo).
- CLAUDE.md — minor session notes carried forward.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Activity-feed: shared formatting module
(src/components/shared/activity-formatting.ts) centralises action
verbs, badge variants, entity-type labels, enum-value normalisation,
shortValue, and buildDiffLine. The dashboard widget feed and the
per-entity audit feed now both consume it - duplicate ~250 lines
collapsed, vocabularies aligned, badge palette unified.
- Signing order setting becomes tri-state. The new
TEMPLATE_DEFAULT value (the new default) skips overriding the
template's own signingOrder so each Documenso template's stored
setting wins. PARALLEL / SEQUENTIAL keep forcing the override.
- Admin Documenso page now ships a Webhook health card backed by
/api/v1/admin/documenso-webhook/health (secret status,
expected URL, last received event, recent secret rejections) and
a "Test now" button that fires a synthetic DOCUMENT_OPENED through
/api/v1/admin/documenso-webhook/test against the local receiver
to verify the full pipeline without driving a real Documenso event.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>