fix(uat): batch — timeline overshoot, name-sync, reset-password, dashboard cleanup, queue/seed hygiene + alpha UAT findings doc

UAT findings landed across the last few Playwright + React Grab passes;
single grouped commit so the index doesn't fragment into 30 one-liners.

User & auth:
- `user-settings`: name now updates the avatar + topbar menu after save
  (was reading stale session).
- `me/password-reset`: 3 bugs (token validation, error response shape,
  redirect chain).
- Admin user permission-overrides route honours the same envelope as
  the rest of the admin surface.

Dashboard:
- Removed obsolete `revenue-breakdown-chart` + `dashboard-widgets-card`
  (replaced by the customisable widget grid).
- Strip `revenue_breakdown` from analytics route + use-analytics +
  service + integration test so nothing renders an empty card.
- Activity log timeline overshoot fix (`interest-timeline` +
  `entity-activity-feed`).
- Tightened tiles: active-deals, berth-heat-widget, pipeline-value, kpi-tile.
- `dev-mode-banner`: derive dismissed state synchronously instead of
  via an effect (set-state-in-effect lint rule).

Forms & lists (assorted polish):
- client / company / yacht / interest / reminder forms — validation +
  empty-state copy + tab transitions.
- companies/yachts list tweaks; berth recommender panel; qualification
  checklist; supplemental info request button.

Infra & misc:
- Queue workers (ai / email / notifications) — log shape +
  per-job timeout consistency.
- Auth / brochures / users schema small adjustments; seeds reflect
  permissions matrix changes.
- Scan shell + scanner manifest + AI admin page small fixes.
- `next.config.transpilePackages` adds `echarts`/`zrender`/`echarts-for-react`
  (recommended config from echarts-for-react inside Next).

Docs:
- `docs/superpowers/audits/alpha-uat-master.md` — single rolling
  cross-cutting UAT findings doc (per CLAUDE.md convention).
- `docs/BACKLOG.md`: dashboard stats cards (§I) + activity-log
  normalization (§J).
- 2026-05-18 audit log updated with this batch.
- `CLAUDE.md` — small manual UAT scaffold notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 15:56:11 +02:00
parent 8c669e2918
commit 449b9497ab
59 changed files with 1831 additions and 631 deletions

View File

@@ -13,6 +13,7 @@ import {
} from '@/lib/email/templates/residential-inquiry';
import { resolveSubject } from '@/lib/email/resolve-subject';
import { getBrandingShell } from '@/lib/email/branding-resolver';
import { getPortBrandingConfig, getPortEmailConfig } from '@/lib/services/port-config';
import { env } from '@/lib/env';
import { parseBody } from '@/lib/api/route-helpers';
import { errorResponse, RateLimitError, ValidationError } from '@/lib/errors';
@@ -142,12 +143,22 @@ async function sendResidentialNotifications(args: {
}): Promise<void> {
const { portId, data, crmDeepLink } = args;
const branding = await getBrandingShell(portId);
const [branding, portBrand, emailCfg] = await Promise.all([
getBrandingShell(portId),
getPortBrandingConfig(portId).catch(() => null),
getPortEmailConfig(portId).catch(() => null),
]);
// Use the port's configured From address (or branding app name) for
// the "contact us" line on the confirmation email so other ports don't
// direct replies to sales@portnimara.com.
const contactEmail = emailCfg?.fromAddress ?? '';
const portName = portBrand?.appName ?? 'our team';
// Client confirmation
const confirmation = await residentialClientConfirmation(
{
firstName: data.firstName,
contactEmail: 'sales@portnimara.com',
contactEmail,
},
{ branding },
);
@@ -155,7 +166,7 @@ async function sendResidentialNotifications(args: {
key: 'residential_inquiry_client_confirmation',
portId,
fallback: confirmation.subject,
tokens: { portName: 'Port Nimara', recipientName: data.firstName },
tokens: { portName, recipientName: data.firstName },
});
await sendEmail(data.email, confirmationSubject, confirmation.html, undefined, undefined, portId);
@@ -204,7 +215,7 @@ async function sendResidentialNotifications(args: {
portId,
fallback: alert.subject,
tokens: {
portName: 'Port Nimara',
portName,
clientName: `${data.firstName} ${data.lastName}`.trim(),
email: data.email,
phone: data.phone,

View File

@@ -46,7 +46,7 @@ const ALLOWED_RESOURCE_ACTIONS: Record<string, Set<string>> = {
'generate_eoi',
'export',
]),
berths: new Set(['view', 'edit', 'import', 'manage_waiting_list']),
berths: new Set(['view', 'edit', 'import', 'manage_waiting_list', 'update_prices']),
documents: new Set([
'view',
'create',

View File

@@ -7,7 +7,6 @@ import {
getLeadSourceAttribution,
getOccupancyTimeline,
getPipelineFunnel,
getRevenueBreakdown,
type DateRange,
type MetricBase,
type PresetDateRange,
@@ -16,7 +15,6 @@ import {
const METRICS: Record<MetricBase, (portId: string, range: DateRange) => Promise<unknown>> = {
pipeline_funnel: getPipelineFunnel,
occupancy_timeline: getOccupancyTimeline,
revenue_breakdown: getRevenueBreakdown,
lead_source_attribution: getLeadSourceAttribution,
};

View File

@@ -155,7 +155,7 @@ export const POST = withAuth(
await sendSigningInvitation({
portId: ctx.portId,
portName: port?.name ?? 'Port Nimara',
portName: port?.name ?? 'the marina',
recipient: { name: target.signerName, email: target.signerEmail },
documensoSigningUrl: target.signingUrl,
documentLabel: DOC_TYPE_LABEL[doc.documentType] ?? 'Expression of Interest',

View File

@@ -20,7 +20,9 @@ export const POST = withAuth(async (_req, ctx) => {
await auth.api.requestPasswordReset({
body: {
email: ctx.user.email,
redirectTo: '/reset-password',
// /set-password is the form that actually consumes the token.
// /reset-password is the "request a link" entry point.
redirectTo: '/set-password',
},
});
return NextResponse.json({ ok: true });