Mode2Test/AuthAndToken: assume tokens always fresh with background refresh; fix jobs API to use correct collection; align Home UI to Mode1 (exact fields, styling, strict Active dot); add full-field IndexedDB cache w/ pagination; remove progress bar; dev TLS bypass; rebuild bundles
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
// PERMANENT: IndexedDB cache for jobs data
|
||||
// Provides lightning-fast job listing with zero-delay loading
|
||||
//
|
||||
// Strategy:
|
||||
// - Display cached jobs instantly on page load (0ms delay)
|
||||
// - Fetch fresh data in background after displaying cache
|
||||
// - Cache TTL: 5 minutes (configurable)
|
||||
// - Stores minimal fields for card display only
|
||||
//
|
||||
// Fields cached: Job_Number, Job_Name, Division, Active, Status, Estimator
|
||||
//
|
||||
// Usage:
|
||||
// await jobCache.initialize();
|
||||
// const jobs = await jobCache.getCachedJobs(); // Instant
|
||||
// await jobCache.refreshJobs(); // Background refresh
|
||||
|
||||
export interface JobCard {
|
||||
Job_Number: string;
|
||||
Job_Name: string;
|
||||
Division: string;
|
||||
Active: string;
|
||||
Status: string;
|
||||
Estimator: string;
|
||||
}
|
||||
|
||||
interface CacheMetadata {
|
||||
lastUpdated: number;
|
||||
version: number;
|
||||
}
|
||||
|
||||
const DB_NAME = 'JobInfoCache';
|
||||
const DB_VERSION = 2; // bump to invalidate old minimal-field cache
|
||||
const JOBS_STORE = 'jobs';
|
||||
const META_STORE = 'metadata';
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
const PAGE_SIZE = 500; // match Mode1 bulk fetch
|
||||
|
||||
class JobCache {
|
||||
private db: IDBDatabase | null = null;
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
|
||||
// Create jobs store with Job_Number as key
|
||||
if (!db.objectStoreNames.contains(JOBS_STORE)) {
|
||||
db.createObjectStore(JOBS_STORE, { keyPath: 'Job_Number' });
|
||||
}
|
||||
|
||||
// Create metadata store
|
||||
if (!db.objectStoreNames.contains(META_STORE)) {
|
||||
db.createObjectStore(META_STORE);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getCachedJobs(): Promise<JobCard[]> {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([JOBS_STORE], 'readonly');
|
||||
const store = transaction.objectStore(JOBS_STORE);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async isCacheValid(): Promise<boolean> {
|
||||
if (!this.db) return false;
|
||||
|
||||
try {
|
||||
const metadata = await this.getMetadata();
|
||||
if (!metadata || !metadata.lastUpdated) return false;
|
||||
|
||||
const age = Date.now() - metadata.lastUpdated;
|
||||
return age < CACHE_TTL;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async saveJobs(jobs: JobCard[]): Promise<void> {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([JOBS_STORE, META_STORE], 'readwrite');
|
||||
const jobsStore = transaction.objectStore(JOBS_STORE);
|
||||
const metaStore = transaction.objectStore(META_STORE);
|
||||
|
||||
// Clear existing jobs
|
||||
jobsStore.clear();
|
||||
|
||||
// Save new jobs
|
||||
jobs.forEach(job => jobsStore.add(job));
|
||||
|
||||
// Update metadata
|
||||
const metadata: CacheMetadata = {
|
||||
lastUpdated: Date.now(),
|
||||
version: DB_VERSION
|
||||
};
|
||||
metaStore.put(metadata, 'main');
|
||||
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
}
|
||||
|
||||
async refreshJobs(): Promise<JobCard[]> {
|
||||
try {
|
||||
// Fetch fresh data from backend with pagination to mirror Mode1 behavior
|
||||
let page = 1;
|
||||
let all: JobCard[] = [];
|
||||
let totalPages = 1;
|
||||
|
||||
while (page <= totalPages) {
|
||||
const url = `/api/jobs?page=${page}&perPage=${PAGE_SIZE}&sort=-Job_Number`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch jobs (status ${response.status})`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const items: JobCard[] = data.items || [];
|
||||
const reportedTotalPages = data.totalPages || 1;
|
||||
|
||||
all = all.concat(items);
|
||||
totalPages = Math.max(totalPages, reportedTotalPages);
|
||||
page += 1;
|
||||
}
|
||||
|
||||
// Save to cache
|
||||
await this.saveJobs(all);
|
||||
|
||||
return all;
|
||||
} catch (err) {
|
||||
console.error('Failed to refresh jobs cache:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async getMetadata(): Promise<CacheMetadata | null> {
|
||||
if (!this.db) return null;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([META_STORE], 'readonly');
|
||||
const store = transaction.objectStore(META_STORE);
|
||||
const request = store.get('main');
|
||||
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async clearCache(): Promise<void> {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([JOBS_STORE, META_STORE], 'readwrite');
|
||||
|
||||
transaction.objectStore(JOBS_STORE).clear();
|
||||
transaction.objectStore(META_STORE).clear();
|
||||
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
}
|
||||
|
||||
async getCacheAge(): Promise<number | null> {
|
||||
const metadata = await this.getMetadata();
|
||||
if (!metadata || !metadata.lastUpdated) return null;
|
||||
|
||||
return Date.now() - metadata.lastUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
export const jobCache = new JobCache();
|
||||
Reference in New Issue
Block a user