diff --git a/CLOUD_IMPLEMENTATION_PROMPT.md b/CLOUD_IMPLEMENTATION_PROMPT.md new file mode 100644 index 0000000..f2afdec --- /dev/null +++ b/CLOUD_IMPLEMENTATION_PROMPT.md @@ -0,0 +1,700 @@ +# 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 "