feat(phase-b): ship analytics dashboard, alerts, scanner PWA, dedup, audit view

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>
This commit is contained in:
Matt Ciaccio
2026-04-28 17:21:55 +02:00
parent 2fa70f4582
commit f52d21df83
63 changed files with 4459 additions and 206 deletions

View File

@@ -1,5 +1,5 @@
import { test, expect } from '@playwright/test';
import { login, navigateTo, PORT_SLUG } from './helpers';
import { login, navigateTo } from './helpers';
import path from 'path';
test.describe('Document Management', () => {
@@ -26,7 +26,7 @@ test.describe('Document Management', () => {
if (await uploadBtn.isVisible({ timeout: 5_000 }).catch(() => false)) {
// Check for a file input (may be hidden)
const fileInput = page.locator('input[type="file"]').first();
if (await fileInput.count() > 0) {
if ((await fileInput.count()) > 0) {
const testFilePath = path.resolve('tests/e2e/fixtures/test-document.txt');
await fileInput.setInputFiles(testFilePath);
await page.waitForTimeout(5000);
@@ -35,7 +35,7 @@ test.describe('Document Management', () => {
await uploadBtn.click();
await page.waitForTimeout(1000);
const fileInput2 = page.locator('input[type="file"]').first();
if (await fileInput2.count() > 0) {
if ((await fileInput2.count()) > 0) {
const testFilePath = path.resolve('tests/e2e/fixtures/test-document.txt');
await fileInput2.setInputFiles(testFilePath);
await page.waitForTimeout(5000);
@@ -47,4 +47,14 @@ test.describe('Document Management', () => {
const pageContent = page.getByText(/documents|files/i).first();
await expect(pageContent).toBeVisible({ timeout: 5_000 });
});
test('documents hub shows the EOI queue tab', async ({ page }) => {
await navigateTo(page, '/documents');
await page.waitForLoadState('networkidle');
const tab = page.getByRole('tab', { name: /eoi queue/i });
await expect(tab).toBeVisible({ timeout: 10_000 });
await tab.click();
await expect(tab).toHaveAttribute('data-state', 'active');
});
});