Files
Job-Info/instructions/auth/RULES.md
T
2026-01-09 04:08:06 +00:00

13 KiB

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):

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:

// ✅ 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)

// 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

// 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)

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:

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:

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:

const sort = c.req.query('sort') || '-Job_Number';
const pbUrl = `...?sort=${encodeURIComponent(sort)}`;

4. State Management

Frontend State Object

// 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

{
  "items": [...],      // Array of records
  "total": 50,         // Total record count
  "page": 1,
  "perPage": 10,
  "source": "cache|fetch"  // For debugging
}

Error Response

{
  "error": "User-friendly error message",
  "status": 400
}

File List Response

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

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

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.