From ab2403e914b35bb9912b7cd96f1c81e66aee70d9 Mon Sep 17 00:00:00 2001 From: Abhinav Sinha Date: Mon, 15 Jun 2026 15:28:53 +0530 Subject: [PATCH] feat: implement scenario caching and hot-reloading Signed-off-by: Abhinav Sinha --- README.md | 21 ++++++++- backend/server.js | 31 +++++++++++++- frontend/src/components/Header.jsx | 39 +++++++++++++++++ frontend/src/components/Header.module.css | 52 +++++++++++++++++++++++ 4 files changed, 140 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e1cf677..1db0471 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,22 @@ Each scenario is a single JSON file in `scenarios/data` directory; each bundle i - `correct_option` must match one of the `options[].id` values - Always include an `explanation` +### In-Memory Cache & Hot Reloading + +To ensure high performance and zero disk-I/O bottlenecking, scenarios and bundles are cached in memory on backend startup. When developing or updating scenarios, you can hot-reload the definitions without rebuilding the image or restarting the container: + +1. **Mount Scenarios Directory:** Run the container with the local `scenarios/` directory mounted to `/app/scenarios`: + ```bash + docker run --rm -itd --privileged -p 7554:80 --name kubekosh -v :/app/scenarios zeborg/kubekosh:latest + ``` +2. **Reload Cache:** Click the **Reload Scenario Cache** (↻) button in the top right corner of the header in the web user interface, or send an API request: + ```bash + curl -X POST http://localhost:7554/api/cache/reload + ``` + +> **NOTE:** +> The content in `` should be the path to the local `scenarios/` directory of the cloned repository with your updates, i.e., it should contain the updated `scenarios/data` and `scenarios/bundles` directories. + ### Workflow ```bash @@ -168,7 +184,10 @@ vim scenarios/data/my-new-scenario.json # edit the new scenario as per [SCHEMA.m vim scenarios/bundles/k8s-basics.json # edit the bundle to include the new scenario ID # 5. Build and test locally -docker build -t kubekosh . && docker run --rm -itd --privileged -p 7554:80 kubekosh +# Run the built container directly: +docker build -t kubekosh . && docker run --rm -itd --privileged -p 7554:80 --name kubekosh kubekosh +# Or mount the scenarios folder for hot-reloading: +docker run --rm -itd --privileged -p 7554:80 -v $PWD/scenarios:/app/scenarios --name kubekosh zeborg/kubekosh:dev # 6. Commit and push to your fork (example for adding `my-new-scenario` to `k8s-basics` bundle) git add scenarios/data/my-new-scenario.json scenarios/bundles/k8s-basics.json diff --git a/backend/server.js b/backend/server.js index 4c3669b..a669682 100644 --- a/backend/server.js +++ b/backend/server.js @@ -90,12 +90,28 @@ function loadJsonDir(dir) { .map(f => JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'))); } +let scenariosCache = []; +let bundlesCache = []; + +function reloadCache() { + try { + scenariosCache = loadJsonDir(SCENARIOS_DIR); + bundlesCache = loadJsonDir(BUNDLES_DIR); + console.log(`Loaded ${scenariosCache.length} scenarios and ${bundlesCache.length} bundles into cache.`); + } catch (e) { + console.error('Failed to reload cache:', e.message); + } +} + +// Initial cache populate +reloadCache(); + function loadScenarios() { - return loadJsonDir(SCENARIOS_DIR); + return scenariosCache; } function loadBundles() { - return loadJsonDir(BUNDLES_DIR); + return bundlesCache; } async function runCommand(cmd, timeoutMs = 15000) { @@ -466,6 +482,17 @@ app.post('/api/progress/reset/:id', (req, res) => { res.json({ ok: true }); }); +// POST /api/cache/reload — reload scenarios and bundles cache +app.post('/api/cache/reload', (req, res) => { + reloadCache(); + res.json({ + ok: true, + message: 'Cache reloaded successfully', + scenarios_count: loadScenarios().length, + bundles_count: loadBundles().length + }); +}); + // GET /api/health app.get('/api/health', async (req, res) => { const kube = await runCommand('kubectl cluster-info --request-timeout=3s 2>&1 | head -1'); diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index 6f489e8..be0b844 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -13,6 +13,31 @@ export default function Header({ clusterReady }) { const toggleTheme = () => setTheme(t => t === 'dark' ? 'light' : 'dark') + const handleReloadCache = async () => { + const sendReloadRequest = async () => { + return fetch('/api/cache/reload', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + }; + + try { + let response = await sendReloadRequest(); + if (response.ok) { + const data = await response.json(); + alert(`Success: ${data.message}\nScenarios: ${data.scenarios_count}\nBundles: ${data.bundles_count}`); + window.location.reload(); + } else { + const data = await response.json().catch(() => ({})); + alert(`Error reloading cache: ${data.error || 'Unknown error'}`); + } + } catch (err) { + alert(`Network error reloading cache: ${err.message}`); + } + }; + return (
@@ -54,6 +79,20 @@ export default function Header({ clusterReady }) { {clusterReady ? 'Cluster Ready' : 'Connecting…'}
+ + {/* Reload cache */} +
+ +
) diff --git a/frontend/src/components/Header.module.css b/frontend/src/components/Header.module.css index 0729c9a..c0d8348 100644 --- a/frontend/src/components/Header.module.css +++ b/frontend/src/components/Header.module.css @@ -107,6 +107,58 @@ transform: rotate(12deg); } +.reloadBtnContainer { + position: relative; + display: flex; + align-items: center; +} + +.reloadBtnContainer::after { + content: "Reload Scenario Cache"; + position: absolute; + top: calc(100% + 8px); + right: 0; + background: var(--surface3); + color: var(--text); + font-family: var(--mono); + font-size: 11px; + padding: 4px 8px; + border-radius: 4px; + border: 1px solid var(--border2); + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 0.1s ease; + z-index: 1000; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35); +} + +.reloadBtnContainer:hover::after { + opacity: 1; +} + +.reloadBtn { + background: none; + border: 1px solid var(--border); + border-radius: 8px; + width: 34px; + height: 34px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + cursor: pointer; + transition: background 0.15s, border-color 0.15s, transform 0.2s, color 0.15s; + color: var(--text-2); + line-height: 1; +} +.reloadBtn:hover { + background: var(--surface2); + border-color: var(--border2); + color: var(--text); + transform: rotate(12deg); +} + .clusterBadge { display: flex; align-items: center;