diff --git a/NAVIDOCS_TESTING_SESSION.md b/NAVIDOCS_TESTING_SESSION.md
new file mode 100644
index 0000000..66fa1e5
--- /dev/null
+++ b/NAVIDOCS_TESTING_SESSION.md
@@ -0,0 +1,1149 @@
+# NaviDocs - Testing Session (6 Haiku Agents)
+
+**Mission:** Comprehensive E2E testing with real user simulation using Playwright
+
+**Budget:** $5-8 (6 Haiku agents × 1-2 hours)
+**Timeline:** 2-3 hours
+**Repository:** https://github.com/dannystocker/navidocs
+
+---
+
+## Prerequisites
+
+**Must be complete before running this session:**
+- ✅ H-01 through H-15 build session complete
+- ✅ 16 database tables created
+- ✅ 5 backend APIs deployed (inventory, maintenance, cameras, contacts, expenses)
+- ✅ 5 frontend components built
+- ✅ Production deployment at https://digital-lab.ca/navidocs/app/
+
+---
+
+## Mission Overview
+
+**Spawn 6 Haiku agents to create and run E2E tests:**
+- T-01: Test infrastructure setup (Playwright, fixtures, test data)
+- T-02: Inventory flow test
+- T-03: Maintenance flow test
+- T-04: Camera flow test
+- T-05: Contacts flow test
+- T-06: Expenses flow test
+
+**Plus 3 agents for quality assurance:**
+- T-07: Lighthouse performance audit
+- T-08: API load testing
+- T-09: Security scan (OWASP Top 10)
+
+---
+
+## T-01: Test Infrastructure Setup (MUST RUN FIRST)
+
+**Agent Type:** Haiku
+**Model:** haiku
+**Priority:** P0 (all other tests depend on this)
+**Duration:** 30-45 min
+
+**Task:**
+```bash
+# 1. Install Playwright
+cd /workspace/navidocs
+npm install -D @playwright/test
+npx playwright install chromium
+
+# 2. Create test directories
+mkdir -p tests/fixtures
+mkdir -p tests/e2e
+mkdir -p test-results
+
+# 3. Create test fixtures (mock images)
+curl -o tests/fixtures/gps-photo.jpg "https://via.placeholder.com/800x600.jpg?text=Garmin+GPS+Chartplotter"
+curl -o tests/fixtures/fuel-receipt.jpg "https://via.placeholder.com/600x800.jpg?text=Fuel+Receipt+%E2%82%AC450.00"
+curl -o tests/fixtures/engine-manual.pdf "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
+
+# 4. Create Playwright config
+cat > playwright.config.js << 'EOF'
+import { defineConfig } from '@playwright/test';
+
+export default defineConfig({
+ testDir: './tests/e2e',
+ timeout: 30000,
+ retries: 2,
+ use: {
+ baseURL: 'http://localhost:5173',
+ screenshot: 'only-on-failure',
+ video: 'retain-on-failure',
+ trace: 'on-first-retry',
+ },
+ webServer: [
+ {
+ command: 'cd server && npm start',
+ port: 3000,
+ timeout: 60000,
+ },
+ {
+ command: 'cd client && npm run dev',
+ port: 5173,
+ timeout: 60000,
+ }
+ ],
+});
+EOF
+
+# 5. Create test helper utilities
+cat > tests/e2e/helpers.js << 'EOF'
+export async function loginAsTestUser(page) {
+ await page.goto('/');
+ await page.fill('input[type="email"]', 'test@example.com');
+ await page.fill('input[type="password"]', 'TestPassword123!');
+ await page.click('button[type="submit"]');
+ await page.waitForURL(/\/home/);
+}
+
+export async function loginAsCoOwner(page) {
+ await page.goto('/');
+ await page.fill('input[type="email"]', 'co-owner@example.com');
+ await page.fill('input[type="password"]', 'CoOwnerPass123!');
+ await page.click('button[type="submit"]');
+ await page.waitForURL(/\/home/);
+}
+
+export async function createTestBoat(page) {
+ // Assumes already logged in
+ await page.goto('/boats/new');
+ await page.fill('[name="name"]', 'Test Yacht');
+ await page.fill('[name="make"]', 'Jeanneau');
+ await page.fill('[name="model"]', 'Prestige 520');
+ await page.fill('[name="year"]', '2020');
+ await page.click('button[type="submit"]');
+ await page.waitForURL(/\/boats\/.+/);
+ // Extract boat ID from URL
+ const url = page.url();
+ return url.match(/\/boats\/([^\/]+)/)[1];
+}
+EOF
+
+# 6. Seed test database
+sqlite3 /workspace/navidocs/server/db/navidocs.db << 'EOF'
+-- Create test users (if not exist)
+INSERT OR IGNORE INTO users (id, email, name, password_hash, created_at, updated_at)
+VALUES
+ ('test-user-123', 'test@example.com', 'Test Owner', '$2b$10$N9qo8uLOickgx2ZMRZoMye', 1699999999, 1699999999),
+ ('test-coowner-456', 'co-owner@example.com', 'Co-Owner', '$2b$10$N9qo8uLOickgx2ZMRZoMye', 1699999999, 1699999999);
+
+-- Create test organization
+INSERT OR IGNORE INTO organizations (id, name, type, created_at, updated_at)
+VALUES ('test-org-123', 'Test Organization', 'personal', 1699999999, 1699999999);
+
+-- Link users to organization
+INSERT OR IGNORE INTO user_organizations (user_id, organization_id, role, joined_at)
+VALUES
+ ('test-user-123', 'test-org-123', 'admin', 1699999999),
+ ('test-coowner-456', 'test-org-123', 'member', 1699999999);
+
+-- Create test boat
+INSERT OR IGNORE INTO entities (id, organization_id, user_id, entity_type, name, make, model, year, created_at, updated_at)
+VALUES ('test-boat-123', 'test-org-123', 'test-user-123', 'boat', 'Test Yacht', 'Jeanneau', 'Prestige 520', 2020, 1699999999, 1699999999);
+EOF
+
+echo "✅ Test infrastructure ready"
+echo "Test users: test@example.com / co-owner@example.com (password: see users table)"
+echo "Test boat: test-boat-123"
+```
+
+**Signal completion:** Write `/tmp/T-01-SETUP-COMPLETE.json`:
+```json
+{
+ "status": "complete",
+ "playwright_installed": true,
+ "fixtures_created": 3,
+ "test_users_seeded": 2,
+ "test_boat_id": "test-boat-123",
+ "confidence": 0.95
+}
+```
+
+---
+
+## T-02: Inventory Flow Test
+
+**Agent Type:** Haiku
+**Dependencies:** Wait for T-01 complete
+**Duration:** 45-60 min
+
+**Task:** Create Playwright test for inventory upload workflow
+
+**Test file:** `tests/e2e/inventory-flow.spec.js`
+
+```javascript
+import { test, expect } from '@playwright/test';
+import { loginAsTestUser } from './helpers.js';
+
+test.describe('Inventory Management Flow', () => {
+ test('User uploads equipment photo and sees depreciation calculation', async ({ page }) => {
+ // 1. Login as test user
+ await loginAsTestUser(page);
+
+ // 2. Navigate to boat inventory
+ await page.goto('/boats/test-boat-123');
+ await page.click('a[href="/boats/test-boat-123/inventory"]');
+ await expect(page.locator('h1')).toContainText('Inventory');
+
+ // 3. Upload equipment with photo
+ await page.click('[data-testid="add-equipment"]');
+ await page.fill('[name="name"]', 'Garmin GPS Chartplotter');
+ await page.fill('[name="category"]', 'electronics');
+ await page.fill('[name="purchase_price"]', '2500');
+ await page.fill('[name="purchase_date"]', '2020-01-15');
+
+ // Upload photo
+ await page.setInputFiles('input[type="file"]', 'tests/fixtures/gps-photo.jpg');
+ await page.click('button[type="submit"]');
+
+ // 4. Verify item appears in inventory list
+ await page.waitForSelector('[data-testid="inventory-item"]', { timeout: 5000 });
+ await expect(page.locator('[data-testid="inventory-item"]')).toContainText('Garmin GPS');
+
+ // 5. Check depreciation calculation (4.8 years depreciation from 2020-01-15 to now)
+ await page.click('[data-testid="inventory-item"]:has-text("Garmin GPS")');
+ const currentValue = await page.locator('[data-testid="current-value"]').textContent();
+
+ // Depreciation calculation: $2500 - (4.8 years × $150/year) ≈ $1,780
+ // Accept range $1,700 - $2,100
+ expect(currentValue).toMatch(/\$1,[7-9]\d{2}|\$2,0\d{2}/);
+
+ // 6. Verify appears in ROI dashboard
+ await page.goto('/boats/test-boat-123/roi');
+ const totalInventory = await page.locator('[data-testid="total-inventory"]').textContent();
+ expect(totalInventory).toMatch(/\$1,[5-9]\d{2}|\$2,[0-2]\d{2}/);
+
+ // 7. Test persistence - refresh page
+ await page.reload();
+ await expect(page.locator('[data-testid="total-inventory"]')).toBeVisible();
+
+ // 8. Test multi-tenancy isolation
+ // Login as different org user should NOT see this inventory
+ await page.click('[data-testid="user-menu"]');
+ await page.click('button:has-text("Logout")');
+ // (Would need another test user from different org to verify)
+ });
+
+ test('User adds multiple items and sees total value', async ({ page }) => {
+ await loginAsTestUser(page);
+ await page.goto('/boats/test-boat-123/inventory');
+
+ // Add 3 items quickly
+ const items = [
+ { name: 'Anchor Windlass', category: 'deck', price: '1800' },
+ { name: 'Satellite Phone', category: 'electronics', price: '650' },
+ { name: 'Life Raft', category: 'safety', price: '2200' }
+ ];
+
+ for (const item of items) {
+ await page.click('[data-testid="add-equipment"]');
+ await page.fill('[name="name"]', item.name);
+ await page.fill('[name="category"]', item.category);
+ await page.fill('[name="purchase_price"]', item.price);
+ await page.fill('[name="purchase_date"]', '2024-11-01');
+ await page.click('button[type="submit"]');
+ await page.waitForTimeout(500); // Wait for item to save
+ }
+
+ // Verify count
+ const itemCount = await page.locator('[data-testid="inventory-item"]').count();
+ expect(itemCount).toBeGreaterThanOrEqual(3);
+
+ // Verify total value calculation
+ await page.goto('/boats/test-boat-123/roi');
+ const totalValue = await page.locator('[data-testid="total-inventory"]').textContent();
+ // $1800 + $650 + $2200 = $4,650 (plus previous items)
+ expect(totalValue).toMatch(/\$[4-9],\d{3}/);
+ });
+});
+```
+
+**Run test:**
+```bash
+npx playwright test tests/e2e/inventory-flow.spec.js --reporter=html
+```
+
+**Signal completion:** Write `/tmp/T-02-INVENTORY-TEST-COMPLETE.json`:
+```json
+{
+ "status": "complete",
+ "tests_passed": 2,
+ "tests_failed": 0,
+ "execution_time_ms": 18543,
+ "screenshots": 0,
+ "confidence": 0.95,
+ "report_path": "playwright-report/index.html"
+}
+```
+
+---
+
+## T-03: Maintenance Flow Test
+
+**Agent Type:** Haiku
+**Dependencies:** Wait for T-01 complete
+**Duration:** 45-60 min
+
+**Task:** Create Playwright test for maintenance logging and reminders
+
+**Test file:** `tests/e2e/maintenance-flow.spec.js`
+
+```javascript
+import { test, expect } from '@playwright/test';
+import { loginAsTestUser } from './helpers.js';
+
+test.describe('Maintenance Management Flow', () => {
+ test('User logs service and sets reminder', async ({ page }) => {
+ await loginAsTestUser(page);
+
+ // 1. Navigate to maintenance
+ await page.goto('/boats/test-boat-123/maintenance');
+ await expect(page.locator('h1')).toContainText('Maintenance');
+
+ // 2. Log new service record
+ await page.click('[data-testid="log-service"]');
+ await page.fill('[name="service_type"]', 'Oil Change');
+ await page.fill('[name="cost"]', '250');
+ await page.fill('[name="provider"]', 'Marine Services Inc');
+ await page.fill('[name="provider_phone"]', '+33494563412');
+ await page.fill('[name="service_date"]', '2025-11-14');
+ await page.fill('[name="notes"]', 'Changed oil and filter, engine running smoothly');
+ await page.click('button[type="submit"]');
+
+ // 3. Verify service appears in history
+ await page.waitForSelector('[data-testid="maintenance-record"]', { timeout: 5000 });
+ await expect(page.locator('[data-testid="maintenance-record"]')).toContainText('Oil Change');
+ await expect(page.locator('[data-testid="maintenance-record"]')).toContainText('€250');
+
+ // 4. Set reminder for next service (6 months)
+ await page.click('[data-testid="set-reminder"]');
+
+ // Calculate 6 months from now
+ const futureDate = new Date();
+ futureDate.setMonth(futureDate.getMonth() + 6);
+ const reminderDate = futureDate.toISOString().split('T')[0];
+
+ await page.fill('[name="reminder_date"]', reminderDate);
+ await page.fill('[name="reminder_note"]', 'Schedule next oil change - 6 months overdue');
+ await page.selectOption('[name="reminder_type"]', 'whatsapp');
+ await page.click('button:has-text("Save Reminder")');
+
+ // 5. Verify reminder appears in calendar
+ await page.click('[data-testid="calendar-view"]');
+ await page.waitForSelector('.calendar-event', { timeout: 5000 });
+
+ // Navigate to future month
+ for (let i = 0; i < 6; i++) {
+ await page.click('button[aria-label="Next month"]');
+ }
+
+ await expect(page.locator('.calendar-event:has-text("Oil Change")')).toBeVisible();
+
+ // 6. Verify notification queued
+ const notificationResponse = await page.request.get('/api/notifications', {
+ params: { type: 'reminder', boatId: 'test-boat-123' }
+ });
+ expect(notificationResponse.ok()).toBeTruthy();
+ const notifications = await notificationResponse.json();
+ expect(notifications.length).toBeGreaterThan(0);
+
+ // 7. Mark service as complete
+ await page.click('.calendar-event:has-text("Oil Change")');
+ await page.click('button:has-text("Mark Complete")');
+ await expect(page.locator('[data-testid="completed-badge"]')).toBeVisible();
+ });
+
+ test('User searches service history and filters by provider', async ({ page }) => {
+ await loginAsTestUser(page);
+ await page.goto('/boats/test-boat-123/maintenance');
+
+ // Search service records
+ await page.fill('[data-testid="search-maintenance"]', 'oil');
+ await page.waitForTimeout(500); // Debounce
+
+ const results = await page.locator('[data-testid="maintenance-record"]').count();
+ expect(results).toBeGreaterThan(0);
+
+ // Filter by provider
+ await page.selectOption('[name="filter_provider"]', 'Marine Services Inc');
+ await page.waitForTimeout(500);
+
+ // Verify all results show that provider
+ const providerNames = await page.locator('[data-testid="provider-name"]').allTextContents();
+ expect(providerNames.every(name => name === 'Marine Services Inc')).toBeTruthy();
+ });
+});
+```
+
+**Run test:**
+```bash
+npx playwright test tests/e2e/maintenance-flow.spec.js --reporter=html
+```
+
+**Signal completion:** Write `/tmp/T-03-MAINTENANCE-TEST-COMPLETE.json`
+
+---
+
+## T-04: Camera Flow Test
+
+**Agent Type:** Haiku
+**Dependencies:** Wait for T-01 complete
+**Duration:** 45-60 min
+
+**Task:** Create Playwright test for camera integration and live streaming
+
+**Test file:** `tests/e2e/camera-flow.spec.js`
+
+```javascript
+import { test, expect } from '@playwright/test';
+import { loginAsTestUser } from './helpers.js';
+
+test.describe('Camera Integration Flow', () => {
+ test('User connects camera and receives motion alerts', async ({ page }) => {
+ await loginAsTestUser(page);
+
+ // 1. Navigate to cameras
+ await page.goto('/boats/test-boat-123/cameras');
+ await expect(page.locator('h1')).toContainText('Camera');
+
+ // 2. Add new camera
+ await page.click('[data-testid="add-camera"]');
+ await page.fill('[name="camera_name"]', 'Stern Camera');
+ await page.fill('[name="location"]', 'stern');
+ await page.fill('[name="home_assistant_entity"]', 'camera.boat_stern');
+ await page.fill('[name="rtsp_url"]', 'rtsp://192.168.1.100:554/stream');
+ await page.click('button[type="submit"]');
+
+ // 3. Verify camera appears in list
+ await page.waitForSelector('[data-testid="camera-tile"]', { timeout: 5000 });
+ await expect(page.locator('[data-testid="camera-tile"]')).toContainText('Stern Camera');
+
+ // 4. Get webhook URL
+ await page.click('[data-testid="camera-tile"]:has-text("Stern Camera")');
+ const webhookUrl = await page.locator('[data-testid="webhook-url"]').textContent();
+ expect(webhookUrl).toContain('/api/cameras/webhook');
+
+ // 5. Simulate webhook from Home Assistant (motion detected)
+ const webhookResponse = await page.request.post('/api/cameras/webhook', {
+ data: {
+ event_type: 'motion',
+ camera_entity: 'camera.boat_stern',
+ timestamp: Date.now(),
+ snapshot_url: 'https://example.com/snapshot.jpg',
+ confidence: 0.92
+ },
+ headers: {
+ 'X-HA-Webhook-Secret': process.env.HOME_ASSISTANT_WEBHOOK_SECRET || 'test-secret'
+ }
+ });
+ expect(webhookResponse.ok()).toBeTruthy();
+
+ // 6. Verify motion alert appears
+ await page.reload();
+ await page.waitForSelector('[data-testid="motion-alert"]', { timeout: 5000 });
+ await expect(page.locator('[data-testid="motion-alert"]')).toContainText('Motion detected');
+
+ // 7. Test live stream viewer (mock stream)
+ await page.click('[data-testid="camera-tile"]:has-text("Stern Camera")');
+ await page.click('button:has-text("View Live")');
+
+ // Check video player loaded
+ const videoPlayer = page.locator('video');
+ await expect(videoPlayer).toBeVisible({ timeout: 10000 });
+
+ // Verify video source set (even if stream not available in test)
+ const videoSrc = await videoPlayer.getAttribute('src');
+ expect(videoSrc).toBeTruthy();
+
+ // 8. Daily check workflow
+ await page.click('[data-testid="daily-check-tab"]');
+ await page.click('button:has-text("Boat looks OK ✓")');
+ await expect(page.locator('[data-testid="last-checked"]')).toContainText('Today');
+ await expect(page.locator('[data-testid="check-streak"]')).toContainText('1 day');
+ });
+
+ test('User views camera event history', async ({ page }) => {
+ await loginAsTestUser(page);
+ await page.goto('/boats/test-boat-123/cameras');
+
+ // Navigate to events tab
+ await page.click('[data-testid="events-tab"]');
+
+ // Verify events listed
+ await page.waitForSelector('[data-testid="camera-event"]', { timeout: 5000 });
+ const eventCount = await page.locator('[data-testid="camera-event"]').count();
+ expect(eventCount).toBeGreaterThan(0);
+
+ // Filter by camera
+ await page.selectOption('[name="filter_camera"]', 'Stern Camera');
+ await page.waitForTimeout(500);
+
+ // Check snapshot image loads
+ const firstEventSnapshot = page.locator('[data-testid="event-snapshot"]').first();
+ await expect(firstEventSnapshot).toBeVisible();
+ });
+});
+```
+
+**Run test:**
+```bash
+npx playwright test tests/e2e/camera-flow.spec.js --reporter=html
+```
+
+**Signal completion:** Write `/tmp/T-04-CAMERA-TEST-COMPLETE.json`
+
+---
+
+## T-05: Contacts Flow Test
+
+**Agent Type:** Haiku
+**Dependencies:** Wait for T-01 complete
+**Duration:** 30-45 min
+
+**Task:** Create Playwright test for contact management
+
+**Test file:** `tests/e2e/contacts-flow.spec.js`
+
+```javascript
+import { test, expect } from '@playwright/test';
+import { loginAsTestUser } from './helpers.js';
+
+test.describe('Contact Management Flow', () => {
+ test('User adds contact and uses one-tap call', async ({ page }) => {
+ await loginAsTestUser(page);
+
+ // 1. Navigate to contacts
+ await page.goto('/boats/test-boat-123/contacts');
+ await expect(page.locator('h1')).toContainText('Contact');
+
+ // 2. Add marina contact
+ await page.click('[data-testid="add-contact"]');
+ await page.fill('[name="name"]', 'Port Pin Rolland Marina');
+ await page.selectOption('[name="category"]', 'marina');
+ await page.fill('[name="phone"]', '+33494563412');
+ await page.fill('[name="email"]', 'contact@portpinrolland.com');
+ await page.fill('[name="address"]', 'Saint-Tropez, France');
+ await page.fill('[name="notes"]', 'Main marina for berth. Ask for Jean-Pierre.');
+ await page.click('button[type="submit"]');
+
+ // 3. Verify contact appears
+ await page.waitForSelector('[data-testid="contact-card"]', { timeout: 5000 });
+ await expect(page.locator('[data-testid="contact-card"]')).toContainText('Port Pin Rolland');
+
+ // 4. Test one-tap call link
+ const callLink = page.locator('a[href^="tel:"]');
+ await expect(callLink).toHaveAttribute('href', 'tel:+33494563412');
+
+ // 5. Test one-tap email link
+ const emailLink = page.locator('a[href^="mailto:"]');
+ await expect(emailLink).toHaveAttribute('href', 'mailto:contact@portpinrolland.com');
+
+ // 6. Search contacts
+ await page.fill('[data-testid="contact-search"]', 'marina');
+ await page.waitForTimeout(300);
+ const searchResults = await page.locator('[data-testid="contact-card"]').count();
+ expect(searchResults).toBeGreaterThanOrEqual(1);
+
+ // 7. Export vCard
+ await page.click('[data-testid="contact-card"]:has-text("Port Pin Rolland")');
+ const [download] = await Promise.all([
+ page.waitForEvent('download'),
+ page.click('button:has-text("Export vCard")')
+ ]);
+ expect(download.suggestedFilename()).toMatch(/port-pin-rolland.*\.vcf/);
+
+ // 8. Log call
+ await page.click('button:has-text("Log Call")');
+ await page.fill('[name="call_notes"]', 'Confirmed berth reservation for summer 2026');
+ await page.fill('[name="call_duration"]', '8');
+ await page.click('button:has-text("Save")');
+
+ await expect(page.locator('[data-testid="call-history"]')).toContainText('Confirmed berth');
+ });
+
+ test('User filters contacts by category', async ({ page }) => {
+ await loginAsTestUser(page);
+ await page.goto('/boats/test-boat-123/contacts');
+
+ // Add contacts in different categories
+ const contacts = [
+ { name: 'Marine Mechanic Pro', category: 'mechanic', phone: '+33612345678' },
+ { name: 'Parts Supplier', category: 'vendor', phone: '+33687654321' }
+ ];
+
+ for (const contact of contacts) {
+ await page.click('[data-testid="add-contact"]');
+ await page.fill('[name="name"]', contact.name);
+ await page.selectOption('[name="category"]', contact.category);
+ await page.fill('[name="phone"]', contact.phone);
+ await page.click('button[type="submit"]');
+ await page.waitForTimeout(500);
+ }
+
+ // Filter by mechanic
+ await page.click('[data-testid="category-tab-mechanic"]');
+ await expect(page.locator('[data-testid="contact-card"]')).toContainText('Marine Mechanic');
+ await expect(page.locator('[data-testid="contact-card"]')).not.toContainText('Parts Supplier');
+
+ // Filter by vendor
+ await page.click('[data-testid="category-tab-vendor"]');
+ await expect(page.locator('[data-testid="contact-card"]')).toContainText('Parts Supplier');
+ await expect(page.locator('[data-testid="contact-card"]')).not.toContainText('Marine Mechanic');
+ });
+});
+```
+
+**Run test:**
+```bash
+npx playwright test tests/e2e/contacts-flow.spec.js --reporter=html
+```
+
+**Signal completion:** Write `/tmp/T-05-CONTACTS-TEST-COMPLETE.json`
+
+---
+
+## T-06: Expenses Flow Test
+
+**Agent Type:** Haiku
+**Dependencies:** Wait for T-01 complete
+**Duration:** 60-75 min (most complex test)
+
+**Task:** Create Playwright test for expense tracking and approval
+
+**Test file:** `tests/e2e/expenses-flow.spec.js`
+
+```javascript
+import { test, expect } from '@playwright/test';
+import { loginAsTestUser, loginAsCoOwner } from './helpers.js';
+
+test.describe('Expense Tracking Flow', () => {
+ test('User uploads receipt, OCR extracts data, co-owner approves', async ({ page, context }) => {
+ // 1. Login as primary owner
+ await loginAsTestUser(page);
+
+ // 2. Navigate to expenses
+ await page.goto('/boats/test-boat-123/expenses');
+ await expect(page.locator('h1')).toContainText('Expense');
+
+ // 3. Upload receipt
+ await page.click('[data-testid="upload-receipt"]');
+ await page.setInputFiles('input[type="file"]', 'tests/fixtures/fuel-receipt.jpg');
+
+ // Wait for OCR processing (max 10 seconds)
+ await page.waitForSelector('[data-testid="ocr-result"]', { timeout: 10000 });
+
+ // 4. Verify OCR extracted amount
+ const extractedAmount = await page.locator('[data-testid="ocr-amount"]').inputValue();
+ expect(extractedAmount).toBe('450.00'); // From fixture
+
+ // 5. Complete expense details
+ await page.selectOption('[name="category"]', 'fuel');
+ await page.fill('[name="vendor"]', 'Total Energies');
+ await page.fill('[name="date"]', '2025-11-10');
+ await page.fill('[name="notes"]', 'Full tank refuel before winter storage');
+ await page.click('button[type="submit"]');
+
+ // 6. Verify expense appears in list
+ await page.waitForSelector('[data-testid="expense-item"]', { timeout: 5000 });
+ await expect(page.locator('[data-testid="expense-item"]')).toContainText('€450.00');
+ await expect(page.locator('[data-testid="approval-badge"]')).toContainText('Pending');
+
+ // 7. Logout primary owner
+ await page.click('[data-testid="user-menu"]');
+ await page.click('button:has-text("Logout")');
+
+ // 8. Login as co-owner
+ await loginAsCoOwner(page);
+
+ // 9. Navigate to expenses (co-owner perspective)
+ await page.goto('/boats/test-boat-123/expenses');
+
+ // 10. Find pending expense
+ await page.click('[data-testid="filter-status-pending"]');
+ await expect(page.locator('[data-testid="expense-item"]')).toContainText('€450.00');
+
+ // 11. Co-owner approves expense
+ await page.click('[data-testid="expense-item"]:has-text("€450.00")');
+ await page.click('button:has-text("Approve")');
+
+ // Verify approval badge updated
+ await expect(page.locator('[data-testid="approval-badge"]')).toContainText('Approved');
+
+ // 12. Verify annual spend updated
+ const annualTotal = await page.locator('[data-testid="annual-total"]').textContent();
+ expect(annualTotal).toContain('€450');
+
+ // 13. Check category breakdown chart
+ await page.click('[data-testid="category-breakdown-tab"]');
+ const fuelCategory = page.locator('[data-testid="category-fuel"]');
+ await expect(fuelCategory).toContainText('€450.00');
+
+ // 14. Test CSV export
+ const [download] = await Promise.all([
+ page.waitForEvent('download'),
+ page.click('button:has-text("Export CSV")')
+ ]);
+ expect(download.suggestedFilename()).toMatch(/expenses-\d{4}-\d{2}-\d{2}\.csv/);
+ });
+
+ test('User splits expense with co-owner', async ({ page }) => {
+ await loginAsTestUser(page);
+ await page.goto('/boats/test-boat-123/expenses');
+
+ // Add expense
+ await page.click('[data-testid="upload-receipt"]');
+ await page.setInputFiles('input[type="file"]', 'tests/fixtures/fuel-receipt.jpg');
+ await page.waitForSelector('[data-testid="ocr-result"]', { timeout: 10000 });
+
+ await page.selectOption('[name="category"]', 'maintenance');
+ await page.fill('[name="vendor"]', 'Marine Services');
+ await page.fill('[name="date"]', '2025-11-12');
+
+ // Enable expense splitting
+ await page.click('[name="split_expense"]');
+
+ // Select co-owner for 50/50 split
+ await page.click('[data-testid="add-split-participant"]');
+ await page.selectOption('[name="split_user"]', 'co-owner@example.com');
+ await page.fill('[name="split_percentage"]', '50');
+
+ await page.click('button[type="submit"]');
+
+ // Verify split shows correctly
+ await page.click('[data-testid="expense-item"]:has-text("Marine Services")');
+ await expect(page.locator('[data-testid="split-details"]')).toContainText('50% (€225.00)');
+ await expect(page.locator('[data-testid="split-details"]')).toContainText('co-owner@example.com: 50%');
+ });
+});
+```
+
+**Run test:**
+```bash
+npx playwright test tests/e2e/expenses-flow.spec.js --reporter=html
+```
+
+**Signal completion:** Write `/tmp/T-06-EXPENSE-TEST-COMPLETE.json`
+
+---
+
+## T-07: Performance Testing
+
+**Agent Type:** Haiku
+**Dependencies:** Wait for T-02 through T-06 complete
+**Duration:** 30-45 min
+
+**Task:** Run Lighthouse audits and API load tests
+
+```bash
+# 1. Install Lighthouse
+npm install -g lighthouse
+
+# 2. Run Lighthouse on all key pages
+pages=(
+ "http://localhost:5173/"
+ "http://localhost:5173/boats/test-boat-123/inventory"
+ "http://localhost:5173/boats/test-boat-123/maintenance"
+ "http://localhost:5173/boats/test-boat-123/cameras"
+ "http://localhost:5173/boats/test-boat-123/contacts"
+ "http://localhost:5173/boats/test-boat-123/expenses"
+)
+
+for url in "${pages[@]}"; do
+ page_name=$(echo $url | sed 's/.*\///' | sed 's/http.*5173/home/')
+ lighthouse "$url" \
+ --output=json \
+ --output=html \
+ --output-path="test-results/lighthouse-$page_name" \
+ --chrome-flags="--headless --no-sandbox"
+done
+
+# 3. Extract scores
+for file in test-results/lighthouse-*.json; do
+ page=$(basename $file .json | sed 's/lighthouse-//')
+ performance=$(jq '.categories.performance.score * 100' $file)
+ accessibility=$(jq '.categories.accessibility.score * 100' $file)
+ echo "$page: Performance=$performance, Accessibility=$accessibility"
+done
+
+# 4. API load testing (using Apache Bench)
+echo "Testing API endpoints..."
+
+# Get JWT token first
+JWT_TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
+ -H "Content-Type: application/json" \
+ -d '{"email":"test@example.com","password":"TestPassword123!"}' | jq -r '.token')
+
+# Test each endpoint (1000 requests, 10 concurrent)
+endpoints=(
+ "/api/inventory/test-boat-123"
+ "/api/maintenance/test-boat-123"
+ "/api/cameras/test-boat-123"
+ "/api/contacts/test-boat-123"
+ "/api/expenses/test-boat-123"
+)
+
+for endpoint in "${endpoints[@]}"; do
+ echo "Testing $endpoint..."
+ ab -n 1000 -c 10 \
+ -H "Authorization: Bearer $JWT_TOKEN" \
+ "http://localhost:3000$endpoint" \
+ > "test-results/load-test-$(echo $endpoint | tr '/' '-').txt"
+done
+
+# Extract p95 latency
+for file in test-results/load-test-*.txt; do
+ endpoint=$(basename $file .txt | sed 's/load-test-//')
+ p95=$(grep "95%" $file | awk '{print $2}')
+ echo "$endpoint: p95 latency = $p95 ms"
+done
+
+# 5. Bundle size check
+cd /workspace/navidocs/client
+npm run build
+echo "Bundle sizes:"
+du -sh dist/assets/*.js
+gzip -c dist/assets/index.*.js | wc -c | awk '{print "Gzipped: " $1/1024 " KB"}'
+```
+
+**Target Metrics:**
+- Lighthouse Performance: >90
+- Lighthouse Accessibility: >95
+- API p95 latency: <200ms
+- Bundle size (gzipped): <500KB
+
+**Signal completion:** Write `/tmp/T-07-PERFORMANCE-COMPLETE.json`:
+```json
+{
+ "status": "complete",
+ "lighthouse_scores": {
+ "home": { "performance": 94, "accessibility": 97 },
+ "inventory": { "performance": 92, "accessibility": 95 },
+ "maintenance": { "performance": 91, "accessibility": 96 },
+ "cameras": { "performance": 88, "accessibility": 94 },
+ "contacts": { "performance": 93, "accessibility": 97 },
+ "expenses": { "performance": 90, "accessibility": 95 }
+ },
+ "api_latency_p95": {
+ "inventory": 145,
+ "maintenance": 167,
+ "cameras": 189,
+ "contacts": 123,
+ "expenses": 198
+ },
+ "bundle_size_kb": 387,
+ "confidence": 0.95
+}
+```
+
+---
+
+## T-08: Security Testing
+
+**Agent Type:** Haiku
+**Dependencies:** Wait for T-02 through T-06 complete
+**Duration:** 30-45 min
+
+**Task:** Run OWASP Top 10 security scans
+
+```bash
+# 1. SQL Injection Tests
+echo "Testing SQL injection vulnerabilities..."
+
+JWT_TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
+ -H "Content-Type: application/json" \
+ -d '{"email":"test@example.com","password":"TestPassword123!"}' | jq -r '.token')
+
+# Test SQL injection in query params
+curl -X GET "http://localhost:3000/api/inventory/test-boat-123' OR '1'='1" \
+ -H "Authorization: Bearer $JWT_TOKEN" \
+ -w "\nStatus: %{http_code}\n" \
+ -o /tmp/sql-injection-test.json
+
+# Should return 400 or 404, not 200 with data leak
+if grep -q "id" /tmp/sql-injection-test.json; then
+ echo "❌ VULNERABILITY: SQL injection possible"
+else
+ echo "✅ SQL injection blocked"
+fi
+
+# 2. XSS Tests
+echo "Testing XSS vulnerabilities..."
+
+# Try to inject script tag in contact name
+curl -X POST http://localhost:3000/api/contacts \
+ -H "Authorization: Bearer $JWT_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "boatId": "test-boat-123",
+ "name": "",
+ "phone": "123456789",
+ "category": "vendor"
+ }' \
+ -w "\nStatus: %{http_code}\n" \
+ -o /tmp/xss-test.json
+
+# Should sanitize the script tag
+if grep -q "