Set all PDFs to 140% zoom (Word docs + native PDFs)

This commit is contained in:
2026-01-01 21:53:56 +00:00
parent c08513636a
commit cbb14e69fb
2 changed files with 287 additions and 62 deletions
+187 -20
View File
@@ -141,16 +141,27 @@ app.get('/api/jobs-all', async (c) => {
if (isRedisConnected()) {
const cached = await getCache(cacheKey);
if (cached) {
logLine('logs/server.log', `Cache HIT for ${cacheKey} (${cached.items?.length || 0} jobs)`);
// Return cached data immediately
// Return first 100 jobs immediately for instant display, rest loads in background
const fastBatch = Array.isArray(cached.items) ? cached.items.slice(0, 100) : [];
const hasMore = cached.items && cached.items.length > 100;
logLine('logs/server.log', `Cache HIT for ${cacheKey} - sending ${fastBatch.length} jobs (${hasMore ? 'partial' : 'complete'})`);
// Refresh cache in background
setImmediate(async () => {
try {
await refreshJobsCache(cacheKey, ttl, c.req.header('Authorization') || '');
} catch (err) {}
});
// For ultra-fast display, send only the first 40 jobs in the initial response
const fastBatch = Array.isArray(cached.items) ? cached.items.slice(0, 40) : [];
return c.json({ ...cached, items: fastBatch, partial: cached.items && cached.items.length > 40 });
return c.json({
page: 1,
perPage: fastBatch.length,
totalItems: cached.totalItems || fastBatch.length,
totalPages: 1,
items: fastBatch,
partial: hasMore
});
} else {
// No cache: return empty array instantly, trigger background fetch
setImmediate(async () => {
@@ -176,6 +187,27 @@ app.get('/api/jobs-all', async (c) => {
}
});
// Get remaining jobs after initial batch
app.get('/api/jobs-remaining', async (c) => {
const cacheKey = 'jobs:all';
try {
if (isRedisConnected()) {
const cached = await getCache(cacheKey);
if (cached && cached.items && cached.items.length > 100) {
const remaining = cached.items.slice(100);
return c.json({
items: remaining,
totalItems: cached.totalItems || cached.items.length
});
}
}
return c.json({ items: [] });
} catch (err) {
logLine('logs/error.log', `jobs-remaining error: ${(err as Error)?.message || String(err)}`);
return c.json({ error: 'Failed to fetch remaining jobs' }, 500);
}
});
// Helper function to fetch all jobs from PocketBase
async function fetchAllJobsFromPocketBase(authHeader: string): Promise<any> {
const allItems: any[] = [];
@@ -510,6 +542,25 @@ app.get('/api/job-file-pdf', async (c) => {
const itemId = c.req.query('itemId');
if (!driveId || !itemId) return c.json({ error: 'driveId and itemId required' }, 400);
// Check cache first
const cacheKey = `job-file-pdf:${driveId}:${itemId}`;
if (isRedisConnected()) {
const cached = await getCache(cacheKey);
if (cached && cached.data) {
logLine('logs/server.log', `PDF cache HIT: ${driveId}:${itemId}`);
const buffer = Buffer.from(cached.data, 'base64');
const headers: Record<string, string> = {
'Content-Type': 'application/pdf',
};
if (cached.filename) {
headers['Content-Disposition'] = `inline; filename="${cached.filename}"`;
}
return new Response(buffer, { status: 200, headers });
} else {
logLine('logs/server.log', `PDF cache MISS: ${driveId}:${itemId}`);
}
}
// Use Graph API to convert to PDF format
const res = await fetch(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content?format=pdf`, {
headers: { Authorization: `Bearer ${token}` },
@@ -519,16 +570,30 @@ app.get('/api/job-file-pdf', async (c) => {
logLine('logs/error.log', `/api/job-file-pdf conversion failed: ${res.status} - ${text}`);
return c.json({ error: `PDF conversion failed: ${res.status}` }, 502);
}
const pdfBuffer = Buffer.from(await res.arrayBuffer());
const headers: Record<string, string> = {
'Content-Type': 'application/pdf',
};
const cd = res.headers.get('content-disposition');
let filename = 'document.pdf';
if (cd) {
// Replace .docx/.doc extension with .pdf in filename
headers['Content-Disposition'] = cd.replace(/\.(docx?)/gi, '.pdf');
const match = cd.match(/filename="?([^"]+)"?/);
if (match) filename = match[1].replace(/\.(docx?)/gi, '.pdf');
}
// Cache the converted PDF for 30 minutes
if (isRedisConnected()) {
await setCache(cacheKey, {
data: pdfBuffer.toString('base64'),
filename
}, 1800);
}
logLine('logs/server.log', `Successfully converted document to PDF: driveId=${driveId}, itemId=${itemId}`);
return new Response(res.body, { status: 200, headers });
return new Response(pdfBuffer, { status: 200, headers });
} catch (err) {
const message = (err as Error)?.message || String(err);
const status = (err as any)?.status || 500;
@@ -595,8 +660,8 @@ logLine('logs/server.log', `Starting frontend server (Redis version) on port ${P
const token = await getAppOnlyToken();
if (token) {
logLine('logs/server.log', 'Service principal authenticated, starting background file caching...');
// Cache files for first 1000 jobs with folder links
startFileCaching(result.items.slice(0, 1000), token);
// Cache files for ALL jobs with folder links
startFileCaching(result.items, token);
} else {
logLine('logs/server.log', 'Service principal not configured, file caching will occur on-demand');
}
@@ -615,9 +680,9 @@ let fileCachingTimer: NodeJS.Timeout | null = null;
async function startFileCaching(jobs: any[], token: string) {
if (fileCachingActive) return;
fileCachingJobs = jobs.filter(job => job.Job_Folder_Link);
fileCachingJobs = jobs;
if (fileCachingJobs.length === 0) {
logLine('logs/server.log', 'No jobs with folder links to cache');
logLine('logs/server.log', 'No jobs to cache');
return;
}
@@ -632,14 +697,16 @@ async function startFileCaching(jobs: any[], token: string) {
async function cacheNextFileBatch() {
if (!fileCachingActive || !isRedisConnected()) return;
const batchSize = 5; // Cache 5 jobs at a time for faster coverage
const batchSize = 10; // Cache 10 jobs at a time for faster initial coverage
const maxJobs = fileCachingJobs.length;
if (fileCachingIndex >= maxJobs) {
// Completed one full cycle, refresh job list and restart after 3 minutes
logLine('logs/server.log', `✓ Completed file caching cycle for ${maxJobs} jobs. Restarting in 3 minutes...`);
// Completed initial full cache of all jobs
console.log(`✓ Completed caching all ${maxJobs} jobs. Scheduling refresh in 10 minutes...`);
logLine('logs/server.log', `✓ Completed caching all ${maxJobs} jobs. Scheduling refresh in 10 minutes...`);
fileCachingIndex = 0;
// After completing all jobs, wait 10 minutes before refreshing
fileCachingTimer = setTimeout(async () => {
try {
// Refresh the jobs cache first
@@ -648,8 +715,9 @@ async function cacheNextFileBatch() {
// Get updated job list from cache
const cached = await getCache('jobs:all');
if (cached && cached.items) {
fileCachingJobs = cached.items.slice(0, 1000).filter((job: any) => job.Job_Folder_Link);
logLine('logs/server.log', `Updated file caching list with ${fileCachingJobs.length} jobs`);
fileCachingJobs = cached.items;
console.log(`Starting refresh cycle: ${fileCachingJobs.length} jobs to update`);
logLine('logs/server.log', `Starting refresh cycle: ${fileCachingJobs.length} jobs to update`);
}
// Refresh token if needed
@@ -658,18 +726,24 @@ async function cacheNextFileBatch() {
fileCachingToken = newToken;
cacheNextFileBatch();
} else {
console.error('Failed to refresh app-only token, stopping file caching');
logLine('logs/error.log', 'Failed to refresh app-only token, stopping file caching');
fileCachingActive = false;
}
} catch (err) {
console.error(`File cache cycle restart error: ${(err as Error)?.message}`);
logLine('logs/error.log', `File cache cycle restart error: ${(err as Error)?.message}`);
}
}, 3 * 60 * 1000);
}, 10 * 60 * 1000);
return;
}
// Get next batch of jobs to cache
const batch = fileCachingJobs.slice(fileCachingIndex, fileCachingIndex + batchSize);
const jobNumbers = batch.map(j => j.Job_Number).join(', ');
const batchMsg = `[Cache Batch] Processing jobs: ${jobNumbers} (${fileCachingIndex}/${maxJobs})`;
console.log(batchMsg);
logLine('logs/server.log', batchMsg);
for (const job of batch) {
try {
@@ -682,13 +756,20 @@ async function cacheNextFileBatch() {
fileCachingIndex += batchSize;
// Continue with next batch after a short delay (2 seconds between batches for faster cycling)
fileCachingTimer = setTimeout(() => cacheNextFileBatch(), 2000);
// Continue with next batch after a short delay (1 second between batches for faster throughput)
fileCachingTimer = setTimeout(() => cacheNextFileBatch(), 1000);
}
async function cacheJobFiles(job: any, token: string) {
const link = job.Job_Folder_Link;
if (!link) return;
if (!link) {
// Job has no folder link, just mark it as cached
const cacheKey = `job-files:${job.Job_Number}`;
await setCache(cacheKey, { items: [] }, 1800);
console.log(`✓ No folder for job ${job.Job_Number}`);
logLine('logs/server.log', `✓ No folder for job ${job.Job_Number}`);
return;
}
const cacheKey = `job-files:${link}`;
const ttl = 1800; // 30 minutes
@@ -714,7 +795,93 @@ async function cacheJobFiles(job: any, token: string) {
};
await setCache(cacheKey, data, ttl);
logLine('logs/server.log', `✓ Cached ${items.length} files for job ${job.Job_Number}`);
// Pre-cache PDF conversions for Word/Excel documents AND native PDF files
const officeFiles = items.filter(item => {
const name = (item.name || '').toLowerCase();
return name.endsWith('.docx') || name.endsWith('.doc') || name.endsWith('.xlsx') || name.endsWith('.xls');
});
const pdfFiles = items.filter(item => {
const name = (item.name || '').toLowerCase();
return name.endsWith('.pdf');
});
let pdfsCached = 0;
// Cache converted Office documents
for (const file of officeFiles) {
try {
const fileDriveId = file.parentReference?.driveId || driveId;
const pdfCacheKey = `job-file-pdf:${fileDriveId}:${file.id}`;
// Check if already cached
const existing = await getCache(pdfCacheKey);
if (existing) {
pdfsCached++;
continue;
}
// Convert and cache
const res = await fetch(`https://graph.microsoft.com/v1.0/drives/${fileDriveId}/items/${file.id}/content?format=pdf`, {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
const pdfBuffer = Buffer.from(await res.arrayBuffer());
const cd = res.headers.get('content-disposition');
let filename = file.name.replace(/\.(docx?|xlsx?)$/gi, '.pdf');
if (cd) {
const match = cd.match(/filename="?([^"]+)"?/);
if (match) filename = match[1].replace(/\.(docx?|xlsx?)$/gi, '.pdf');
}
await setCache(pdfCacheKey, {
data: pdfBuffer.toString('base64'),
filename
}, ttl);
pdfsCached++;
}
} catch (err) {
// Silently skip PDF caching errors
}
}
// Cache native PDF files
for (const file of pdfFiles) {
try {
const fileDriveId = file.parentReference?.driveId || driveId;
const pdfCacheKey = `job-file-pdf:${fileDriveId}:${file.id}`;
// Check if already cached
const existing = await getCache(pdfCacheKey);
if (existing) {
pdfsCached++;
continue;
}
// Fetch and cache native PDF
const res = await fetch(`https://graph.microsoft.com/v1.0/drives/${fileDriveId}/items/${file.id}/content`, {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
const pdfBuffer = Buffer.from(await res.arrayBuffer());
await setCache(pdfCacheKey, {
data: pdfBuffer.toString('base64'),
filename: file.name
}, ttl);
pdfsCached++;
}
} catch (err) {
// Silently skip PDF caching errors
}
}
const totalPDFs = officeFiles.length + pdfFiles.length;
const msg = `✓ Cached ${items.length} files${totalPDFs > 0 ? ` + ${pdfsCached}/${totalPDFs} PDFs` : ''} for job ${job.Job_Number}`;
console.log(msg);
logLine('logs/server.log', msg);
} catch (err: any) {
// Only log if not a token expiration issue
if (!err.message?.includes('401') && !err.message?.includes('token')) {