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

@@ -1,7 +1,8 @@
'use client';
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { AlertTriangle } from 'lucide-react';
import { AlertTriangle, X } from 'lucide-react';
import { apiFetch } from '@/lib/api/client';
@@ -10,6 +11,8 @@ interface DevFlags {
isDev: boolean;
}
const DISMISS_KEY = 'pn-crm.devBanner.dismissed';
/**
* Single-line warning banner shown across the app whenever a dev-mode
* safety net is active (today: `EMAIL_REDIRECT_TO`). Sticky at the top
@@ -19,18 +22,35 @@ interface DevFlags {
* Production hides the banner entirely because env.ts refuses to boot
* with EMAIL_REDIRECT_TO set when NODE_ENV=production — the flag is
* only ever non-null in dev / staging.
*
* Dismissal is persisted in localStorage keyed by the redirect address —
* changing `EMAIL_REDIRECT_TO` re-shows the banner so the new target
* can't be silently inherited.
*/
export function DevModeBanner() {
const { data } = useQuery<{ data: DevFlags }>({
queryKey: ['internal', 'dev-flags'],
queryFn: () => apiFetch<{ data: DevFlags }>('/api/v1/internal/dev-flags'),
staleTime: 5 * 60_000,
// Don't refetch on focus; the flag changes only on a restart.
refetchOnWindowFocus: false,
});
const redirect = data?.data?.emailRedirectTo;
if (!redirect) return null;
const [overrideDismissed, setOverrideDismissed] = useState(false);
const persistedDismissed =
typeof window !== 'undefined' && !!redirect
? window.localStorage.getItem(DISMISS_KEY) === redirect
: false;
const dismissed = overrideDismissed || persistedDismissed;
if (!redirect || dismissed) return null;
const handleDismiss = () => {
if (typeof window !== 'undefined') {
window.localStorage.setItem(DISMISS_KEY, redirect);
}
setOverrideDismissed(true);
};
return (
<div
@@ -42,6 +62,14 @@ export function DevModeBanner() {
<span>
Dev mode: outbound emails redirected to <code className="font-mono">{redirect}</code>
</span>
<button
type="button"
onClick={handleDismiss}
aria-label="Dismiss dev mode banner"
className="ml-2 inline-flex size-5 shrink-0 items-center justify-center rounded hover:bg-amber-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-500"
>
<X className="size-3.5" aria-hidden />
</button>
</div>
);
}

View File

@@ -214,9 +214,13 @@ export function EntityActivityFeed({ endpoint, emptyText = 'No activity yet.' }:
No activity matches the current filters.
</div>
) : (
<ol className="relative border-l border-muted-foreground/20 ml-3 pl-6 space-y-4 py-2">
<ol className="relative ml-3 pl-6 space-y-4 py-2">
{groups.map((group, gi) => (
<SessionGroupItem key={`${group.actorKey}-${gi}`} group={group} />
<SessionGroupItem
key={`${group.actorKey}-${gi}`}
group={group}
isLast={gi === groups.length - 1}
/>
))}
</ol>
)}
@@ -224,15 +228,25 @@ export function EntityActivityFeed({ endpoint, emptyText = 'No activity yet.' }:
);
}
function SessionGroupItem({ group }: { group: SessionGroup }) {
function SessionGroupItem({ group, isLast }: { group: SessionGroup; isLast: boolean }) {
const [expanded, setExpanded] = useState(group.rows.length <= 3);
const first = group.rows[0]!;
const created = new Date(first.createdAt);
const ago = formatDistanceToNow(created, { addSuffix: true });
// Vertical connector — runs from below this bubble down to the next item,
// omitted on the last item so the line never trails past the last bubble.
const connector = !isLast ? (
<span
aria-hidden
className="absolute left-[-26px] top-3 bottom-[-1rem] w-px bg-muted-foreground/20"
/>
) : null;
if (group.rows.length === 1) {
return (
<li className="relative">
{connector}
<span className="absolute left-[-31px] top-1 h-2.5 w-2.5 rounded-full bg-primary/70 ring-2 ring-background" />
<RowBody row={first} actor={group.actorLabel} ago={ago} />
</li>
@@ -241,6 +255,7 @@ function SessionGroupItem({ group }: { group: SessionGroup }) {
return (
<li className="relative">
{connector}
<span className="absolute left-[-31px] top-1 h-2.5 w-2.5 rounded-full bg-primary/70 ring-2 ring-background" />
<button
type="button"