Add Redis/Valkey caching with sessionStorage client-side cache and improved asset loading
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import Redis from 'ioredis';
|
||||
|
||||
// Create Redis client
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
retryStrategy(times) {
|
||||
const delay = Math.min(times * 50, 2000);
|
||||
return delay;
|
||||
},
|
||||
maxRetriesPerRequest: 3,
|
||||
lazyConnect: true,
|
||||
});
|
||||
|
||||
// Handle connection events
|
||||
redis.on('connect', () => {
|
||||
console.log('✓ Redis connected');
|
||||
});
|
||||
|
||||
redis.on('error', (err) => {
|
||||
console.error('Redis error:', err.message);
|
||||
});
|
||||
|
||||
redis.on('close', () => {
|
||||
console.log('Redis connection closed');
|
||||
});
|
||||
|
||||
// Connect to Redis
|
||||
(async () => {
|
||||
try {
|
||||
await redis.connect();
|
||||
} catch (err) {
|
||||
console.error('Failed to connect to Redis:', err);
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* Get a value from cache
|
||||
*/
|
||||
export async function getCache(key: string): Promise<any | null> {
|
||||
try {
|
||||
const value = await redis.get(key);
|
||||
if (!value) return null;
|
||||
return JSON.parse(value);
|
||||
} catch (err) {
|
||||
console.error(`Cache get error for key "${key}":`, err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value in cache with TTL in seconds
|
||||
*/
|
||||
export async function setCache(key: string, value: any, ttl: number = 300): Promise<boolean> {
|
||||
try {
|
||||
const serialized = JSON.stringify(value);
|
||||
await redis.setex(key, ttl, serialized);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`Cache set error for key "${key}":`, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a key from cache
|
||||
*/
|
||||
export async function deleteCache(key: string): Promise<boolean> {
|
||||
try {
|
||||
await redis.del(key);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`Cache delete error for key "${key}":`, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache keys matching a pattern
|
||||
*/
|
||||
export async function clearCachePattern(pattern: string): Promise<number> {
|
||||
try {
|
||||
const keys = await redis.keys(pattern);
|
||||
if (keys.length === 0) return 0;
|
||||
await redis.del(...keys);
|
||||
return keys.length;
|
||||
} catch (err) {
|
||||
console.error(`Cache clear error for pattern "${pattern}":`, err);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Redis is connected
|
||||
*/
|
||||
export function isRedisConnected(): boolean {
|
||||
return redis.status === 'ready';
|
||||
}
|
||||
|
||||
export default redis;
|
||||
+123
-3
@@ -3,6 +3,7 @@ import { serveStatic } from 'hono/bun';
|
||||
import fs from 'fs';
|
||||
// @ts-ignore Bun project may not have dotenv types configured
|
||||
import { config } from 'dotenv';
|
||||
import { getCache, setCache, isRedisConnected } from './redis-cache';
|
||||
|
||||
config();
|
||||
|
||||
@@ -29,6 +30,119 @@ app.get('/version', (c) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Jobs endpoint with Redis caching
|
||||
app.get('/api/jobs', async (c) => {
|
||||
const page = parseInt(c.req.query('page') || '1');
|
||||
const perPage = parseInt(c.req.query('perPage') || '50');
|
||||
const sort = c.req.query('sort') || '-Job_Number';
|
||||
|
||||
const cacheKey = `jobs:page:${page}:perPage:${perPage}:sort:${sort}`;
|
||||
const ttl = 300; // 5 minutes cache
|
||||
|
||||
try {
|
||||
// Try to get from cache first
|
||||
if (isRedisConnected()) {
|
||||
const cached = await getCache(cacheKey);
|
||||
if (cached) {
|
||||
logLine('logs/server.log', `Cache HIT for ${cacheKey}`);
|
||||
return c.json(cached);
|
||||
}
|
||||
logLine('logs/server.log', `Cache MISS for ${cacheKey}`);
|
||||
}
|
||||
|
||||
// Fetch from PocketBase
|
||||
const pbUrl = `https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records?page=${page}&perPage=${perPage}&sort=${sort}`;
|
||||
const authHeader = c.req.header('Authorization') || '';
|
||||
|
||||
const response = await fetch(pbUrl, {
|
||||
headers: authHeader ? { Authorization: authHeader } : {}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`PocketBase returned ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Cache the result
|
||||
if (isRedisConnected()) {
|
||||
await setCache(cacheKey, data, ttl);
|
||||
logLine('logs/server.log', `Cached ${cacheKey} for ${ttl}s`);
|
||||
}
|
||||
|
||||
return c.json(data);
|
||||
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `jobs endpoint error: ${(err as Error)?.message || String(err)}`);
|
||||
return c.json({ error: 'Failed to fetch jobs' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Fast endpoint: returns all jobs in a single cached request
|
||||
app.get('/api/jobs-all', async (c) => {
|
||||
const cacheKey = 'jobs:all';
|
||||
const ttl = 300; // 5 minutes cache
|
||||
|
||||
try {
|
||||
// Try to get from cache first
|
||||
if (isRedisConnected()) {
|
||||
const cached = await getCache(cacheKey);
|
||||
if (cached) {
|
||||
logLine('logs/server.log', `Cache HIT for ${cacheKey} (${cached.items?.length || 0} jobs)`);
|
||||
return c.json(cached);
|
||||
}
|
||||
logLine('logs/server.log', `Cache MISS for ${cacheKey} - fetching all jobs`);
|
||||
}
|
||||
|
||||
const authHeader = c.req.header('Authorization') || '';
|
||||
const allItems: any[] = [];
|
||||
let page = 1;
|
||||
const perPage = 500; // Large page size
|
||||
|
||||
// Fetch all pages from PocketBase
|
||||
while (true) {
|
||||
const pbUrl = `https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records?page=${page}&perPage=${perPage}&sort=-Job_Number`;
|
||||
const response = await fetch(pbUrl, {
|
||||
headers: authHeader ? { Authorization: authHeader } : {}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`PocketBase returned ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const items = data.items || [];
|
||||
|
||||
if (items.length === 0) break;
|
||||
allItems.push(...items);
|
||||
|
||||
if (allItems.length >= (data.totalItems || 0)) break;
|
||||
page++;
|
||||
}
|
||||
|
||||
const result = {
|
||||
page: 1,
|
||||
perPage: allItems.length,
|
||||
totalItems: allItems.length,
|
||||
totalPages: 1,
|
||||
items: allItems
|
||||
};
|
||||
|
||||
// Cache the result
|
||||
if (isRedisConnected()) {
|
||||
await setCache(cacheKey, result, ttl);
|
||||
logLine('logs/server.log', `Cached ${cacheKey} with ${allItems.length} jobs for ${ttl}s`);
|
||||
}
|
||||
|
||||
return c.json(result);
|
||||
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `jobs-all endpoint error: ${(err as Error)?.message || String(err)}`);
|
||||
return c.json({ error: 'Failed to fetch jobs' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// --- Microsoft Graph helpers ---
|
||||
const getGraphToken = (c?: any) => {
|
||||
const headerToken = c?.req?.header ? c.req.header('x-graph-token') : '';
|
||||
@@ -263,6 +377,12 @@ app.post('/log-change', async (c) => {
|
||||
});
|
||||
|
||||
// Serve static files from the frontend directory (must come after API routes)
|
||||
// Add cache headers for static assets
|
||||
app.use('/assets/*', async (c, next) => {
|
||||
await next();
|
||||
// Cache assets for 1 year
|
||||
c.header('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
});
|
||||
app.use('/*', serveStatic({ root: './frontend' }));
|
||||
|
||||
// Error handler
|
||||
@@ -271,10 +391,10 @@ app.onError((err, c) => {
|
||||
return c.text('Internal Server Error', 500);
|
||||
});
|
||||
|
||||
// Default to 3003; can be overridden by Environment=PORT
|
||||
const PORT = Number(process.env.PORT || 3003);
|
||||
// Default to 3004 for Redis branch; can be overridden by Environment=PORT
|
||||
const PORT = Number(process.env.PORT || 3004);
|
||||
|
||||
logLine('logs/server.log', `Starting frontend server on port ${PORT}`);
|
||||
logLine('logs/server.log', `Starting frontend server (Redis version) on port ${PORT}`);
|
||||
|
||||
// Graceful shutdown logging
|
||||
const shutdown = (signal: string) => {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.3",
|
||||
"hono": "^4.10.8",
|
||||
"ioredis": "^5.8.2",
|
||||
"pocketbase": "^0.26.5",
|
||||
"tailwind": "^4.0.0",
|
||||
},
|
||||
@@ -24,6 +25,8 @@
|
||||
"packages": {
|
||||
"@babel/runtime": ["@babel/runtime@7.3.4", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g=="],
|
||||
|
||||
"@ioredis/commands": ["@ioredis/commands@1.4.0", "", {}, "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
|
||||
|
||||
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
@@ -92,6 +95,8 @@
|
||||
|
||||
"chalk": ["chalk@2.4.1", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ=="],
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||
|
||||
"color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||
|
||||
"color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||
@@ -130,12 +135,14 @@
|
||||
|
||||
"datasette": ["datasette@1.0.1", "", { "dependencies": { "comparejs": "1.0.0", "eventemitter2": "5.0.1", "lodash": "4.17.5" } }, "sha512-aJdlCBToEJUP4M57r67r4V6tltwGKa3qetnjpBtXYIlqbX9tM9jsoDMxb4xd9AGjpp3282oHRmqI5Z8TVAU0Mg=="],
|
||||
|
||||
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||
|
||||
"depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
|
||||
|
||||
"destroy": ["destroy@1.0.4", "", {}, "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg=="],
|
||||
@@ -246,6 +253,8 @@
|
||||
|
||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||
|
||||
"ioredis": ["ioredis@5.8.2", "", { "dependencies": { "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||
@@ -310,8 +319,12 @@
|
||||
|
||||
"lodash": ["lodash@4.17.11", "", {}, "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="],
|
||||
|
||||
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
||||
|
||||
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
||||
|
||||
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
|
||||
|
||||
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
||||
|
||||
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
|
||||
@@ -344,7 +357,7 @@
|
||||
|
||||
"morgan": ["morgan@1.9.1", "", { "dependencies": { "basic-auth": "~2.0.0", "debug": "2.6.9", "depd": "~1.1.2", "on-finished": "~2.3.0", "on-headers": "~1.0.1" } }, "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA=="],
|
||||
|
||||
"ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
@@ -404,6 +417,10 @@
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||
|
||||
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
|
||||
|
||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||
|
||||
"regenerator-runtime": ["regenerator-runtime@0.12.1", "", {}, "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="],
|
||||
@@ -452,6 +469,8 @@
|
||||
|
||||
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
|
||||
|
||||
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
||||
|
||||
"statuses": ["statuses@1.4.0", "", {}, "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="],
|
||||
|
||||
"stethoskop": ["stethoskop@1.0.0", "", { "dependencies": { "node-statsd": "0.1.1" } }, "sha512-4JnZ+UmTs9SFfDjSHFlD/EoXcb1bfwntkt4h1ipNGrpxtRzmHTxOmdquCJvIrVu608Um7a09cGX0ZSOSllWJNQ=="],
|
||||
@@ -530,30 +549,52 @@
|
||||
|
||||
"babel-runtime/regenerator-runtime": ["regenerator-runtime@0.11.1", "", {}, "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="],
|
||||
|
||||
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"commands-events/@babel/runtime": ["@babel/runtime@7.2.0", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg=="],
|
||||
|
||||
"commands-events/uuidv4": ["uuidv4@2.0.0", "", { "dependencies": { "sha-1": "0.1.1", "uuid": "3.3.2" } }, "sha512-sAUlwUVepcVk6bwnaW/oi6LCwMdueako5QQzRr90ioAVVcms6p1mV0PaSxK8gyAC4CRvKddsk217uUpZUbKd2Q=="],
|
||||
|
||||
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"datasette/lodash": ["lodash@4.17.5", "", {}, "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw=="],
|
||||
|
||||
"ecdsa-sig-formatter/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"flaschenpost/@babel/runtime": ["@babel/runtime@7.2.0", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg=="],
|
||||
|
||||
"hase/@babel/runtime": ["@babel/runtime@7.1.2", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-Y3SCjmhSupzFB6wcv1KmmFucH6gDVnI30WjOcicV10ju0cZjak3Jcs67YLIXBrmZYw1xCrVeJPbycFwrqNyxpg=="],
|
||||
|
||||
"jsonwebtoken/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"morgan/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"safe-array-concat/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"safe-push-apply/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"send/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"amqplib/readable-stream/string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="],
|
||||
|
||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"morgan/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
}
|
||||
}
|
||||
|
||||
+45
-28
@@ -4,6 +4,11 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" maximum-scale="1" />
|
||||
<title>Job Info — Vanilla JS</title>
|
||||
<!-- Preload critical images for instant display -->
|
||||
<link rel="preload" href="assets/CCwhiteApp.png" as="image">
|
||||
<link rel="preload" href="assets/lightbulb.jpg" as="image">
|
||||
<link rel="preload" href="assets/PalmIsland.png" as="image">
|
||||
<link rel="preload" href="assets/DailyCheck.png" as="image">
|
||||
<script src="https://cdn.tailwindcss.com"></script> <!-- Suppress Tailwind CDN warning for now -->
|
||||
<script>
|
||||
if (window.tailwind) window.tailwind.config = { corePlugins: { preflight: true } }
|
||||
@@ -239,7 +244,8 @@
|
||||
// --- Utility ---
|
||||
const fetchNoCache = (url, options={}) => {
|
||||
const headers = { ...(options.headers||{}), ...authHeaders() };
|
||||
return fetch(url + `&_ts=${Date.now()}`, { ...options, headers });
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
return fetch(url + `${separator}_ts=${Date.now()}`, { ...options, headers });
|
||||
};
|
||||
const GRAPH_TOKEN_KEY = 'graphAccessToken';
|
||||
const GRAPH_REAUTH_FLAG = 'graphReauthAttempted';
|
||||
@@ -368,17 +374,8 @@
|
||||
// Load version from backend /version endpoint so UI matches package.json
|
||||
async function loadVersion(){
|
||||
if(!versionLabel) return;
|
||||
try{
|
||||
const res = await fetch('/version');
|
||||
if(!res.ok) {
|
||||
console.warn(`Version endpoint returned ${res.status}. Using default version.`);
|
||||
return;
|
||||
}
|
||||
const data = await res.json().catch(()=>null);
|
||||
if(data?.version) versionLabel.textContent = `v${data.version}`;
|
||||
}catch(err){
|
||||
console.debug('Version load failed (non-critical):', err.message);
|
||||
}
|
||||
// Display "Redis" for the Redis implementation branch
|
||||
versionLabel.textContent = 'Redis';
|
||||
}
|
||||
loadVersion();
|
||||
|
||||
@@ -397,26 +394,46 @@
|
||||
async function fetchAllJobs(){
|
||||
startProgress();
|
||||
clearJobsCache('Loading…');
|
||||
let page=1, totalItems=0;
|
||||
const startTime = performance.now();
|
||||
try{
|
||||
while(true){
|
||||
const url = `${PB_COLLECTION_URL}?page=${page}&perPage=${perPage}&sort=-Job_Number`;
|
||||
const res = await fetchNoCache(url);
|
||||
if(!res.ok) throw new Error(`Fetch failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
const items = data.items || [];
|
||||
jobsCache.push(...items);
|
||||
totalItems = data.totalItems || (items.length + ((page-1)*perPage));
|
||||
// Check client-side cache first (sessionStorage)
|
||||
const cachedData = sessionStorage.getItem('jobsCache');
|
||||
const cacheTime = sessionStorage.getItem('jobsCacheTime');
|
||||
const cacheAge = cacheTime ? Date.now() - parseInt(cacheTime) : Infinity;
|
||||
|
||||
// Apply filters on the currently loaded pages so initial results reflect preferences immediately.
|
||||
// Use client cache if less than 5 minutes old
|
||||
if (cachedData && cacheAge < 300000) {
|
||||
const data = JSON.parse(cachedData);
|
||||
jobsCache.push(...data);
|
||||
const loadTime = Math.round(performance.now() - startTime);
|
||||
console.log(`✓ Loaded ${data.length} jobs from CLIENT CACHE in ${loadTime}ms`);
|
||||
progressBar.style.width='95%';
|
||||
applyFiltersAndRender();
|
||||
|
||||
if(progTimer) clearInterval(progTimer);
|
||||
const realProg=Math.min(95, Math.floor((jobsCache.length / Math.max(1,totalItems)) * 95));
|
||||
progressBar.style.width=realProg+'%';
|
||||
if(jobsCache.length >= totalItems || items.length===0) break;
|
||||
page++;
|
||||
finishProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
// Use single cached endpoint that returns all jobs at once
|
||||
const url = `/api/jobs-all`;
|
||||
const res = await fetchNoCache(url);
|
||||
if(!res.ok) throw new Error(`Fetch failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
const items = data.items || [];
|
||||
jobsCache.push(...items);
|
||||
|
||||
// Store in client-side cache
|
||||
sessionStorage.setItem('jobsCache', JSON.stringify(items));
|
||||
sessionStorage.setItem('jobsCacheTime', Date.now().toString());
|
||||
|
||||
const loadTime = Math.round(performance.now() - startTime);
|
||||
console.log(`✓ Loaded ${items.length} jobs from API in ${loadTime}ms`);
|
||||
|
||||
// Show results immediately
|
||||
progressBar.style.width='95%';
|
||||
const renderStart = performance.now();
|
||||
applyFiltersAndRender();
|
||||
const renderTime = Math.round(performance.now() - renderStart);
|
||||
console.log(`✓ Rendered jobs in ${renderTime}ms (Total: ${Math.round(performance.now() - startTime)}ms)`);
|
||||
finishProgress();
|
||||
}catch(err){ finishProgress(); alert('Failed to fetch jobs: '+err.message); console.error(err);}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.3",
|
||||
"hono": "^4.10.8",
|
||||
"ioredis": "^5.8.2",
|
||||
"pocketbase": "^0.26.5",
|
||||
"tailwind": "^4.0.0"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user