Implement server-side file caching with service principal auth

- Remove progress meter for faster perceived load times
- Fix async getGraphToken() calls with await
- Implement service principal (app-only) authentication for file caching
- Pre-cache jobs (2242 jobs) at startup via Redis
- Background file caching for first 300 jobs on continuous cycle
- Cache refreshes every 5 minutes to keep data current
- All caches served instantly from Redis for all users/devices
- No user authentication required for backend caching
- App token auto-refreshes every 55 minutes

Changes:
- backend/src/index.ts: Added service principal flow, startup cache warming, background file caching
- frontend/public/index.html: Removed progress bar, fixed getGraphToken() awaits
- .env.example: Added GRAPH_TOKEN documentation
This commit is contained in:
2026-01-01 18:49:02 +00:00
parent 5974606071
commit 16ed6e412f
3 changed files with 265 additions and 50 deletions
+17 -30
View File
@@ -60,10 +60,6 @@
</div>
<h1 class="m-0 mb-3 text-center text-blue-600 text-[33px] font-bold">Job Info</h1>
<div class="w-full bg-blue-50 h-1.5 rounded overflow-hidden mb-3">
<div id="progressBar" class="h-full w-0 bg-blue-600 transition-all duration-300 ease-out"></div>
</div>
<div class="relative mb-2">
<input id="searchBox" placeholder="Search jobs (just start typing or use voice)" class="w-full p-2.5 pr-12 rounded-lg border border-gray-300 text-lg placeholder:text-gray-400">
<button id="voiceSearchBtn" class="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full flex items-center justify-center hover:bg-gray-100 transition-colors" title="Voice search">
@@ -289,7 +285,6 @@
}
window.clearJobsCache = clearJobsCache;
const progressBar = document.getElementById('progressBar');
const searchBox = document.getElementById('searchBox');
const resultsEl = document.getElementById('results');
const filtersPanel = document.getElementById('filtersPanel');
@@ -341,19 +336,8 @@
}
loadVersion();
let progTimer=null;
function startProgress(){
if(progTimer) clearInterval(progTimer);
let simulatedProg=0;
progTimer=setInterval(()=>{
if(simulatedProg<65){ simulatedProg+=0.8; progressBar.style.width=Math.min(65,simulatedProg).toFixed(1)+'%'; }
},150);
}
function finishProgress(){ if(progTimer) clearInterval(progTimer); progressBar.style.width='100%'; setTimeout(()=>{progressBar.parentElement.classList.add('hidden');},400); }
// --- Fetch all jobs ---
async function fetchAllJobs(){
startProgress();
clearJobsCache('Loading…');
const startTime = performance.now();
let pollCount = 0;
@@ -367,20 +351,17 @@
if (pollCount === 0 || jobsCache.length === 0) {
jobsCache.length = 0;
jobsCache.push(...items);
progressBar.style.width='95%';
applyFiltersAndRender();
}
if (data.partial) {
// If partial, poll again after short delay
pollCount++;
if (pollCount < 10) setTimeout(pollJobs, 600);
else finishProgress();
} else {
finishProgress();
const loadTime = Math.round(performance.now() - startTime);
console.log(`✓ Loaded ${items.length} jobs from API in ${loadTime}ms`);
}
}catch(err){ finishProgress(); alert('Failed to fetch jobs: '+err.message); console.error(err); }
}catch(err){ alert('Failed to fetch jobs: '+err.message); console.error(err); }
}
pollJobs();
}
@@ -1567,7 +1548,7 @@
async function loadFilesForNotes(folderLink) {
console.log('loadFilesForNotes called with:', folderLink);
const graphToken = getGraphToken();
const graphToken = await getGraphToken();
const headers = graphToken ? { 'x-graph-token': graphToken } : {};
try {
@@ -2222,15 +2203,18 @@
return 'other';
};
// File cache for preloading
// File cache for client-side only (server handles pre-caching)
const fileCache = new Map();
async function preloadJobFiles(job) {
const link = job.Job_Folder_Link;
if (!link || fileCache.has(link)) return; // Skip if no link or already cached
if (!link) return;
// Simple client-side cache check
if (fileCache.has(link)) return;
try {
const graphToken = getGraphToken();
const graphToken = await getGraphToken();
const headers = graphToken ? { 'x-graph-token': graphToken } : {};
const controller = new AbortController();
@@ -2246,11 +2230,14 @@
...f,
driveId: f.driveId || rootDriveId
}));
fileCache.set(link, { items, driveId: rootDriveId });
console.log(`Preloaded ${items.length} files for job ${job.Job_Number}`);
fileCache.set(link, { items, driveId: rootDriveId, cachedAt: Date.now() });
console.log(`✓ Cached ${items.length} files for job ${job.Job_Number}`);
}
} catch (err) {
console.log('Preload failed (non-critical):', err);
// Silently fail for background caching
if (err.name !== 'AbortError') {
console.debug(`Cache skip for job ${job.Job_Number}:`, err.message);
}
}
}
@@ -2515,7 +2502,7 @@
return;
}
const graphToken = getGraphToken();
const graphToken = await getGraphToken();
if (!graphToken) {
console.warn('Graph token missing; using fallback URL');
openFileInViewer(fallbackUrl, name);
@@ -2755,7 +2742,7 @@
</div>
`;
const graphToken = getGraphToken();
const graphToken = await getGraphToken();
const headers = graphToken ? { 'x-graph-token': graphToken } : {};
try {