524 lines
13 KiB
Markdown
524 lines
13 KiB
Markdown
# Job Info Data Handling Rules & Patterns
|
|
|
|
**Version:** 1.0
|
|
**Status:** PRODUCTION
|
|
**Last Updated:** January 2026
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
This document enforces the data handling patterns, caching strategies, and view structure used in Job Info. All developers must follow these rules to maintain consistency, performance, and reliability.
|
|
|
|
---
|
|
|
|
## 1. Authentication & Token Rules (DUAL-TOKEN SYSTEM)
|
|
|
|
### Dual Token Architecture
|
|
|
|
The app uses **PocketBase OAuth through Microsoft** to obtain TWO tokens:
|
|
|
|
1. **PocketBase Token** - For app operations (jobs, files, notes)
|
|
- Obtained via PocketBase OAuth
|
|
- Stored in `localStorage` under `pocketbase_auth`
|
|
- Expires: 3600 seconds (1 hour)
|
|
|
|
2. **Microsoft Graph Token** - For SharePoint/Office 365 integration
|
|
- Obtained during PocketBase OAuth (Microsoft scopes)
|
|
- Stored in `localStorage` under `graph_token`
|
|
- Expires: 3600 seconds (1 hour)
|
|
|
|
### Critical Requirement: NO User Re-authentication After Login
|
|
|
|
**RULE:** Once user logs in, they should NEVER be asked to authenticate again (except system restart)
|
|
|
|
**HOW:**
|
|
- Both tokens obtained once during initial OAuth flow
|
|
- Tokens stored in `localStorage` (frontend) and Valkey cache (backend)
|
|
- Background refresh loop keeps tokens fresh (every 30 minutes)
|
|
- If frontend token expires, backend provides fresh one
|
|
- If both fail, use stale token as fallback
|
|
- Only redirect to login if completely unavailable (system restart)
|
|
|
|
### Token Storage
|
|
|
|
**Frontend (localStorage):**
|
|
```javascript
|
|
pocketbase_auth // PocketBase token + metadata
|
|
graph_token // Microsoft Graph token
|
|
token_expires_pb // PocketBase expiry timestamp
|
|
token_expires_graph // Graph expiry timestamp
|
|
```
|
|
|
|
**Backend (Valkey/Redis):**
|
|
```
|
|
tokens:${userId}:pb // Fresh PocketBase token (TTL: 1 hour)
|
|
tokens:${userId}:graph // Fresh Graph token (TTL: 1 hour)
|
|
tokens:${userId}:refresh_pb // PocketBase refresh token (TTL: 30 days)
|
|
tokens:${userId}:refresh_graph // Graph refresh token (TTL: 30 days)
|
|
```
|
|
|
|
### Single Token Check Pattern
|
|
|
|
Before ANY API call:
|
|
|
|
```javascript
|
|
// ✅ CORRECT: Check & refresh if needed
|
|
async function ensureToken(type) {
|
|
// type: 'pb' or 'graph'
|
|
|
|
// 1. Get from localStorage
|
|
const token = localStorage.getItem(`${type}_token`);
|
|
const expiry = localStorage.getItem(`token_expires_${type}`);
|
|
|
|
// 2. If fresh (> 15 min remaining), use immediately
|
|
if (token && expiry > Date.now() + 15*60*1000) {
|
|
return token;
|
|
}
|
|
|
|
// 3. If expiring soon, refresh from backend
|
|
const response = await fetch(`/api/refresh-token?type=${type}`);
|
|
if (response.ok) {
|
|
const { token: newToken, expiresAt } = await response.json();
|
|
localStorage.setItem(`${type}_token`, newToken);
|
|
localStorage.setItem(`token_expires_${type}`, expiresAt);
|
|
return newToken;
|
|
}
|
|
|
|
// 4. Fallback: Use stale token if available (field workers!)
|
|
if (token) {
|
|
console.warn(`Using stale ${type} token - backend unreachable`);
|
|
return token;
|
|
}
|
|
|
|
// 5. Only last resort: Redirect to login
|
|
redirectToLogin();
|
|
throw new Error('Authentication required');
|
|
}
|
|
|
|
// ❌ WRONG: Refreshing on every request
|
|
const token = await pb.authRefresh(); // DON'T DO THIS
|
|
fetch('/api/endpoint', { headers: { 'Authorization': `Bearer ${token}` } });
|
|
```
|
|
|
|
### Background Token Refresh (Backend)
|
|
|
|
```typescript
|
|
// Runs every 30 minutes for each logged-in user
|
|
// Transparently refreshes tokens before they expire
|
|
// Users never see a re-auth prompt
|
|
|
|
async function refreshTokensInBackground(userId: string) {
|
|
// Check PocketBase token
|
|
const pbToken = await cache.get(`tokens:${userId}:pb`);
|
|
if (isExpiring(pbToken)) {
|
|
const refreshed = await refreshPocketBaseToken(pbToken.refresh_token);
|
|
await cache.set(`tokens:${userId}:pb`, refreshed, ttl: 3600);
|
|
}
|
|
|
|
// Check Graph token
|
|
const graphToken = await cache.get(`tokens:${userId}:graph`);
|
|
if (isExpiring(graphToken)) {
|
|
const refreshed = await refreshMicrosoftGraphToken(graphToken.refresh_token);
|
|
await cache.set(`tokens:${userId}:graph`, refreshed, ttl: 3600);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Sign Out
|
|
- Clear `localStorage` items: `pocketbase_auth`, `graph_token`, expiry times
|
|
- Clear `sessionStorage`
|
|
- Call backend `/api/auth/logout` to stop refresh loops
|
|
- Revoke tokens with PocketBase & Microsoft (if available)
|
|
- Redirect to signin page
|
|
|
|
---
|
|
|
|
## 2. Caching Strategy
|
|
|
|
### Backend Cache (Redis/Valkey)
|
|
|
|
**Rule:** Cache GET requests with deterministic keys, NOT mutations.
|
|
|
|
#### Jobs List
|
|
```typescript
|
|
// Cache key format: jobs:page:${page}:perPage:${perPage}:sort:${sort}
|
|
const cacheKey = `jobs:page:${page}:perPage:${perPage}:sort:${sort}`;
|
|
const ttl = 300; // 5 minutes
|
|
|
|
// Pattern:
|
|
const cached = await getCache(cacheKey);
|
|
if (cached) return c.json(cached);
|
|
|
|
const data = await fetchFromPocketBase(...);
|
|
await setCache(cacheKey, data, ttl);
|
|
return c.json(data);
|
|
```
|
|
|
|
#### Cache TTL Guidelines
|
|
| Data Type | TTL | Reason |
|
|
|-----------|-----|--------|
|
|
| Jobs List | 300s (5m) | Frequently searched, moderate change rate |
|
|
| Job Details | 600s (10m) | Less frequently accessed |
|
|
| File Lists | 900s (15m) | Rarely changes, expensive to fetch |
|
|
| Metadata | 1800s (30m) | Static reference data |
|
|
|
|
#### Cache Invalidation
|
|
- Manual: `clearCachePattern('jobs:*')` after mutations
|
|
- Automatic: TTL expiration
|
|
- **Never cache:** POST, PUT, DELETE requests
|
|
|
|
### Frontend Cache (In-Memory)
|
|
|
|
**File Cache Pattern** (Current Implementation)
|
|
```javascript
|
|
const fileCache = new Map();
|
|
|
|
// Check before fetching
|
|
if (fileCache.has(folderLink)) {
|
|
const cached = fileCache.get(folderLink);
|
|
fileListState.items = cached.items;
|
|
renderFileGroups(folderLink);
|
|
return;
|
|
}
|
|
|
|
// Fetch and cache
|
|
const data = await fetch(`/api/job-files?link=${link}`);
|
|
fileCache.set(folderLink, { items: data.items, driveId: data.driveId });
|
|
renderFileGroups(folderLink);
|
|
|
|
// Clear on sign out
|
|
fileCache.clear();
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Data Filtering & Transformation
|
|
|
|
### File Filtering Rules
|
|
|
|
**Supported File Types:**
|
|
- PDFs (`.pdf`)
|
|
- Word Documents (`.doc`, `.docx`)
|
|
- Images (`.jpg`, `.jpeg`, `.png`, `.gif`, `.bmp`, `.tiff`, `.webp`)
|
|
|
|
**Folders:** Excluded (never shown)
|
|
|
|
**Implementation:**
|
|
```javascript
|
|
function renderFileGroups(folderLink) {
|
|
const term = (fileListState.filter || '').toLowerCase().trim();
|
|
|
|
const filtered = fileListState.items.filter((f) => {
|
|
// Exclude folders
|
|
if (f.isFolder) return false;
|
|
|
|
// Check file type
|
|
const name = f.name.toLowerCase();
|
|
const contentType = (f.contentType || '').toLowerCase();
|
|
const isPdfOrWord = name.endsWith('.pdf') || contentType.includes('pdf') ||
|
|
name.endsWith('.doc') || name.endsWith('.docx') ||
|
|
contentType.includes('word');
|
|
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']
|
|
.some(ext => name.endsWith(`.${ext}`)) || contentType.includes('image');
|
|
|
|
if (!isPdfOrWord && !isImage) return false;
|
|
|
|
// Apply search term filter
|
|
return name.includes(term);
|
|
});
|
|
}
|
|
```
|
|
|
|
### File Categorization
|
|
|
|
**Categories:** Manager Info → Contracts → Submittals → Plans → Other
|
|
|
|
**Rules:**
|
|
```javascript
|
|
function categorize(filename) {
|
|
const name = filename.toLowerCase();
|
|
if (name.includes('manager')) return 'managerInfo';
|
|
if (name.includes('contract') || name.includes('estimate')) return 'contracts';
|
|
if (name.includes('submittal')) return 'submittals';
|
|
if (name.includes('plan') || name.includes('blueprint')) return 'plans';
|
|
return 'other';
|
|
}
|
|
```
|
|
|
|
### Sorting Rules
|
|
|
|
**Default Sort:** `-Job_Number` (descending)
|
|
|
|
**Available Sorts:**
|
|
```
|
|
Job_Number, -Job_Number (job #)
|
|
Job_Full_Name, -Job_Full_Name (alphabetical)
|
|
Job_Start_Date, -Job_Start_Date (chronological)
|
|
```
|
|
|
|
**Backend Implementation:**
|
|
```typescript
|
|
const sort = c.req.query('sort') || '-Job_Number';
|
|
const pbUrl = `...?sort=${encodeURIComponent(sort)}`;
|
|
```
|
|
|
|
---
|
|
|
|
## 4. State Management
|
|
|
|
### Frontend State Object
|
|
```javascript
|
|
// fileListState: Tracks current file view
|
|
const fileListState = {
|
|
items: [], // Array of file objects
|
|
filter: '', // Current search term
|
|
jobNumber: '', // Current job number
|
|
jobName: '', // Current job name
|
|
};
|
|
|
|
// Do NOT create other state stores
|
|
// All state flows through this single object
|
|
```
|
|
|
|
### Rules
|
|
- Single source of truth per view
|
|
- State modified only by explicit functions
|
|
- No hidden/global state
|
|
- Clear state on logout
|
|
|
|
---
|
|
|
|
## 5. API Response Format
|
|
|
|
### Success Response
|
|
```json
|
|
{
|
|
"items": [...], // Array of records
|
|
"total": 50, // Total record count
|
|
"page": 1,
|
|
"perPage": 10,
|
|
"source": "cache|fetch" // For debugging
|
|
}
|
|
```
|
|
|
|
### Error Response
|
|
```json
|
|
{
|
|
"error": "User-friendly error message",
|
|
"status": 400
|
|
}
|
|
```
|
|
|
|
### File List Response
|
|
```json
|
|
{
|
|
"items": [
|
|
{
|
|
"id": "...",
|
|
"name": "document.pdf",
|
|
"url": "https://...",
|
|
"driveId": "...",
|
|
"size": 1024,
|
|
"modified": "2026-01-01T00:00:00Z",
|
|
"contentType": "application/pdf",
|
|
"isFolder": false,
|
|
"path": "/folder/path"
|
|
}
|
|
],
|
|
"total": 5,
|
|
"driveId": "...",
|
|
"source": "walk|search"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. View Architecture (Industry Standard)
|
|
|
|
### View Structure
|
|
|
|
```
|
|
AuthView
|
|
└── Login Form
|
|
└── Redirect to LandingView on success
|
|
|
|
LandingView (Home)
|
|
├── Header (User info, Sign Out)
|
|
├── JobCardView
|
|
│ ├── Search Input
|
|
│ ├── Filters (Status, Date Range)
|
|
│ └── Job Cards (Grid)
|
|
│ └── Click → ManagerInfoView
|
|
└── Loading/Error States
|
|
|
|
ManagerInfoView (Job Detail)
|
|
├── Job Header (Job #, Name, Status)
|
|
├── Job Info Panel (dates, manager, contacts)
|
|
├── File List Section
|
|
│ ├── File Search
|
|
│ ├── File Groups (categorized)
|
|
│ └── Preview/Download Buttons
|
|
└── Notes Section
|
|
└── Click → NotesView
|
|
|
|
NotesView (Expanded Notes)
|
|
├── Sticky Notes Panel (read-only for now)
|
|
├── Note Preview
|
|
└── Back to ManagerInfoView
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Naming Conventions
|
|
|
|
### File/Folder Naming
|
|
- `auth/` - Authentication module
|
|
- `backend/` - Server code
|
|
- `frontend/` - Client code
|
|
- `docs/` - Documentation
|
|
- `logs/` - Runtime logs (gitignored)
|
|
- `tests/` - Test files
|
|
|
|
### Variable Naming
|
|
- `pb` - PocketBase instance
|
|
- `authHeaders` - Auth token from PocketBase
|
|
- `fileCache` - Frontend file cache (Map)
|
|
- `fileListState` - Current file view state object
|
|
- `cacheKey` - Redis key (format: `noun:filter:value`)
|
|
|
|
### Class/Type Naming
|
|
- `JobCard` - Individual job card component
|
|
- `FileGroup` - Categorized file group
|
|
- `FileItem` - Single file object
|
|
- `AuthSession` - User session
|
|
|
|
---
|
|
|
|
## 8. Testing & Validation Rules
|
|
|
|
### What to Test
|
|
- Cache hit/miss behavior
|
|
- File filtering (correct types shown)
|
|
- Search functionality (partial matches work)
|
|
- Categorization (files in correct group)
|
|
- Sorting (correct order)
|
|
- Auth session (token validity)
|
|
|
|
### What NOT to Test
|
|
- External API responses (mock them)
|
|
- Redis connection (too fragile)
|
|
- Third-party library behavior
|
|
|
|
---
|
|
|
|
## 9. Performance Rules
|
|
|
|
### Frontend
|
|
- Lazy load job files (don't fetch all at once)
|
|
- Use file cache to avoid redundant fetches
|
|
- Debounce search input (300ms)
|
|
- Virtualize long lists (50+ items)
|
|
|
|
### Backend
|
|
- Cache GET requests always
|
|
- Limit page size to 50 records max
|
|
- Use pagination (default: 10 per page)
|
|
- Timeout file searches after 30 seconds
|
|
|
|
---
|
|
|
|
## 10. Error Handling
|
|
|
|
### Frontend
|
|
```javascript
|
|
try {
|
|
const resp = await fetch('/api/endpoint');
|
|
if (!resp.ok) throw new Error(`Server error: ${resp.status}`);
|
|
const data = await resp.json();
|
|
// Process data
|
|
} catch (err) {
|
|
console.error('Operation failed:', err);
|
|
showErrorUI(err.message);
|
|
}
|
|
```
|
|
|
|
### Backend
|
|
```typescript
|
|
try {
|
|
// Operation
|
|
} catch (err) {
|
|
const message = (err as Error)?.message || String(err);
|
|
console.error('[endpoint] ERROR:', message);
|
|
logLine('logs/error.log', `error message`);
|
|
return c.json({ error: message }, 500);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 11. Logging Rules
|
|
|
|
### What to Log
|
|
- Cache HIT/MISS (for debugging)
|
|
- API request start/end
|
|
- Errors with full stack trace
|
|
- Authentication events (login, logout)
|
|
|
|
### What NOT to Log
|
|
- Tokens or secrets
|
|
- User passwords
|
|
- Personal data (except user ID)
|
|
- Verbose debug output in production
|
|
|
|
### Log Format
|
|
```
|
|
[endpoint] message
|
|
[cache] Cache HIT for key
|
|
[auth] User logged in: user123
|
|
```
|
|
|
|
---
|
|
|
|
## 12. Version Control Rules
|
|
|
|
### Commits
|
|
- Small, atomic commits
|
|
- Clear commit messages: `feat: add caching` or `fix: file filter bug`
|
|
- One feature per commit
|
|
|
|
### Branches
|
|
- `main` - Production ready
|
|
- `develop` - Integration branch
|
|
- `feature/*` - New features
|
|
- `fix/*` - Bug fixes
|
|
|
|
### .gitignore
|
|
```
|
|
node_modules/
|
|
.env
|
|
.env.local
|
|
logs/
|
|
*.log
|
|
dist/
|
|
.bun/
|
|
.DS_Store
|
|
```
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
| Topic | Rule |
|
|
|-------|------|
|
|
| Authentication | Single PocketBase token, no refresh loops |
|
|
| Caching | Redis for GET, TTL-based, cache key format: `noun:filters` |
|
|
| Files | Only PDF, Word, Image types; categorize by filename |
|
|
| State | Single state object per view |
|
|
| Views | Auth → Landing → Detail → Notes |
|
|
| Performance | Lazy load, paginate, debounce |
|
|
| Errors | Catch, log, return JSON response |
|
|
| Testing | Mock external APIs, test core logic |
|
|
|
|
**All new code must follow these rules. Violations will be caught in code review.**
|