# 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 "