# NaviDocs Critical Fixes - Cloud Implementation Mission **GitHub Repository:** https://github.com/dannystocker/navidocs **Branch:** `navidocs-cloud-coordination` **Target:** Premium boat management platform for €800K-€1.5M yachts --- ## Mission Overview You are a Sonnet instance managing 15 Haiku agents to implement critical security, performance, and UX fixes identified in comprehensive code reviews (Codex + Gemini). This is a **3-week sprint** to production readiness. **Review Sources (on GitHub in branch `navidocs-cloud-coordination`):** - `HANDOVER_SESSION_2025-11-14.md` - Full context - `reviews/CODEX_SECURITY_ARCHITECTURE_REPORT.md` - Security review (if exists) - `reviews/GEMINI_PERFORMANCE_UX_REPORT.md` - Performance/UX review (if exists) **Reference Implementation Guide:** The detailed implementation patterns are in the Windows downloads folder, but I'll embed the key patterns here for zero-dependency execution. --- ## Critical Issues to Fix ### 🔴 BLOCKERS (Week 1 - Must Fix Before Production) 1. **JWT Secret Enforcement** - Hard-coded default secret allows token forgery 2. **Auth Gaps** - Document/search/upload/image/stats routes use `req.user?.id || 'test-user-id'` 3. **Meilisearch Token Fallback** - Falls back to global search key on failure 4. **Bundle Size 2.3MB** - No lazy loading, target <500KB gzipped 5. **Touch Targets 12-40px** - Need 60×60px minimum for marine gloves 6. **No Pagination** - 11 `.all()` queries without limits 7. **Small Fonts** - As low as 10px, need 24-48px for sunlight readability 8. **Missing ARIA Labels** - 29 icon-only buttons fail WCAG ### 🟡 HIGH PRIORITY (Week 2) 9. **God Components** - `DocumentView.vue` (1386 lines), `SearchView.vue` (628 lines) 10. **Service Layer** - Business logic in routes, need service extraction 11. **Token Refresh** - No automatic 401 handling 12. **Images Without Alt** - Accessibility failures 13. **Legacy Auth Middleware** - Two auth implementations cause confusion --- ## Agent Assignment & Parallel Execution **Spawn 15 Haiku agents in PARALLEL** using a single message with 15 Task tool calls: ### Security Team (Agents 1-5) **Agent 1: JWT Secret Enforcement** - Create `server/config/env.js` with mandatory `JWT_SECRET` validation - Update `server/services/auth.service.js` to use central config - Update `server/middleware/auth.middleware.js` to use central config - Remove or deprecate `server/middleware/auth.js` (legacy) - Add startup assertion: `assert(JWT_SECRET && JWT_SECRET.length >= 32)` **Agent 2: Document Route Auth** - Add `authenticateToken` middleware to all routes in `server/routes/documents.js` - Remove all `req.user?.id || 'test-user-id'` patterns - Use `req.user.userId` directly (set by middleware) - Add `requireOrganizationMember` where organization scoping needed - Test: Verify 401 on unauthenticated requests **Agent 3: Search/Upload/Image Route Auth** - Secure `server/routes/search.js` (both `/token` and `/search`) - Secure `server/routes/upload.js` and `/upload/quick-ocr` - Secure `server/routes/images.js` (all image endpoints) - Remove `test-user-id` fallbacks - Add organization membership checks **Agent 4: Stats & Jobs Auth** - Secure `server/routes/stats.js` with `requireSystemAdmin` for global stats - Create `/stats/organizations/:organizationId` for scoped stats - Secure `server/routes/jobs.js` with proper auth - Remove public access to operational metrics **Agent 5: Meilisearch Token Hardening** - Modify `server/routes/search.js` POST `/token` to fail closed - Only allow fallback in `NODE_ENV === 'development'` - Log tenant token failures to ops team - Return 500 with retry message in production on failure --- ### Performance Team (Agents 6-9) **Agent 6: Lazy Loading Routes** - Update `client/src/router/index.js` - Convert all direct imports to `() => import('./views/XYZ.vue')` - Apply to: HomeView, DocumentView, LibraryView, SearchView, SettingsView - Test: Verify bundle splits in `npm run build` - Target: Reduce initial bundle from 2.3MB to <1MB **Agent 7: Pagination Utility** - Create `server/utils/pagination.js` with `parsePagination(req, options)` - Support `?page=1&pageSize=50` query params - Max page size: 200, default: 50 - Return `{ page, pageSize, offset }` for SQL queries - Add total count helper for responses **Agent 8: Apply Pagination to Routes** - Update `server/routes/documents.js` GET `/` with pagination - Update `server/routes/images.js` listings - Update `server/routes/jobs.js` GET `/api/jobs` - Replace all `.all()` with `.all(limit, offset)` pattern - Return `{ data: rows, page, pageSize, total }` format **Agent 9: Central API Client with Token Refresh** - Create `client/src/api/http.js` using Axios - Add request interceptor to inject `Authorization: Bearer ${accessToken}` - Add response interceptor for 401 handling - Implement automatic refresh using `/api/auth/refresh` - Prevent multiple concurrent refresh requests (use promise singleton) --- ### Architecture Team (Agents 10-12) **Agent 10: Document Service Layer** - Create `server/services/document.service.js` - Extract methods: `listForUser()`, `getForUser()`, `deleteForUser()` - Use pagination support from Agent 7 - Enforce organization membership via JOIN - Throw 404 errors with `.status = 404` for service-level handling **Agent 11: Image Service Layer** - Create `server/services/image.service.js` - Extract image retrieval, validation, path safety logic - Move Meilisearch cleanup logic from routes - Support pagination for image lists **Agent 12: Stats Service Layer** - Create `server/services/stats.service.js` - Implement `getGlobalStats()` (admin only) - Implement `getOrgStats(organizationId, userId)` with membership check - Always join via `user_organizations` to prevent cross-tenant leaks --- ### UX Team (Agents 13-15) **Agent 13: Marine CSS Baseline** - Create `client/src/assets/marine.css` - Define CSS variables: `--nd-touch-target: 60px`, `--nd-font-base: 16px`, `--nd-font-large: 24px`, `--nd-font-xlarge: 32px` - Apply to all `button`, `[role='button']`, `a.nav-link` (min 60×60px) - Update `client/src/main.js` to import marine.css - Test: Verify all interactive elements meet 60px minimum **Agent 14: ARIA Labels & Alt Text** - Scan all `.vue` files for icon-only buttons - Add `aria-label` to all interactive elements without visible text - Add `aria-hidden="true"` to decorative icons inside labeled buttons - Add `:alt` attributes to all `` tags - Use descriptive labels: "Delete item", "Close dialog", etc. **Agent 15: Typography & Contrast** - Update all font-size CSS < 16px to at least 16px base - Create `.nd-metric` class for critical numbers (32-48px, font-weight: 700) - Apply to key metrics in dashboard/stats components - Add `.nd-heading` class (24px, font-weight: 600) - Verify contrast ratios ≥7:1 for WCAG AAA (Navy #1E3A8A on White #FFF) --- ## Implementation Patterns (Zero-Dependency Reference) ### Pattern 1: JWT Secret Enforcement ```javascript // server/config/env.js import assert from 'node:assert'; export const NODE_ENV = process.env.NODE_ENV || 'development'; export const JWT_SECRET = process.env.JWT_SECRET; assert( JWT_SECRET && JWT_SECRET.length >= 32, 'JWT_SECRET environment variable is required and must be at least 32 chars' ); export const MEILISEARCH_HOST = process.env.MEILISEARCH_HOST; export const MEILISEARCH_MASTER_KEY = process.env.MEILISEARCH_MASTER_KEY; ``` ```javascript // server/services/auth.service.js import jwt from 'jsonwebtoken'; import { JWT_SECRET } from '../config/env.js'; export function signAccessToken(payload, options = {}) { return jwt.sign(payload, JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES_IN || '15m', ...options, }); } ``` ### Pattern 2: Auth Middleware with RBAC ```javascript // server/middleware/auth.middleware.js import jwt from 'jsonwebtoken'; import { JWT_SECRET } from '../config/env.js'; export function authenticateToken(req, res, next) { const header = req.headers['authorization']; const token = header && header.split(' ')[1]; if (!token) return res.sendStatus(401); jwt.verify(token, JWT_SECRET, (err, decoded) => { if (err) return res.sendStatus(401); req.user = decoded; // { userId, email, organizationIds, ... } next(); }); } export function requireOrganizationMember(req, res, next) { const orgId = req.params.organizationId || req.body.organizationId; if (!orgId) return res.status(400).json({ error: 'Organization ID required' }); const isMember = req.user.organizationIds?.includes(orgId); if (!isMember) return res.status(403).json({ error: 'Not a member of this organization' }); next(); } ``` ### Pattern 3: Pagination Utility ```javascript // server/utils/pagination.js export function parsePagination(req, { defaultSize = 50, maxSize = 200 } = {}) { const page = Math.max(parseInt(req.query.page || '1', 10), 1); const pageSize = Math.min( Math.max(parseInt(req.query.pageSize || String(defaultSize), 10), 1), maxSize ); const offset = (page - 1) * pageSize; return { page, pageSize, offset }; } ``` ### Pattern 4: Service Layer for Documents ```javascript // server/services/document.service.js import db from '../db/db.js'; export function listForUser(userId, { limit, offset }) { const rows = db.prepare(` SELECT d.* FROM documents d JOIN user_organizations uo ON uo.organization_id = d.organization_id WHERE uo.user_id = ? ORDER BY d.created_at DESC LIMIT ? OFFSET ? `).all(userId, limit, offset); const { count } = db.prepare(` SELECT COUNT(*) as count FROM documents d JOIN user_organizations uo ON uo.organization_id = d.organization_id WHERE uo.user_id = ? `).get(userId); return { rows, total: count }; } export function getForUser(userId, documentId) { const doc = db.prepare(` SELECT d.* FROM documents d JOIN user_organizations uo ON uo.organization_id = d.organization_id WHERE d.id = ? AND uo.user_id = ? `).get(documentId, userId); if (!doc) { const err = new Error('Document not found'); err.status = 404; throw err; } return doc; } ``` ### Pattern 5: Route Using Service + Pagination ```javascript // server/routes/documents.js import { Router } from 'express'; import { authenticateToken } from '../middleware/auth.middleware.js'; import { parsePagination } from '../utils/pagination.js'; import * as documentService from '../services/document.service.js'; const router = Router(); router.get('/', authenticateToken, async (req, res, next) => { try { const { page, pageSize, offset } = parsePagination(req); const { rows, total } = documentService.listForUser( req.user.userId, { limit: pageSize, offset } ); res.json({ data: rows, page, pageSize, total }); } catch (err) { next(err); } } ); router.get('/:id', authenticateToken, async (req, res, next) => { try { const doc = documentService.getForUser(req.user.userId, req.params.id); res.json(doc); } catch (err) { next(err); } } ); export default router; ``` ### Pattern 6: Lazy Loading Routes ```javascript // client/src/router/index.js import { createRouter, createWebHistory } from 'vue-router'; const routes = [ { path: '/', name: 'home', component: () => import('../views/HomeView.vue') }, { path: '/documents/:id', name: 'document', component: () => import('../views/DocumentView.vue'), props: true }, { path: '/library', name: 'library', component: () => import('../views/LibraryView.vue') }, { path: '/search', name: 'search', component: () => import('../views/SearchView.vue') } ]; export default createRouter({ history: createWebHistory(), routes, }); ``` ### Pattern 7: Central API Client with Token Refresh ```javascript // client/src/api/http.js import axios from 'axios'; import { useAuthStore } from '@/stores/auth'; const api = axios.create({ baseURL: '/api', }); api.interceptors.request.use((config) => { const auth = useAuthStore(); if (auth.accessToken) { config.headers.Authorization = `Bearer ${auth.accessToken}`; } return config; }); let refreshPromise = null; api.interceptors.response.use( (response) => response, async (error) => { const { response, config } = error; const auth = useAuthStore(); if (response?.status === 401 && !config._retry && auth.refreshToken) { config._retry = true; if (!refreshPromise) { refreshPromise = auth.refreshAccessToken(); } try { await refreshPromise; refreshPromise = null; config.headers.Authorization = `Bearer ${auth.accessToken}`; return api(config); } catch (err) { refreshPromise = null; auth.logout(); window.location.href = '/login'; } } return Promise.reject(error); } ); export default api; ``` ### Pattern 8: Marine CSS Baseline ```css /* client/src/assets/marine.css */ :root { --nd-touch-target: 60px; --nd-font-base: 16px; --nd-font-large: 24px; --nd-font-xlarge: 32px; --nd-font-metric: 48px; --nd-navy: #1E3A8A; --nd-teal: #0D9488; --nd-white: #ffffff; } body { font-size: var(--nd-font-base); line-height: 1.5; } /* All interactive elements */ button, [role='button'], a.nav-link, .btn, .icon-button { min-width: var(--nd-touch-target); min-height: var(--nd-touch-target); padding: 0.75rem 1rem; } /* Typography scale */ .nd-heading { font-size: var(--nd-font-large); font-weight: 600; } .nd-metric { font-size: var(--nd-font-metric); font-weight: 700; color: var(--nd-navy); } /* High contrast for marine use */ .nd-high-contrast { background: var(--nd-navy); color: var(--nd-white); } ``` ### Pattern 9: ARIA Labels ```vue ``` ```vue ``` --- ## Execution Instructions ### Step 1: Clone Repository ```bash git clone https://github.com/dannystocker/navidocs.git cd navidocs git checkout navidocs-cloud-coordination ``` ### Step 2: Analyze Current State ```bash # Find all .all() queries without limits grep -r "\.all()" server/routes/ | wc -l # Find test-user-id patterns grep -r "test-user-id" server/routes/ # Find small touch targets grep -r "width.*px\|height.*px" client/src/components/ | grep -E "width: [1-5][0-9]px|height: [1-5][0-9]px" # Find small fonts grep -r "font-size.*px" client/src/ | grep -E "[0-9]{1}px|1[0-5]px" # Find missing ARIA labels grep -r "