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

@@ -150,6 +150,13 @@ export const documentSends = pgTable(
bounceStatus: text('bounce_status'), // 'hard' | 'soft' | 'ooo'
bounceReason: text('bounce_reason'),
bounceDetectedAt: timestamp('bounce_detected_at', { withTimezone: true }),
// Phase 4b — email open tracking. When `trackOpens` is true the send
// includes a 1×1 pixel pointing at /api/public/email-pixel/[sendId].
// `firstOpenedAt` + `openCount` are denormalised aggregates so the
// sends list can render an "opened" pill without a JOIN.
trackOpens: boolean('track_opens').notNull().default(false),
firstOpenedAt: timestamp('first_opened_at', { withTimezone: true }),
openCount: integer('open_count').notNull().default(0),
},
(t) => [
index('idx_ds_client').on(t.clientId, t.sentAt),
@@ -164,9 +171,40 @@ export const documentSends = pgTable(
],
);
/**
* Per-open log for emails with `trackOpens=true`. The 1×1 pixel
* endpoint inserts here on every fetch (Apple Mail privacy proxy will
* over-count; most other clients under-count when images are blocked —
* this is the universal email-tracking caveat). Cached aggregates on
* `document_sends` keep list rendering fast.
*/
export const documentSendOpens = pgTable(
'document_send_opens',
{
id: text('id')
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
portId: text('port_id')
.notNull()
.references(() => ports.id),
sendId: text('send_id')
.notNull()
.references(() => documentSends.id, { onDelete: 'cascade' }),
openedAt: timestamp('opened_at', { withTimezone: true }).notNull().defaultNow(),
userAgent: text('user_agent'),
referer: text('referer'),
},
(t) => [
index('idx_dso_send').on(t.sendId, t.openedAt),
index('idx_dso_port').on(t.portId, t.openedAt),
],
);
export type Brochure = typeof brochures.$inferSelect;
export type NewBrochure = typeof brochures.$inferInsert;
export type BrochureVersion = typeof brochureVersions.$inferSelect;
export type NewBrochureVersion = typeof brochureVersions.$inferInsert;
export type DocumentSend = typeof documentSends.$inferSelect;
export type NewDocumentSend = typeof documentSends.$inferInsert;
export type DocumentSendOpen = typeof documentSendOpens.$inferSelect;
export type NewDocumentSendOpen = typeof documentSendOpens.$inferInsert;

View File

@@ -31,6 +31,15 @@ export type RolePermissions = {
edit: boolean;
import: boolean;
manage_waiting_list: boolean;
/**
* Update berth `price` / `priceCurrency` via the dedicated single
* (`PATCH /api/v1/berths/[id]/price`) and bulk
* (`POST /api/v1/berths/bulk-update-prices`) endpoints. Carved out
* from generic `berths.edit` so admins can grant sales reps the
* ability to retune prices without exposing the full berth-edit
* surface (dimensions, mooring type, etc.). Always audited.
*/
update_prices: boolean;
};
documents: {
view: boolean;