redis optimization/improvments
This commit is contained in:
@@ -0,0 +1,523 @@
|
||||
# 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.**
|
||||
Reference in New Issue
Block a user