diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/CHANGELOG_SESSION.md b/CHANGELOG_SESSION.md new file mode 100644 index 0000000..9eeaa46 --- /dev/null +++ b/CHANGELOG_SESSION.md @@ -0,0 +1,58 @@ +# Session Changelog - December 11-12, 2025 + +## Summary of Changes + +### 1. Fixed Decimal Number Sorting +- **Issue**: Job numbers with decimals (e.g., 3887.2) were sorting incorrectly, appearing before higher whole numbers (e.g., 4735) +- **Solution**: Replaced parseInt with parseFloat and used regex `/[0-9]+\.?[0-9]*/` to correctly extract and compare decimal job numbers +- **File**: Frontend/index.html +- **Function**: `jobNumberValue()` + +### 2. Removed Premature Initial Render +- **Issue**: Cards were flashing with incorrect order during initial page load, then updating after full load completed +- **Solution**: Removed the `initialRendered` flag and early `applyFiltersAndRender()` call during the fetch loop +- **Result**: Cards no longer render until data is properly sorted and ready + +### 3. Implemented Server-Side Sorting +- **Change**: Added `&sort=-Job_Number` parameter to PocketBase API query +- **Benefit**: Data arrives pre-sorted in descending order, eliminating client-side sort overhead +- **File**: Frontend/index.html +- **Function**: `fetchAllJobs()` + +### 4. Implemented Progressive Loading (2-Phase) +- **Phase 1**: First 40 cards display immediately after first batch loads (~50 records from perPage=50) +- **Phase 2**: Remaining cards load in background and full list renders when complete +- **Benefit**: Users see meaningful content within 1-2 seconds instead of waiting for full dataset +- **File**: Frontend/index.html +- **Functions**: + - `fetchAllJobs()` - triggers render at 40 cards + - `renderInitialBatch()` - renders first 40 cards immediately + - `applyFiltersAndRender()` - renders complete sorted list after all data loaded + +### 5. Expanded Cards Viewing Area +- **Change**: Converted layout to flexbox with `height:100vh` on body +- **Result**: Cards area now extends to bottom of screen, leaving only version footer visible +- **Improvements**: + - Better use of screen real estate + - More cards visible without scrolling + - Cleaner full-screen experience +- **Files Modified**: Frontend/index.html +- **CSS Changes**: + - `body`: Added `height:100vh`, `display:flex`, `flex-direction:column`, `overflow:hidden` + - `.wrap`: Changed to `display:flex`, `flex-direction:column`, `flex:1`, `overflow:hidden`, removed margin + - `#results`: Changed `max-height:56vh` to `flex:1` for dynamic expansion + +## Performance Improvements +- **Perceived Load Time**: ~1-2 seconds for first 40 cards (was waiting for full load) +- **Search/Filter Responsiveness**: Now uses server-side sort, reducing client-side computation +- **Memory**: Progressive loading means full dataset isn't necessarily all rendered at once + +## Files Modified +- Frontend/index.html (primary changes) +- No backend or configuration changes required + +## Testing Notes +- Decimal job numbers (e.g., 3887.2, 4735) now sort correctly in descending order +- Initial page load shows first 40 cards immediately +- Remaining pages load in background with progress bar indication +- Full list renders with all filters/search applied once complete diff --git a/README.md b/README.md index d4bc05b..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,2 +0,0 @@ -# Job-Info - diff --git a/backend/server.ts b/backend/server.ts new file mode 100644 index 0000000..91e8f6e --- /dev/null +++ b/backend/server.ts @@ -0,0 +1,58 @@ +import { Hono } from 'hono'; +import { serveStatic } from 'hono/bun'; +import fs from 'fs'; +// @ts-ignore Bun project may not have dotenv types configured +import { config } from 'dotenv'; + +config(); + +const app = new Hono(); + +// Minimal log helpers +const logLine = (path: string, line: string) => { + try { + fs.appendFileSync(path, `${new Date().toISOString()} ${line}\n`); + } catch (err) { + console.error('Failed to write log:', err); + } +}; + +// Serve static files from the frontend directory +app.use('/*', serveStatic({ root: './frontend' })); + +// Error handler +app.onError((err, c) => { + logLine('logs/error.log', `Unhandled error: ${err?.message || err}`); + return c.text('Internal Server Error', 500); +}); + +// Mutation logging endpoint +app.post('/log-change', async (c) => { + try { + const body = await c.req.json(); + const { action = 'unknown', user = 'unknown', target = '', detail = '' } = body || {}; + const payload = JSON.stringify({ action, user, target, detail }); + logLine('logs/changes.log', payload); + return c.json({ status: 'ok' }); + } catch (err) { + logLine('logs/error.log', `log-change error: ${(err as Error)?.message || String(err)}`); + return c.text('Bad Request', 400); + } +}); + +const PORT = Number(process.env.PORT || 3000); + +logLine('logs/server.log', `Starting frontend server on port ${PORT}`); + +// Graceful shutdown logging +const shutdown = (signal: string) => { + logLine('logs/server.log', `Shutting down due to ${signal}`); + process.exit(0); +}; +process.on('SIGINT', () => shutdown('SIGINT')); +process.on('SIGTERM', () => shutdown('SIGTERM')); + +export default { + port: PORT, + fetch: app.fetch, +}; diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..9bbede5 --- /dev/null +++ b/bun.lock @@ -0,0 +1,48 @@ +{ + "lockfileVersion": 1, + "configVersion": 0, + "workspaces": { + "": { + "name": "job-info", + "dependencies": { + "@microsoft/microsoft-graph-client": "^3.0.7", + "dotenv": "^17.2.3", + "hono": "^4.10.8", + "pocketbase": "^0.26.5", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + + "@microsoft/microsoft-graph-client": ["@microsoft/microsoft-graph-client@3.0.7", "", { "dependencies": { "@babel/runtime": "^7.12.5", "tslib": "^2.2.0" } }, "sha512-/AazAV/F+HK4LIywF9C+NYHcJo038zEnWkteilcxC1FM/uK/4NVGDKGrxx7nNq1ybspAroRKT4I1FHfxQzxkUw=="], + + "@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=="], + + "@types/react": ["@types/react@19.2.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A=="], + + "bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="], + + "csstype": ["csstype@3.2.0", "", {}, "sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg=="], + + "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], + + "hono": ["hono@4.10.8", "", {}, "sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww=="], + + "pocketbase": ["pocketbase@0.26.5", "", {}, "sha512-SXcq+sRvVpNxfLxPB1C+8eRatL7ZY4o3EVl/0OdE3MeR9fhPyZt0nmmxLqYmkLvXCN9qp3lXWV/0EUYb3MmMXQ=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + } +} diff --git a/frontend/auth.js b/frontend/auth.js new file mode 100644 index 0000000..145f755 --- /dev/null +++ b/frontend/auth.js @@ -0,0 +1,33 @@ +// PocketBase auth bootstrap shared by pages +(function (global) { + const pb = new PocketBase('https://pocketbase.ccllc.pro'); + + function authHeaders() { + if (pb.authStore.isValid && pb.authStore.token) { + return { Authorization: `Bearer ${pb.authStore.token}` }; + } + return {}; + } + + function getDisplayName() { + const model = pb.authStore.model || {}; + return ( + model.display_name || + model.name || + model.email || + model.username || + 'Unknown' + ); + } + + async function ensureAuth() { + if (pb.authStore.isValid) return true; + const path = new URL(window.location.href).pathname; + if (!path.endsWith('signin.html')) { + window.location.href = 'signin.html'; + } + return false; + } + + global.Auth = { pb, authHeaders, ensureAuth, getDisplayName }; +})(window); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..2be05b3 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,459 @@ + + + + + +Job Info — Vanilla JS + + + + + +
+

Job Info

+ +
+
+
+ + + + + +
+
+ Division
+ + + +
+ +
+ Active
+ + +
+ +
+ Estimator
+ + +
+ +
+ Status
+ + + + + + + +
+ +
+ +
+ +
+ + + + + + + diff --git a/frontend/signin.html b/frontend/signin.html new file mode 100644 index 0000000..6491a90 --- /dev/null +++ b/frontend/signin.html @@ -0,0 +1,216 @@ + + + + + + Sign In + + + + + +
+

Welcome

+

Sign in with your Microsoft account

+ + + +
+ +
+

Signed in

+

Name:

+

Email:

+ + + +
+
+ + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..1698922 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "job-info-pb", + "type": "module", + "private": true, + "scripts": { + "dev": "bun run backend/server.ts", + "start": "bun run backend/server.ts" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "dotenv": "^17.2.3", + "hono": "^4.10.8", + "pocketbase": "^0.26.5" + } +} diff --git a/supervisor.sh b/supervisor.sh new file mode 100644 index 0000000..b9af7ad --- /dev/null +++ b/supervisor.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +trap "echo 'Server stopped by user'; exit 0" SIGINT + +MAX_BACKOFF=60 +FAIL_COUNT=0 +PID_FILE="bun.pid" +PORT=${PORT:-3000} +ENTRYPOINT="backend/server.ts" + +mkdir -p logs + +while true; do + # Check if previous server PID exists + if [ -f "$PID_FILE" ]; then + PREV_PID=$(cat "$PID_FILE") + if ps -p $PREV_PID > /dev/null 2>&1; then + echo "Previous server (PID $PREV_PID) still running. Waiting 2 seconds..." + sleep 2 + continue + else + # Clean up stale PID file + rm "$PID_FILE" + fi + fi + + echo "Starting Bun server on port $PORT..." | tee -a logs/server.log + + # Start Bun in background and store PID + bun run $ENTRYPOINT 2>&1 | tee -a logs/server.log & + SERVER_PID=$! + echo $SERVER_PID > "$PID_FILE" + + # Wait for server to exit + wait $SERVER_PID + EXIT_CODE=$? + + echo "Server exited with code $EXIT_CODE" | tee -a logs/server.log + + # Remove PID file + rm -f "$PID_FILE" + + # Backoff logic + FAIL_COUNT=$((FAIL_COUNT + 1)) + BACKOFF=$((2 ** FAIL_COUNT)) + [ $BACKOFF -gt $MAX_BACKOFF ] && BACKOFF=$MAX_BACKOFF + JITTER=$((RANDOM % 4)) + SLEEP_TIME=$((BACKOFF + JITTER)) + + echo "Restarting in $SLEEP_TIME seconds (failures: $FAIL_COUNT)..." | tee -a logs/server.log + sleep $SLEEP_TIME + + if [ $FAIL_COUNT -ge 10 ]; then + echo "High number of failures — cooling down for 5 minutes..." | tee -a logs/server.log + sleep 300 + FAIL_COUNT=0 + fi + +done