Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions INSTALL_THE_APP/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ This is one of three ways to use the CSF Profile Assessment Database:
| `GET_THE_SPREADSHEETS/` | You just want CSV/Excel artifacts to work in spreadsheets. |
| `GET_THE_NOTION_TEMPLATE/` | You prefer to assess inside Notion. Quick-start bundle and import guide. |

## Install from your browser (no toolchain)

The web app is a Progressive Web App: open a **production copy** in Chrome or Edge — a hosted deployment, or a local build served with `npm run build` then `npx serve -s build` — and click the **install icon** in the address bar (or menu → *Install CSF Profile Assessment*). You get a standalone app window, a launcher/dock icon, and offline support: after the first visit the app opens and runs with no connection. (The `npm start` dev server deliberately skips the offline worker so hot reload never fights a cache.) Your data already lives entirely in your browser's local storage, so nothing about installing changes where data goes.

This is the fastest path on machines where you can't install Node or Rust — nothing to build, nothing to run as admin.

## Quick start

See the main [README — Installation and Setup](../README.md#installation-and-setup) for the full walkthrough. Short version:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"build": "react-scripts build && node scripts/stamp-service-worker.mjs",
"test": "react-scripts test",
"eject": "react-scripts eject",
"tauri": "tauri",
Expand Down
Binary file added public/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
name="description"
content="Free open-source NIST CSF 2.0 maturity assessment tool — score all 106 subcategories, track quarterly progress, visualize gaps on a dashboard radar, and export audit-ready CSV workpapers."
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/SC_Logo.png" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
Expand Down
Binary file modified public/logo192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/logo512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 19 additions & 13 deletions public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
{
"short_name": "CSF Profile Assessment",
"id": "./",
"name": "CSF Profile Assessment",
"short_name": "CSF Profile",
"description": "Free open-source NIST CSF 2.0 maturity assessment tool — score all 106 subcategories, track quarterly progress, visualize gaps on a dashboard radar, and export audit-ready CSV workpapers.",
"start_url": ".",
"scope": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#000000",
"icons": [
{
"src": "SC_Logo.png",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/png"
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192",
"purpose": "any"
},
{
"src": "SC_Logo.png",
"src": "logo512.png",
"type": "image/png",
"sizes": "192x192"
"sizes": "512x512",
"purpose": "any"
},
{
"src": "SC_Logo.png",
"src": "maskable-512.png",
"type": "image/png",
"sizes": "512x512"
"sizes": "512x512",
"purpose": "maskable"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
]
}
Binary file added public/maskable-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
167 changes: 167 additions & 0 deletions public/service-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/* eslint-disable no-restricted-globals */
/*
* Offline-first service worker for the CSF Profile Assessment PWA.
*
* Strategy, by request class (same-origin GET only — everything else,
* including POSTs and /api/, passes through untouched):
* - Navigations: network-first with the cached app shell as offline
* fallback, so a deploy is picked up on the next online visit and the
* app still opens with no connection.
* - /static/ build output: cache-first. These files are content-hashed,
* so a cached copy is never stale.
* - Everything else (the CSV datasets, icons, manifest): stale-while-
* revalidate. Served from cache for speed and offline, refreshed in
* the background so installed users keep receiving dataset updates.
*
* The build script appends a fingerprint comment to this file so its
* bytes change whenever the bundle changes; the browser's byte-diff
* update check then re-installs the worker and re-runs the precache.
*/

const CACHE_NAME = 'csf-profile-v1';
const APP_SHELL = [
'.',
'index.html',
'manifest.json',
'logo192.png',
'logo512.png',
'tblProfile_Demo.csv',
'Confluence-Requirements.csv',
'scoring_legend.csv'
];

/*
* Chrome refuses to serve a redirected response for a navigation, and
* static hosts commonly redirect /index.html -> /. Re-wrap before caching
* so the offline shell is always servable.
*/
async function withoutRedirect(response) {
if (!response.redirected) return response;
const body = await response.blob();
return new Response(body, {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
}

function isHtml(response) {
return (response.headers.get('content-type') || '').includes('text/html');
}

self.addEventListener('install', (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE_NAME);
await Promise.all(
APP_SHELL.map(async (url) => {
const response = await fetch(url);
if (!response.ok) throw new Error(`precache failed: ${url} ${response.status}`);
await cache.put(url, await withoutRedirect(response));
})
);
// Precache the content-hashed build assets listed in the CRA
// asset manifest; tolerate its absence (e.g. `npm start` dev server).
try {
const response = await fetch('asset-manifest.json');
if (response.ok) {
const manifest = await response.json();
const files = Object.values(manifest.files || {}).filter((path) =>
/\.(js|css)$/.test(path)
);
await cache.addAll(files);
}
} catch (err) {
// Offline or missing manifest — runtime caching will fill the gap.
}
await self.skipWaiting();
})()
);
});

self.addEventListener('activate', (event) => {
event.waitUntil(
(async () => {
const names = await caches.keys();
await Promise.all(
names.filter((name) => name !== CACHE_NAME).map((name) => caches.delete(name))
);
await self.clients.claim();
})()
);
});

self.addEventListener('fetch', (event) => {
const { request } = event;
if (request.method !== 'GET') return;

const url = new URL(request.url);
if (url.origin !== self.location.origin) return;
// Live server state (AI backend status) must never be cached or intercepted.
if (url.pathname.startsWith('/api/')) return;

if (request.mode === 'navigate') {
event.respondWith(
(async () => {
try {
const response = await fetch(request);
if (response.ok) {
const copy = response.clone();
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE_NAME);
await cache.put('index.html', await withoutRedirect(copy));
})().catch(() => {})
);
}
return response;
} catch (err) {
const cached = await caches.match('index.html');
if (cached) return cached;
throw err;
}
})()
);
return;
}

if (url.pathname.includes('/static/')) {
event.respondWith(
(async () => {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
if (response.ok && response.type === 'basic') {
const copy = response.clone();
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.put(request, copy)).catch(() => {})
);
}
return response;
})()
);
return;
}

event.respondWith(
(async () => {
const cached = await caches.match(request);
const refresh = (async () => {
const response = await fetch(request);
// An SPA-fallback HTML page for a non-navigation URL is junk
// (e.g. a 200-with-index.html for a missing path) — never cache it.
if (response.ok && response.type === 'basic' && !isHtml(response)) {
const copy = response.clone();
const cache = await caches.open(CACHE_NAME);
await cache.put(request, await withoutRedirect(copy));
}
return response;
})();
if (cached) {
event.waitUntil(refresh.catch(() => {}));
return cached;
}
return refresh;
})()
);
});
14 changes: 14 additions & 0 deletions scripts/stamp-service-worker.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Appends a build fingerprint to build/service-worker.js so its bytes change
// whenever the bundle changes. The browser updates a service worker only when
// the file's bytes differ, so without this stamp a deploy would never re-run
// the precache step. Idempotent: re-running replaces the previous stamp.
import { createHash } from 'node:crypto';
import { readFileSync, writeFileSync } from 'node:fs';

const manifest = readFileSync('build/asset-manifest.json');
const stamp = createHash('sha256').update(manifest).digest('hex').slice(0, 16);

const swPath = 'build/service-worker.js';
const source = readFileSync(swPath, 'utf8').replace(/\n\/\/ build [0-9a-f]+\n$/, '\n');
writeFileSync(swPath, `${source}// build ${stamp}\n`);
console.log(`service-worker.js stamped with build ${stamp}`);
4 changes: 4 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { register as registerServiceWorker } from './serviceWorkerRegistration';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
Expand All @@ -15,3 +16,6 @@ root.render(
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

// Offline support + browser installability (production builds only).
registerServiceWorker();
Loading
Loading