671 lines
20 KiB
Markdown
671 lines
20 KiB
Markdown
# Opus 4.5 Prompt: Job-Info-Prod Initial Architecture & Scaffolding
|
|
|
|
## Project Context & Mission
|
|
|
|
**Project:** Job Info - Production (Job-Info-Prod)
|
|
**Purpose:** Professional field management application for construction/project teams
|
|
**Critical Requirement:** Workers in field cannot afford frequent re-authentication
|
|
**Environment:** Field-deployed, potentially offline, mission-critical
|
|
|
|
---
|
|
|
|
## Core Architecture Requirements
|
|
|
|
### 1. Dual-Token Authentication System
|
|
|
|
The app uses **PocketBase OAuth through Microsoft** to acquire TWO tokens:
|
|
|
|
1. **PocketBase Token** (User session with PocketBase)
|
|
- For all app operations (jobs, files, notes)
|
|
- Managed by PocketBase authentication
|
|
- Expires: 3600 seconds (1 hour)
|
|
|
|
2. **Microsoft Graph Token** (User's Microsoft account access)
|
|
- For SharePoint file access & Office 365 integration
|
|
- Obtained during PocketBase OAuth flow
|
|
- Expires: 3600 seconds (1 hour)
|
|
|
|
### 2. Token Lifecycle (CRITICAL)
|
|
|
|
**Goal:** Users authenticate ONCE at startup, tokens stay fresh for entire session (hours/days)
|
|
|
|
```
|
|
Login Flow:
|
|
├── User clicks "Sign In"
|
|
├── Redirected to PocketBase OAuth (which includes Microsoft)
|
|
├── User grants permissions to app
|
|
├── App receives:
|
|
│ ├── PocketBase token → localStorage
|
|
│ ├── Graph token → localStorage
|
|
│ └── Refresh tokens → Valkey cache (backend)
|
|
├── App initializes background token refresh loop
|
|
└── User sees app, never needs to re-login
|
|
|
|
Token Refresh Loop (Backend):
|
|
├── Every 30 minutes (running in background)
|
|
├── Check token expiry times
|
|
├── Refresh if < 15 minutes to expiry
|
|
├── Update both Valkey cache AND localStorage
|
|
├── Silently continue (no UI interruption)
|
|
└── Never ask user to re-authenticate
|
|
```
|
|
|
|
### 3. Token Storage Strategy
|
|
|
|
**Frontend (localStorage):**
|
|
- `pocketbase_auth` - PocketBase session (includes token)
|
|
- `graph_token` - Microsoft Graph token
|
|
- `token_expires_pb` - Expiry timestamp
|
|
- `token_expires_graph` - Expiry timestamp
|
|
|
|
**Backend (Valkey/Redis Cache):**
|
|
- `tokens:${userId}:pb` - Fresh PocketBase token + refresh token (TTL: 1 hour)
|
|
- `tokens:${userId}:graph` - Fresh Graph token + refresh token (TTL: 1 hour)
|
|
- `tokens:${userId}:refresh_pb` - PocketBase refresh token (TTL: 30 days)
|
|
- `tokens:${userId}:refresh_graph` - Graph refresh token (TTL: 30 days)
|
|
|
|
**Why dual storage:**
|
|
- Frontend has tokens for immediate use
|
|
- Backend cache keeps tokens warm & refreshed
|
|
- If frontend token expires, backend can refresh silently
|
|
- If Redis goes down, frontend token still works
|
|
- If frontend loses token, can get from Redis
|
|
|
|
### 4. Single Token Check Logic
|
|
|
|
Before ANY API call:
|
|
|
|
```javascript
|
|
// Frontend check
|
|
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. Check if fresh (> 15 min remaining)
|
|
if (token && expiry > Date.now() + 15*60*1000) {
|
|
return token; // Use immediately
|
|
}
|
|
|
|
// 3. If not fresh, try backend refresh
|
|
const refreshed = await fetch('/api/refresh-token', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ type })
|
|
});
|
|
|
|
if (refreshed.ok) {
|
|
const { token: newToken, expiresAt } = await refreshed.json();
|
|
localStorage.setItem(`${type}_token`, newToken);
|
|
localStorage.setItem(`token_expires_${type}`, expiresAt);
|
|
return newToken;
|
|
}
|
|
|
|
// 4. If backend refresh failed, redirect to login
|
|
if (!token) {
|
|
redirectToLogin();
|
|
throw new Error('Authentication required');
|
|
}
|
|
|
|
// 5. Use stale token as fallback (better than nothing in field)
|
|
console.warn('Using stale token - backend unreachable');
|
|
return token;
|
|
}
|
|
```
|
|
|
|
### 5. Background Token Refresh (CRITICAL)
|
|
|
|
Backend runs continuous refresh loop:
|
|
|
|
```typescript
|
|
// Backend service
|
|
class TokenRefreshService {
|
|
async refreshTokensInBackground() {
|
|
// Every 30 minutes for each logged-in user
|
|
|
|
for (const userId of getActiveUsers()) {
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 6. Offline-First Capability
|
|
|
|
**When backend is down:**
|
|
- Frontend still works with cached tokens
|
|
- File list cached in memory (Map)
|
|
- File content cached (if accessed before)
|
|
- UI shows "Offline" indicator but remains functional
|
|
- All operations queue locally
|
|
- Sync when backend returns
|
|
|
|
**When network is intermittent:**
|
|
- Requests timeout after 30 seconds
|
|
- Automatic retry with exponential backoff
|
|
- Cache provides fallback data
|
|
- User sees "Trying to reconnect..." briefly
|
|
- App continues working
|
|
|
|
### 7. Error Handling & Graceful Degradation
|
|
|
|
```
|
|
Token Missing
|
|
├── During Init → Redirect to login
|
|
├── During API call → Prompt to login (non-blocking overlay)
|
|
└── In background → Log, don't interrupt user
|
|
|
|
Token Expired
|
|
├── < 15 min to expiry → Silently refresh from backend
|
|
├── Expired & refresh fails → Use stale token if available
|
|
└── Completely unavailable → Read from cache, queue for sync
|
|
|
|
Backend Unreachable
|
|
├── Token refresh → Skip, use current token
|
|
├── API call → Use cache, retry when back
|
|
└── File fetch → Use cached file list
|
|
```
|
|
|
|
---
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
Job-Info-Prod/
|
|
├── auth/ # Auth rules from Job-Info-Test
|
|
│ ├── RULES.md, FLOWMAP.md, VIEWS.md, etc.
|
|
│ └── auth-universal.js # Base auth module (to be enhanced)
|
|
│
|
|
├── backend/
|
|
│ ├── src/
|
|
│ │ ├── index.ts # Main server
|
|
│ │ ├── config/
|
|
│ │ │ ├── env.ts # Environment config
|
|
│ │ │ └── constants.ts # App constants
|
|
│ │ ├── middleware/
|
|
│ │ │ ├── auth.ts # Token validation
|
|
│ │ │ ├── errorHandler.ts # Error handling
|
|
│ │ │ └── logger.ts # Request logging
|
|
│ │ ├── routes/
|
|
│ │ │ ├── auth.ts # /api/auth endpoints (login, refresh, logout)
|
|
│ │ │ ├── jobs.ts # /api/jobs endpoints
|
|
│ │ │ ├── files.ts # /api/job-files endpoints
|
|
│ │ │ ├── health.ts # /health endpoint
|
|
│ │ │ └── index.ts # Route registration
|
|
│ │ ├── services/
|
|
│ │ │ ├── tokenService.ts # Token refresh & management
|
|
│ │ │ ├── cacheService.ts # Redis operations
|
|
│ │ │ ├── pocketbaseService.ts # PocketBase integration
|
|
│ │ │ ├── graphService.ts # Microsoft Graph integration
|
|
│ │ │ └── jobsService.ts # Business logic
|
|
│ │ ├── types/
|
|
│ │ │ ├── auth.ts # Auth interfaces
|
|
│ │ │ ├── job.ts # Job interfaces
|
|
│ │ │ └── common.ts # Common types
|
|
│ │ └── utils/
|
|
│ │ ├── logger.ts # Logging utility
|
|
│ │ └── validators.ts # Input validation
|
|
│ ├── tests/
|
|
│ │ ├── auth.test.ts
|
|
│ │ ├── token-refresh.test.ts
|
|
│ │ └── jobs.test.ts
|
|
│ └── README.md
|
|
│
|
|
├── frontend/
|
|
│ ├── public/
|
|
│ │ ├── index.html # Main app
|
|
│ │ ├── signin.html # OAuth redirect page
|
|
│ │ ├── offline.html # Offline fallback
|
|
│ │ └── assets/
|
|
│ ├── src/
|
|
│ │ ├── main.ts # Entry point
|
|
│ │ ├── auth/
|
|
│ │ │ ├── client.ts # Token management
|
|
│ │ │ ├── pocketbase.ts # PocketBase instance
|
|
│ │ │ └── refresh-loop.ts # Background refresh
|
|
│ │ ├── views/
|
|
│ │ │ ├── auth-view.ts # Sign in page
|
|
│ │ │ ├── landing-view.ts # Jobs list
|
|
│ │ │ ├── job-detail-view.ts # Job detail
|
|
│ │ │ └── notes-view.ts # Expanded notes
|
|
│ │ ├── services/
|
|
│ │ │ ├── api-client.ts # API requests
|
|
│ │ │ ├── cache.ts # Local file cache
|
|
│ │ │ ├── state.ts # Global state
|
|
│ │ │ └── offline.ts # Offline handling
|
|
│ │ ├── components/
|
|
│ │ │ ├── job-card.ts
|
|
│ │ │ ├── file-list.ts
|
|
│ │ │ └── sticky-notes.ts
|
|
│ │ ├── types/
|
|
│ │ │ ├── auth.ts
|
|
│ │ │ └── job.ts
|
|
│ │ └── styles/
|
|
│ │ ├── main.css
|
|
│ │ └── tailwind.css
|
|
│ ├── tests/
|
|
│ └── README.md
|
|
│
|
|
├── shared/
|
|
│ ├── types/
|
|
│ │ ├── auth.ts # Auth interfaces
|
|
│ │ └── job.ts # Job interfaces
|
|
│ └── constants/
|
|
│ └── index.ts
|
|
│
|
|
├── docs/
|
|
│ ├── ARCHITECTURE.md # System design
|
|
│ ├── API.md # API reference
|
|
│ ├── TOKEN_LIFECYCLE.md # Token management detail
|
|
│ ├── OFFLINE_STRATEGY.md # Offline-first approach
|
|
│ └── DEPLOYMENT.md # Production deployment
|
|
│
|
|
├── scripts/
|
|
│ ├── setup.sh
|
|
│ └── deploy.sh
|
|
│
|
|
├── .env.example
|
|
├── .gitignore
|
|
├── package.json
|
|
├── tsconfig.json
|
|
├── tailwind.config.js
|
|
├── postcss.config.js
|
|
├── bun.lockb
|
|
├── README.md
|
|
└── CHANGELOG.md
|
|
```
|
|
|
|
---
|
|
|
|
## Key Implementation Details
|
|
|
|
### Authentication Flow
|
|
|
|
1. **User Visits App**
|
|
```
|
|
App Load
|
|
├── Check localStorage for auth
|
|
├── If found & fresh → Load app
|
|
├── If not found → Redirect to signin.html
|
|
└── If expired → Prompt to re-login
|
|
```
|
|
|
|
2. **Sign In Page (signin.html)**
|
|
```
|
|
signin.html (Hosted by backend)
|
|
├── Redirects to PocketBase OAuth
|
|
│ └── PocketBase includes Microsoft scopes
|
|
│ ├── offline_access (for refresh tokens)
|
|
│ ├── Files.Read (SharePoint)
|
|
│ ├── Sites.Read.All (Sites)
|
|
│ └── User.Read (Profile)
|
|
├── PocketBase handles OAuth flow
|
|
├── Returns both tokens
|
|
└── Redirects back to app with tokens
|
|
```
|
|
|
|
3. **Initial Token Setup**
|
|
```typescript
|
|
// Backend endpoint: POST /api/auth/login-callback
|
|
async function handleLoginCallback(req) {
|
|
const { pbToken, graphToken } = req.body;
|
|
|
|
// 1. Validate tokens
|
|
const pbUser = await validatePocketBaseToken(pbToken);
|
|
const graphUser = await validateGraphToken(graphToken);
|
|
|
|
// 2. Store in Valkey cache
|
|
const userId = pbUser.id;
|
|
await cache.set(`tokens:${userId}:pb`, {
|
|
token: pbToken,
|
|
refresh_token: pbToken.refresh,
|
|
expires_at: pbUser.expires
|
|
}, ttl: 3600);
|
|
|
|
await cache.set(`tokens:${userId}:graph`, {
|
|
token: graphToken,
|
|
refresh_token: graphToken.refresh,
|
|
expires_at: graphUser.expires
|
|
}, ttl: 3600);
|
|
|
|
// 3. Start background refresh loop for this user
|
|
tokenRefreshService.addUser(userId);
|
|
|
|
// 4. Return tokens to frontend
|
|
return {
|
|
pbToken,
|
|
graphToken,
|
|
expiresAt_pb: pbUser.expires,
|
|
expiresAt_graph: graphUser.expires
|
|
};
|
|
}
|
|
```
|
|
|
|
### Token Refresh Endpoints
|
|
|
|
**POST /api/refresh-token**
|
|
```typescript
|
|
async function refreshToken(c: Context) {
|
|
const userId = c.get('userId'); // From auth middleware
|
|
const type = c.req.query('type'); // 'pb' or 'graph'
|
|
|
|
// 1. Try to get fresh token from cache
|
|
const cached = await cache.get(`tokens:${userId}:${type}`);
|
|
|
|
if (cached && !isExpiring(cached)) {
|
|
return c.json({
|
|
token: cached.token,
|
|
expiresAt: cached.expires_at,
|
|
source: 'cache'
|
|
});
|
|
}
|
|
|
|
// 2. If expiring soon, refresh using refresh token
|
|
const refreshToken = cached?.refresh_token;
|
|
if (refreshToken) {
|
|
const refreshed = type === 'pb'
|
|
? await refreshPocketBaseToken(refreshToken)
|
|
: await refreshMicrosoftGraphToken(refreshToken);
|
|
|
|
await cache.set(`tokens:${userId}:${type}`, refreshed, ttl: 3600);
|
|
|
|
return c.json({
|
|
token: refreshed.token,
|
|
expiresAt: refreshed.expires_at,
|
|
source: 'refreshed'
|
|
});
|
|
}
|
|
|
|
// 3. If no refresh token, authentication required
|
|
return c.json({ error: 'Authentication required' }, 401);
|
|
}
|
|
```
|
|
|
|
### Background Refresh Service
|
|
|
|
```typescript
|
|
class TokenRefreshService {
|
|
private refreshLoops = new Map<string, NodeJS.Timer>();
|
|
|
|
addUser(userId: string) {
|
|
if (this.refreshLoops.has(userId)) return; // Already running
|
|
|
|
// Start 30-minute refresh loop for this user
|
|
const loop = setInterval(() => {
|
|
this.refreshUserTokens(userId).catch(err => {
|
|
console.error(`Token refresh failed for ${userId}:`, err);
|
|
});
|
|
}, 30 * 60 * 1000); // 30 minutes
|
|
|
|
this.refreshLoops.set(userId, loop);
|
|
console.log(`Token refresh loop started for user ${userId}`);
|
|
}
|
|
|
|
removeUser(userId: string) {
|
|
const loop = this.refreshLoops.get(userId);
|
|
if (loop) {
|
|
clearInterval(loop);
|
|
this.refreshLoops.delete(userId);
|
|
console.log(`Token refresh loop stopped for user ${userId}`);
|
|
}
|
|
}
|
|
|
|
private async refreshUserTokens(userId: string) {
|
|
// PocketBase
|
|
const pbToken = await cache.get(`tokens:${userId}:pb`);
|
|
if (pbToken && isExpiring(pbToken)) {
|
|
const refreshed = await refreshPocketBaseToken(pbToken.refresh_token);
|
|
await cache.set(`tokens:${userId}:pb`, refreshed, ttl: 3600);
|
|
console.log(`[Refresh] PocketBase token refreshed for ${userId}`);
|
|
}
|
|
|
|
// Graph
|
|
const graphToken = await cache.get(`tokens:${userId}:graph`);
|
|
if (graphToken && isExpiring(graphToken)) {
|
|
const refreshed = await refreshMicrosoftGraphToken(graphToken.refresh_token);
|
|
await cache.set(`tokens:${userId}:graph`, refreshed, ttl: 3600);
|
|
console.log(`[Refresh] Graph token refreshed for ${userId}`);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Frontend Token Management
|
|
|
|
```typescript
|
|
// frontend/src/auth/client.ts
|
|
|
|
class TokenClient {
|
|
private pbToken: string | null = null;
|
|
private graphToken: string | null = null;
|
|
private pbExpiry: number = 0;
|
|
private graphExpiry: number = 0;
|
|
|
|
// Load from localStorage on app start
|
|
loadTokens() {
|
|
this.pbToken = localStorage.getItem('pocketbase_auth');
|
|
this.graphToken = localStorage.getItem('graph_token');
|
|
this.pbExpiry = parseInt(localStorage.getItem('token_expires_pb') || '0');
|
|
this.graphExpiry = parseInt(localStorage.getItem('token_expires_graph') || '0');
|
|
|
|
return this.pbToken && this.graphToken;
|
|
}
|
|
|
|
// Get token, refresh if needed
|
|
async ensureToken(type: 'pb' | 'graph'): Promise<string> {
|
|
const token = type === 'pb' ? this.pbToken : this.graphToken;
|
|
const expiry = type === 'pb' ? this.pbExpiry : this.graphExpiry;
|
|
|
|
// If fresh (> 15 min remaining), return immediately
|
|
if (token && expiry > Date.now() + 15*60*1000) {
|
|
return token;
|
|
}
|
|
|
|
// Try to refresh from backend
|
|
try {
|
|
const response = await fetch(`/api/refresh-token?type=${type}`);
|
|
if (response.ok) {
|
|
const { token: newToken, expiresAt } = await response.json();
|
|
this.storeToken(type, newToken, expiresAt);
|
|
return newToken;
|
|
}
|
|
} catch (err) {
|
|
console.warn(`Failed to refresh ${type} token:`, err);
|
|
}
|
|
|
|
// Fallback to stale token if available
|
|
if (token) {
|
|
console.warn(`Using stale ${type} token (backend unreachable)`);
|
|
return token;
|
|
}
|
|
|
|
// No token available - must re-authenticate
|
|
throw new Error('Authentication required');
|
|
}
|
|
|
|
private storeToken(type: 'pb' | 'graph', token: string, expiresAt: number) {
|
|
if (type === 'pb') {
|
|
this.pbToken = token;
|
|
this.pbExpiry = expiresAt;
|
|
localStorage.setItem('pocketbase_auth', token);
|
|
localStorage.setItem('token_expires_pb', expiresAt.toString());
|
|
} else {
|
|
this.graphToken = token;
|
|
this.graphExpiry = expiresAt;
|
|
localStorage.setItem('graph_token', token);
|
|
localStorage.setItem('token_expires_graph', expiresAt.toString());
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Logout
|
|
|
|
```typescript
|
|
// POST /api/auth/logout
|
|
async function logout(c: Context) {
|
|
const userId = c.get('userId');
|
|
|
|
// 1. Stop background refresh
|
|
tokenRefreshService.removeUser(userId);
|
|
|
|
// 2. Clear backend cache
|
|
await cache.del(`tokens:${userId}:pb`);
|
|
await cache.del(`tokens:${userId}:graph`);
|
|
await cache.del(`tokens:${userId}:refresh_pb`);
|
|
await cache.del(`tokens:${userId}:refresh_graph`);
|
|
|
|
// 3. Revoke tokens with PocketBase & Microsoft
|
|
// (if APIs support it)
|
|
|
|
return c.json({ success: true });
|
|
}
|
|
|
|
// Frontend
|
|
function handleLogout() {
|
|
// 1. Clear localStorage
|
|
localStorage.removeItem('pocketbase_auth');
|
|
localStorage.removeItem('graph_token');
|
|
localStorage.removeItem('token_expires_pb');
|
|
localStorage.removeItem('token_expires_graph');
|
|
|
|
// 2. Call backend logout
|
|
fetch('/api/auth/logout', { method: 'POST' });
|
|
|
|
// 3. Redirect to signin
|
|
window.location.href = 'signin.html';
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Environment Variables
|
|
|
|
```
|
|
# .env.example
|
|
|
|
# Server
|
|
PORT=3005
|
|
NODE_ENV=production
|
|
|
|
# Redis/Valkey
|
|
REDIS_HOST=localhost
|
|
REDIS_PORT=6379
|
|
REDIS_PASSWORD=
|
|
|
|
# PocketBase
|
|
POCKETBASE_URL=https://pocketbase.ccllc.pro
|
|
POCKETBASE_ADMIN_EMAIL=admin@example.com
|
|
POCKETBASE_ADMIN_PASSWORD=
|
|
|
|
# Microsoft OAuth
|
|
MICROSOFT_CLIENT_ID=
|
|
MICROSOFT_CLIENT_SECRET=
|
|
MICROSOFT_REDIRECT_URI=http://localhost:3005/api/auth/callback
|
|
|
|
# Logging
|
|
LOG_LEVEL=info
|
|
LOG_FILE=logs/app.log
|
|
ERROR_LOG_FILE=logs/error.log
|
|
```
|
|
|
|
---
|
|
|
|
## Tech Stack (ENFORCED)
|
|
|
|
- **Runtime:** Bun (latest)
|
|
- **Framework:** Hono ^4.10.8
|
|
- **Auth:** PocketBase ^0.26.5 + Microsoft OAuth
|
|
- **Cache:** ioredis ^5.x (Valkey compatible)
|
|
- **CSS:** Tailwind CSS ^4.1.18
|
|
- **Language:** TypeScript ^5 (strict mode)
|
|
- **Testing:** Bun test + Vitest
|
|
|
|
---
|
|
|
|
## Deliverables for Phase 1
|
|
|
|
1. ✅ Complete project scaffolding
|
|
2. ✅ Backend structure (services, routes, types)
|
|
3. ✅ Frontend structure (views, services, auth)
|
|
4. ✅ Token lifecycle implementation
|
|
5. ✅ Background refresh service
|
|
6. ✅ Error handling & offline support
|
|
7. ✅ Configuration & environment
|
|
8. ✅ Comprehensive documentation
|
|
9. ✅ Example tests
|
|
|
|
**No implementations needed yet** - just professional structure & architecture.
|
|
|
|
---
|
|
|
|
## Important Notes
|
|
|
|
### Why Dual Tokens?
|
|
- PocketBase manages user session & app data
|
|
- Microsoft Graph needed for future Office 365 integration
|
|
- Both need to stay fresh independently
|
|
- One failing doesn't stop the app
|
|
|
|
### Why Background Refresh?
|
|
- Workers in field can't keep re-authenticating
|
|
- Tokens are automatically kept fresh
|
|
- Even if initial login was 8 hours ago, tokens stay valid
|
|
- Only system restart resets auth state
|
|
|
|
### Why Offline-First?
|
|
- Field workers lose connectivity
|
|
- Cached data keeps app functional
|
|
- When connectivity returns, data syncs
|
|
- Users never feel stuck
|
|
|
|
### Why This Matters
|
|
- **Field workers = mission critical**
|
|
- **Every minute they're not working = lost productivity**
|
|
- **Network is unreliable in field**
|
|
- **App must be bulletproof**
|
|
|
|
---
|
|
|
|
## Key Success Criteria
|
|
|
|
✅ Users authenticate once, tokens stay fresh indefinitely
|
|
✅ No user-facing re-authentication (except system restart)
|
|
✅ Graceful degradation when backend is down
|
|
✅ Offline functionality for cached data
|
|
✅ Both tokens always warm in cache
|
|
✅ Automatic background refresh (invisible to user)
|
|
✅ Fallback to stale tokens if refresh fails
|
|
✅ Enterprise-grade error handling
|
|
✅ Comprehensive logging for debugging
|
|
✅ Production-ready from day one
|
|
|
|
---
|
|
|
|
## Ready for Implementation
|
|
|
|
This prompt provides:
|
|
- ✅ Complete architecture understanding
|
|
- ✅ Dual-token lifecycle strategy
|
|
- ✅ Field-worker requirements (no re-auth)
|
|
- ✅ Offline-first approach
|
|
- ✅ Detailed implementation patterns
|
|
- ✅ Project structure
|
|
- ✅ Environment setup
|
|
- ✅ Tech stack enforcement
|
|
|
|
**Proceed with scaffolding Job-Info-Prod following this architecture.**
|