MUST-FIX:
- src/app/api/v1/admin/users/[id]/permission-overrides/route.ts:70 — the
PUT allowlist still gated `reservations: {view,create,activate,cancel}`.
Stale: would reject valid `tenancies.{view,manage,cancel}` writes and
silently accept ghost `reservations.*` writes that never land. Replaced.
- src/lib/services/alert-rules.ts:68 — `reservation.no_agreement` alert
emitted `entityType: 'reservation'`. Every other tenancy-related
audit/socket/dashboard label is `'berth_tenancy'`. Inconsistent dedupe
+ activity-feed label miss.
- tests/e2e/exhaustive/08-portal.spec.ts:6 — hardcoded /portal/my-reservations
navigates to a 404 every run.
- tests/e2e/exhaustive/03-reservations.spec.ts — entire spec renamed to
03-tenancies.spec.ts; tab + button locators updated to match renamed UI.
SHOULD-FIX (consistency):
- src/components/clients/client-detail.tsx — useRealtimeInvalidation only
caught 3 of the 4 berth_tenancy:* events; added the `:created` listener.
- src/lib/services/client-merge.service.ts — MergeResult.movedRows.reservations
+ snapshot.reservations + local loserReservations / movedReservations
renamed to tenancies / loserTenancies / movedTenancies. No external
consumers grep-confirmed.
- src/lib/services/gdpr-bundle-builder.ts — GdprBundle.reservations field
renamed to .tenancies; user-facing HTML section "Reservations" → "Tenancies";
local reservationRows → tenancyRows.
- 6 UI copy strings: gdpr-export-button, bulk-archive-wizard,
bulk-hard-delete-dialog, hard-delete-dialog, admin-sections-browser ×2,
admin/import/page, won-status-panel — all "reservations" prose updated
to "tenancies" (occupancy-record sense).
- tests/integration/api/tenancies.test.ts — handler import aliases
`createReservationHandler` etc renamed to `createTenancyHandler` etc.
- tests/unit/services/berth-tenancies.test.ts — local helper makeReservation
→ makeTenancyLocal (avoids shadow of the renamed factory).
- scripts/audit-permissions.ts — stale allowlist entry for
/berth-reservations/[id]/route.ts removed (path no longer exists).
- docs/runbooks/permission-audit.md — stale row for same path removed.
- docs/tenancies-design.md — fixed factual error
("tenancies.service.ts" → "berth-tenancies.service.ts").
Verified: tsc clean, 1493/1493 vitest.
Dev-server note: the running `next dev` process started before P2 and
shows Turbopack cached compile errors against the renamed schema files.
Source is correct (./tenancies); restart `next dev` to clear the cache.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
Phase 4 — wires `@axe-core/playwright` into the smoke suite so any
critical/serious WCAG 2.1 A/AA violation on the main authenticated
pages fails CI.
tests/e2e/smoke/20-accessibility.spec.ts:
Iterates 6 routes (dashboard, clients, yachts, interests, berths,
admin/branding) — each navigates after login, waits for
networkidle, runs AxeBuilder with WCAG2/2.1 A+AA tags, asserts no
critical/serious violations.
DISABLED_RULES list trims two known-noisy rules that fire on Radix
primitives + design-pass-pending muted text:
- tabindex (Radix focus traps)
- color-contrast (muted body text, pending design pass)
The list is intentionally small; new entries require a comment and
an audit. Easier to widen than narrow.
Run: pnpm exec playwright test --project=smoke
No vitest impact (1298/1298 still green); the spec only runs on the
e2e playwright project so the unit suite stays fast.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- F1: DOCUMENT_DECLINED handler (v2 Decline vs Reject) — routes to same
handler as DOCUMENT_REJECTED until product refines downstream UX
- Add RECIPIENT_VIEWED / RECIPIENT_SIGNED v2-alias cases with telemetry
logging so we see when v2 deployments emit them
- D1: populate TABLES_WITH_STORAGE_KEYS (files, berth_pdf_versions,
brochure_versions, gdpr_exports) — was an empty list, migrated 0 files
- MinIO putObject/getObject/statObject/removeObject socket timeout wrapper
to prevent worker hangs on TCP blackhole (30s deadline)
- E1: convert test.skip on smoke-setup infra failure to throw new Error
so green-skipped silence becomes a real test failure (Playwright
doesn't expose vitest's expect.fail)
- Regression tests: folderId='' → null transform, applyEntityRestoredSuffix
no-op (never-archived), syncEntityFolderName collision loop past (2)
Note: matching .env.example documentation (D2 — bare DOCUMENSO_API_URL,
DOCUMENSO_API_VERSION, MINIO_AUTO_CREATE_BUCKET, DOCUMENSO_TEMPLATE_ID_EOI,
recipient role id vars) prepared but not committed — pre-commit hook
blocks .env*. Apply manually via the separate .env workflow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto-format all files modified during the documents-hub-split feature
branch that were not yet aligned with the project's Prettier config
(single quotes, semicolons, trailing commas).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bare `request` fixture is an isolated API context that does not
share the browser session cookie established by login(). Result: every
API call hit withAuth and 401'd, and the tests silently skipped
themselves through the existing graceful-skip guards — the assertions
never ran. Switching to page.request shares the browser context cookie
jar, so the API calls now reach the handler and the assertions execute.
Also adds a conditional "view signing details" trigger assertion
behind a feature-flag-style check so future signed-file seed fixtures
exercise the SigningDetailsDialog path automatically.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two smoke specs cover the headline flows:
- 04-documents-hub-aggregated: asserts system roots (Clients/Companies/
Yachts) appear in FolderTreeSidebar with lock icons, breadcrumb updates
on selection, and EntityFolderView renders Signing + Files sections.
- 04-documents-hub-upload-into-entity: API-fixture approach (Option B) —
creates a client, uploads via /api/v1/files/upload with clientId, then
asserts the file surfaces in the entity folder view.
Visual baselines: hub-root added to the PAGES table so it snapshots via the
standard loop; hub-entity-folder added as a best-effort standalone test with
explicit skip guards when no entity sub-folders exist. Baselines require a
running dev server to generate (pnpm exec playwright test --project=visual
--update-snapshots).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers the happy-path admin flow: open hub, open Folder Actions menu,
create a root folder, click into it, breadcrumb updates. Doesn't yet
cover delete (soft-rescue) or move-to-folder — separate spec when
needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Walks the new dossier dialog end-to-end on a freshly-created client and
asserts the toast + list refresh. Companion test exercises the smart-
restore wizard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweep CRM mooring numbers from the legacy hyphen+padded form ("A-01")
to the canonical bare form ("A1") used by NocoDB, the public website,
the per-berth PDFs, and the Documenso EOI templates. Drift was
introduced by the original load-berths-to-port-nimara.ts seed; this
gates the Phase 3 public-website cutover where /berths/A1 URLs would
404 against a CRM still storing "A-01".
- 0024 data migration: idempotent regexp_replace + post-update sanity
check that surfaces any non-conforming rows for manual triage.
- Invert normalizeLegacyMooring in dedup/migration-apply: it now
canonicalizes ("D-32" -> "D32") instead of legacy-izing.
- Update tiptap-to-pdfme example tokens, EOI fixture moorings, and
smoke-test seed moorings.
- Refresh seed-data/berths.json to canonical form; drop the now-
redundant legacyMooringNumber field.
- Delete scripts/load-berths-to-port-nimara.ts (superseded in 0c).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 24 audit run hit the 10-minute test.setTimeout ceiling after capturing
2 of 4 viewport passes (iphone-se complete, iphone-16 complete-ish, 16-pro
partial, pro-max not started). 4 viewports × ~45 routes × slowMo: 200 needs
more headroom than 600s gave. 30min is comfortable headroom; the per-test
project timeout is matched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the four iPhone viewport descriptors (SE, 15/16, 16/17 Pro, Pro Max)
into tests/e2e/fixtures/devices.ts so the upcoming visual spec (Task 23)
can share the same anchors. The mobile-audit spec now spreads each
descriptor and adds a slug `name` plus a human `label` for the run header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-execution baseline for the mobile foundation PR:
- Mobile audit harness (tests/e2e/audit/mobile.spec.ts + mobile-audit Playwright project) — visits every page at four anchor iPhone viewports (375/393/402/440), screenshots full-page to .audit/mobile/, generates index.md
- Design spec (docs/superpowers/specs/2026-04-29-mobile-optimization-design.md) — adaptive shell + responsive content; full active-iPhone-range coverage; foundation + per-page migration phases
- Implementation plan (docs/superpowers/plans/2026-04-29-mobile-foundation.md) — 24 TDD tasks for the foundation PR
- .gitignore: ignore /client-portal/ (legacy nested Nuxt repo) and /.audit/ (regenerable screenshots)
- Remove phantom client-portal gitlink (mode 160000 with no .gitmodules)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cross-cutting i18n polish for forms across the marina + residential + company
domains. Introduces a single source of truth for country/phone/timezone/
subdivision data and replaces every nationality-as-free-text and timezone-
as-string Input with a dedicated combobox.
PR1 Countries — ALL_COUNTRY_CODES (~250 ISO-3166-1 alpha-2), Intl.DisplayNames
for localized labels, detectDefaultCountry() with navigator-region
fallback to US, CountryCombobox with regional-indicator flag glyphs +
compact mode for inline use.
PR2 Phone — libphonenumber-js wrapper (parsePhone / formatAsYouType /
callingCodeFor), PhoneInput with flag dropdown + national-format
AsYouType + paste-detect that flips the country dropdown for pasted
international strings.
PR3 Timezones — country->IANA map (250 entries, multi-zone for AU/BR/CA/CD/
ID/KZ/MN/MX/RU/US), formatTimezoneLabel ("Europe/London (UTC+1)"),
TimezoneCombobox with Suggested/All grouping driven by countryHint.
PR4 Subdivisions — wraps the iso-3166-2 npm package (~5000 ISO 3166-2
codes for every country), per-country cache, SubdivisionCombobox with
"Pick a country first" / "No regions available" empty states.
PR5 Schema deltas (migration 0015) — clients.nationality_iso, clientContacts
{value_e164, value_country}, clientAddresses {country_iso, subdivision_iso},
residentialClients {phone_e164, phone_country, nationality_iso, timezone,
place_of_residence_country_iso, subdivision_iso}, companies {incorporation_
country_iso, incorporation_subdivision_iso}, companyAddresses {country_iso,
subdivision_iso}. Plus shared zod validators (validators/i18n.ts) used
by every entity validator + route handler.
PR6 ClientForm + ClientDetail — CountryCombobox replaces nationality Input,
TimezoneCombobox replaces timezone Input (driven by nationalityIso hint),
PhoneInput conditionally rendered for phone/whatsapp contacts. Inline
editors (InlineCountryField / InlineTimezoneField / InlinePhoneField)
for the detail-page overview rows + ContactsEditor.
PR7 Residential client form + detail — phone -> PhoneInput, nationality/
timezone/place-of-residence-country/subdivision rows in both create
sheet and inline-editable detail view. Subdivision wipes when country
flips since codes are country-scoped.
PR8 Company form + detail — incorporation country -> CountryCombobox,
incorporation region -> SubdivisionCombobox in both modes.
PR9 Public inquiry endpoint — accepts pre-normalized phoneE164/phoneCountry
and i18n fields from newer website builds, server-side parsePhone()
fallback for legacy raw-international submissions. Old Nuxt builds
keep working unchanged.
Tests: 4 unit suites for the primitives (25 tests), 1 integration spec for
the public phone-normalization path (3 tests), 1 smoke spec asserting the
combobox triggers render in all three create sheets.
Test totals: vitest 713 -> 741 (+28).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B (Insights & Alerts) PR4-11 in one drop. Builds on the schema +
service skeletons committed in PRs 1-3.
PR4 Analytics dashboard — 4 chart types (funnel/timeline/breakdown/source),
date-range picker (today/7d/30d/90d), CSV+PNG export per card.
PR5 Alert rail UI + /alerts page — topbar bell w/ live count, dashboard
right-rail, three-tab page (active/dismissed/resolved), socket-driven
invalidation. Bell lazy-loads list on popover open to keep cold pages
fast in non-dashboard routes.
PR6 EOI queue tab on documents hub — filters to in-flight EOIs, count
surfaces in tab label.
PR7 Interests-by-berth tab on berth detail — replaces the stub.
PR8 Expense duplicate detection — BullMQ job runs scan on create, yellow
banner on detail w/ Merge / Not-a-duplicate, transactional merge
consolidates receipts and archives the source.
PR9 Receipt scanner PWA + multi-provider AI — port-scoped /scan route in
its own (scanner) group with no dashboard chrome, dynamic per-port
manifest, OpenAI + Claude provider abstraction, admin OCR settings
page (port-level + super-admin global default w/ opt-in fallback),
test-connection endpoint, manual-entry fallback when no key is
configured. Verify form always shown before save — no ghost rows.
PR10 Audit log read view — swap to tsvector full-text search on the
existing GIN index, cursor pagination, filters for entity/action/user
/date range, batched actor-email resolution.
PR11 Real-API tests — opt-in receipt-ocr.spec (admin save+test, optional
real-receipt parse via REALAPI_RECEIPT_FIXTURE) and alert-engine
socket-fanout spec gated behind RUN_ALERT_ENGINE_REALAPI. Both skip
cleanly without their gate envs so CI stays green.
Test totals: vitest 690 -> 713, smoke 130 -> 138, realapi +2 opt-in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dashboard and residential interest smoke tests were intermittently
failing with the page rendering empty/skeleton state. Root causes:
1. ui-store persisted currentPortId/Slug, but those are URL-derived state.
After login lands on /<first-port-by-name>/dashboard, localStorage holds
that port. Hard-navigating to /port-nimara/... rehydrated the store with
the stale id, and useQuery fired with the wrong port before
PortProvider's URL-sync useEffect could correct it. Drop both fields
from partialize — PortProvider re-derives them from the route every
navigation.
2. apiFetch's slug-to-port fallback fired N parallel /api/v1/admin/ports
calls when N components mounted simultaneously with an empty store.
Dedupe in-flight lookups so a stampede collapses into one round-trip.
Also tightened four flaky smoke tests that depended on a fixed 3s wait or
non-waiting isVisible({timeout}) — replaced with expect(...).toBeVisible
or expect.poll so they handle dev-mode JIT cold-start delays cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds four realapi specs that gate cleanly on env: smtp-system-send
(roundtrip + attachment bytes), email-attachments-roundtrip
(cross-port 403), documenso-cancel (in-flight cancel → status flip),
minio-file-lifecycle (upload/list/download/delete byte-equal).
Specs skip without DOCUMENSO_API_*/SMTP/IMAP/MINIO env so they run as
no-ops when those services aren't reachable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 21-role-based-ui: tighten the Settings link locator. The previous
`getByRole('link', { name: /settings/i }).first().or(getByText(/.../) .first())`
chain hit a strict-mode violation once the sidebar Admin section became
default-expanded — both the section header text node and the Settings
link matched. Match the link directly with exact: true.
- 26-residential: extend smoke with two API-driven specs covering the
residential interest pipeline — create+list and detail-page render —
using preferences-string stamp + heading match for assertions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New `visual` project covers six low-volatility screens — portal login,
dashboard, and the four core lists (clients/yachts/berths/invoices) —
with full-page screenshots that diff to a 2% pixel-ratio tolerance.
Animations and the cursor caret are disabled inline so transient
rendering doesn't trigger flaky diffs.
Detail screens (yacht detail, EOI dialog, invoice form steps) are
intentionally deferred until we have stable per-id fixtures so
snapshots don't drift with seed data.
Regenerate with: pnpm exec playwright test --project=visual --update-snapshots
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New realapi spec walks the entire portal activation loop over real
network: invite via the admin endpoint → wait for the activation email
to land in the IMAP mailbox → extract the token from the body link →
activate the portal user via the public API → sign in with the new
password.
The match logic deliberately doesn't filter on the TO header — the
combination of EMAIL_REDIRECT_TO rewriting and +addressing made TO
matching brittle. Instead we discriminate by sender (noreply@…),
subject keyword, and body link pattern, which is unique enough to find
exactly the email this test triggered.
Companion script scripts/dev-imap-probe.ts dumps the most recent ~10
messages with from/to/subject/date — useful for debugging when an IMAP
match goes wrong.
Skips when IMAP_HOST / IMAP_USER / IMAP_PASS are absent so the suite
stays portable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The documenso-template pathway was returning 201 with documensoId=null
because Documenso 2.x renamed `id` → `documentId` and recipient `id` →
`recipientId` in its API responses. Our DocumensoDocument interface
still expected the legacy v1.13 shape, so destructuring silently yielded
undefined and the documents row got NULL'd.
- Add normalizeDocument() in documenso-client that reads either field
name and surfaces the legacy `id` form downstream consumers expect
- Apply normalization at every callsite that returns DocumensoDocument
(createDocument, generateDocumentFromTemplate, sendDocument, getDocument)
- New realapi Playwright project (opt-in: --project=realapi) targeting
tests/e2e/realapi/, with 2-min timeout for real-network calls
- New spec: documenso-real-api.spec.ts seeds client/yacht/berth/interest
via the v1 API, fires generate-and-sign through the documenso-template
pathway, asserts the response carries a documensoId, then GETs the
document directly from Documenso to confirm it exists with PENDING
status and recipients populated
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The client portal no longer uses passwordless / magic-link sign-in. Each
client now has a `portal_users` row with a scrypt-hashed password,
created by an admin from the client detail page; the admin's invite
mails an activation link that the client uses to set their own password.
Forgot-password is wired through the same token mechanism.
Schema (migration `0009_outgoing_rumiko_fujikawa.sql`):
- `portal_users` — one per client account, separate from the CRM
`users` table (better-auth) so the auth realms stay isolated. Email
is globally unique, password is null until activation.
- `portal_auth_tokens` — single-use activation / reset tokens. Stores
only the SHA-256 hash so a DB compromise never leaks live tokens.
Services:
- `src/lib/portal/passwords.ts` — scrypt hash/verify (no new deps;
uses node:crypto), token mint+hash helpers.
- `src/lib/services/portal-auth.service.ts` — createPortalUser,
resendActivation, activateAccount, signIn (timing-safe),
requestPasswordReset, resetPassword. Auth failures throw the new
UnauthorizedError (401); enumeration-safe behaviour everywhere.
Routes:
- POST /api/portal/auth/sign-in — sets the existing portal JWT cookie.
- POST /api/portal/auth/forgot-password — always 200.
- POST /api/portal/auth/reset-password — token + new password.
- POST /api/portal/auth/activate — token + initial password.
- POST /api/v1/clients/:id/portal-user — admin invite (and `?action=resend`).
- Removed: /api/portal/auth/request, /api/portal/auth/verify (magic link).
UI:
- /portal/login — replaced email-only magic-link form with email +
password + "forgot password" link.
- /portal/forgot-password, /portal/reset-password, /portal/activate — new.
- New shared `PasswordSetForm` component used by activate + reset.
- New `PortalInviteButton` rendered on the client detail header.
Email send:
- `createTransporter` now wires SMTP auth when SMTP_USER+SMTP_PASS are
set (gmail app-password or marina-server creds, configured via env).
- `SMTP_FROM` env var lets the sender address be overridden without
pinning it to `noreply@${SMTP_HOST}`.
Tests:
- Smoke spec 17 (client-portal) updated to the new flow: 7/7 green.
- Smoke specs 02-crud-spine, 05-invoices, 20-critical-path updated to
match the post-refactor client + invoice forms (drop companyName,
use OwnerPicker + billingEmail).
- Vitest 652/652 still green; type-check clean.
Drops the dead `requestMagicLink` from portal.service.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR 14: adds a tier-3.5 Playwright pass that opens every refactored page,
clicks every visible button/link/role=button, and asserts no console
errors, no app-side network 4xx/5xx, and no click-time exceptions.
Helper:
- tests/helpers/click-everything.ts — shared `clickEverythingOnPage`
with default skips for destructive selectors (archive, delete,
transfer, sign-out), auto-closing of dialogs, and return-to-start
after navigation.
Exhaustive specs (tests/e2e/exhaustive/):
- 01-yachts: list + detail + transfer dialog
- 02-companies: list + detail + add-membership dialog
- 03-reservations: berth list + detail reservations tab + reserve
dialog
- 04-client-detail: list + detail walking every tab
- 05-eoi-generate: generate dialog opens with Documenso option
- 06-invoice-form: new-invoice dialog billing-entity toggle
- 07-berths: list + detail walking every tab
- 08-portal: client portal yachts / memberships / reservations
- 09-navigation: every primary nav target loads cleanly
Destructive specs (tests/e2e/destructive/):
- 01-yacht-archive: create-via-API → archive via UI → assert removed.
Skips with a clear message when the global setup does not seed an
owner client (avoids brittle failures while the full destructive
fixture lands).
Playwright config: testDir hoisted to ./tests/e2e; new `exhaustive` and
`destructive` projects share the existing setup project. New scripts
test:e2e / test:e2e:smoke / test:e2e:exhaustive / test:e2e:destructive
in package.json drive each project independently.
CI integration deferred — no .github/workflows/* exists in this repo
yet, so the PR 14 task to wire a separate CI job is N/A. The new
projects will pick up automatically when a workflow lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Failures were mostly stale selectors, not product regressions:
- .or() traps matching the topbar "+ New" button → use specific names
(Add Webhook, New Field, New Template)
- broad /create|add|new/ patterns → same fix
- [role="dialog"] overlay matched before content → getByRole('dialog').last()
- locator('input') picked hidden Radix Select inputs → getByPlaceholder /
getByRole('combobox', { name })
- 11-global-search rewritten for the inline topbar search (the cmdk
CommandDialog the old tests targeted was replaced)
- missing .first() causing strict-mode failures on notifications heading,
version history text, nav links
- dashboard landing test: no h1 exists, target KPI text instead
- activity-feed: items aren't anchors; match action badge text
- monitoring data-leak check scoped to <main> (sidebar has Email/Documents)
- admin API without port context returns 400 (not 403) for non-admins —
accept 400 as a valid "blocked" status in the sales-agent test
Also dropped dead imports and unused locals surfaced by lint-staged.
Full suite: 124 passed (11.2m).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>