From 8bb76d8e6cafdda1ddbeaee729863dd1881af9c8 Mon Sep 17 00:00:00 2001 From: kevinBell Date: Wed, 22 Jul 2026 16:26:12 -0600 Subject: [PATCH] fixed issues w/ accessing website on other computers --- README.md | 37 ++++++++--- dashboard/README.md | 51 ++++++++------- dashboard/js/data.js | 101 +++++++++++++++++++++++++++++- dashboard/package.json | 1 + dashboard/server.mjs | 48 ++++++++++++-- dashboard/tests/contract.test.mjs | 26 ++++++++ docs/architecture.mmd | 4 +- docs/bolt_deployment.md | 5 ++ docs/dashboard_spec.md | 16 +++-- docs/demo_script.md | 20 +++--- docs/final_report.md | 12 ++-- docs/presentation_outline.md | 11 ++-- docs/project_charter.md | 4 +- 13 files changed, 267 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index b37753a..a7c5db3 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Start with the [final report](docs/final_report.md). Supporting deliverables: - [Private-to-public architecture](docs/architecture.mmd) - [10-minute presentation outline](docs/presentation_outline.md) - [Dashboard demo script](docs/demo_script.md) -- [Dashboard-only Bolt deployment](docs/bolt_deployment.md) +- [Optional public-hosting notes](docs/bolt_deployment.md) The [data inventory](docs/data_inventory.md) and archived [project options](docs/project_options.md) provide source-discovery history; @@ -142,17 +142,34 @@ checksums. It shows no estimates if the approved aggregate bundle fails its contract. Every checked-in asset is marked as a development preview and not a population estimate. -## Publish through Bolt +### Live demo from another computer -Do **not** import this full source repository into Bolt. Create a separate -deployment project or repository containing only the contents of `dashboard/`, -so `index.html` is at that project's root. Do not copy `.env`, private data, -artifacts, models, SQL, pipeline scripts, credentials, or unrelated repository -history. +When both computers are on the same network, start the LAN demo from the +repository root. Stop any older dashboard process with `Ctrl-C` first: -Follow the complete preflight, import, Bolt Hosting, and post-publication checks -in [docs/bolt_deployment.md](docs/bolt_deployment.md). No publishing action is -performed by this repository. +```bash +node dashboard/server.mjs --lan +``` + +The server prints a **This computer** URL and one or more interface-labeled +**Other devices** URLs. Open the URL for the shared Wi-Fi or Ethernet interface +on the second computer; do not use `0.0.0.0` as the browser address. Confirm the +header reads **Validated sample aggregates**. Stop the server with `Ctrl-C` +after the demo. + +The LAN page still checks every approved JSON asset against its SHA-256 digest. +Browsers do not expose the SubtleCrypto digest API to a non-loopback plain-HTTP +page, so the dashboard includes a dependency-free checksum implementation for +that case. These checks validate bundle consistency; plain HTTP does not +authenticate the network transport, so use a trusted demo network. + +## Public hosting is not required + +The current presentation plan is the same-network live demo above. Nothing +needs to be uploaded or published, and the server exposes only the dashboard's +explicit static-file allowlist. The older +[public-hosting checklist](docs/bolt_deployment.md) is retained only in case the +delivery requirements change later. ## Author diff --git a/dashboard/README.md b/dashboard/README.md index 62ee7f7..c2460d0 100644 --- a/dashboard/README.md +++ b/dashboard/README.md @@ -25,6 +25,28 @@ URL; browsers will block the JSON module requests. To use another port: PORT=8080 node dashboard/server.mjs ``` +## Run a live demo on the same network + +From the source-repository root, run: + +```bash +node dashboard/server.mjs --lan +``` + +Stop any older dashboard process with `Ctrl-C` before starting. Or, from this +directory, run `npm run start:lan`. The server prints a **This computer** URL +and one or more interface-labeled **Other devices** URLs. Open the URL for the +shared Wi-Fi or Ethernet interface on the second computer; `0.0.0.0` is a bind +address, not the address to put in a browser. Confirm the in-app status reads +**Validated sample aggregates**, then stop the server with `Ctrl-C` after the +demo. + +All data-contract and checksum validation remains enabled over LAN HTTP. When a +remote browser does not expose the SubtleCrypto digest API to the plain-HTTP +page, the dashboard uses its dependency-free SHA-256 implementation and still +rejects changed assets. The checks validate bundle consistency, not the +authenticity of a plain-HTTP connection, so use a trusted demo network. + Run the dependency-free contract checks with: ```bash @@ -68,28 +90,9 @@ operational connection, or row-level prediction output. VINs, plates, ZIPs, stations, technician identifiers, raw source JSON, credentials, and operational records must never enter this directory. -## Publish through Bolt +## Public hosting is not required -Use a separate static Bolt project backed by a dashboard-only repository. Do -not import this source repository into Bolt. - -1. Run `npm test` from this directory and confirm every contract check passes. -2. Create a clean dashboard-only repository or Bolt project. -3. Copy only the *contents* of `dashboard/` into that project, so `index.html` - is at the project root. -4. Confirm that private `data/`, `artifacts/`, `models/`, `.env` files, SQL, - notebooks, and database tooling are absent. -5. Preview the project. There is no install or build step; if Bolt requests a - preview command, use `node server.mjs` with `HOST=0.0.0.0` and let Bolt - provide `PORT`. -6. Verify all four views, the development-sample messaging on every view, and - that the in-app status reads **Validated sample aggregates**. -7. Use **Publish** only after the aggregate bundle completes privacy review. - Bolt hosting is the default; Netlify can instead be selected before the - first publish if desired. - -See Bolt's official documentation for [Git -integration](https://support.bolt.new/integrations/git) and [publishing with -Netlify](https://support.bolt.new/integrations/netlify). - -No publishing action is performed by this repository. +The current delivery path is the same-network live demo. No Bolt project, +public URL, dependency install, or build step is needed. If public hosting is +ever reconsidered, publish only a separately reviewed copy of this dashboard +directory—never the private source repository. diff --git a/dashboard/js/data.js b/dashboard/js/data.js index c51ad13..5b6fcae 100644 --- a/dashboard/js/data.js +++ b/dashboard/js/data.js @@ -10,6 +10,34 @@ const ENVELOPE_KEYS = Object.freeze([ "population_estimate_allowed", "schema_version", ]); +const SHA256_INITIAL_STATE = Object.freeze([ + 0x6a09e667, + 0xbb67ae85, + 0x3c6ef372, + 0xa54ff53a, + 0x510e527f, + 0x9b05688c, + 0x1f83d9ab, + 0x5be0cd19, +]); +const SHA256_ROUND_CONSTANTS = Object.freeze([ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, +]); export const REQUIRED_ASSETS = Object.freeze({ manifest: "data_manifest.json", @@ -541,9 +569,80 @@ function parseJsonBytes(bytes, label) { } } +function rotateRight(value, shift) { + return ((value >>> shift) | (value << (32 - shift))) >>> 0; +} + +function sha256HexFallback(bytes) { + const input = new Uint8Array(bytes); + const bitLength = input.byteLength * 8; + if (!Number.isSafeInteger(bitLength)) { + throw new DataContractError("Dashboard asset integrity verification failed."); + } + + const paddedLength = Math.ceil((input.byteLength + 9) / 64) * 64; + const padded = new Uint8Array(paddedLength); + padded.set(input); + padded[input.byteLength] = 0x80; + + const paddedView = new DataView(padded.buffer); + paddedView.setUint32(paddedLength - 8, Math.floor(bitLength / 0x100000000), false); + paddedView.setUint32(paddedLength - 4, bitLength >>> 0, false); + + const state = [...SHA256_INITIAL_STATE]; + const words = new Uint32Array(64); + for (let offset = 0; offset < paddedLength; offset += 64) { + for (let index = 0; index < 16; index += 1) { + words[index] = paddedView.getUint32(offset + index * 4, false); + } + for (let index = 16; index < 64; index += 1) { + const sigma0 = + rotateRight(words[index - 15], 7) ^ + rotateRight(words[index - 15], 18) ^ + (words[index - 15] >>> 3); + const sigma1 = + rotateRight(words[index - 2], 17) ^ + rotateRight(words[index - 2], 19) ^ + (words[index - 2] >>> 10); + words[index] = + (words[index - 16] + sigma0 + words[index - 7] + sigma1) >>> 0; + } + + let [a, b, c, d, e, f, g, h] = state; + for (let index = 0; index < 64; index += 1) { + const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25); + const choose = (e & f) ^ (~e & g); + const temporary1 = + (h + sum1 + choose + SHA256_ROUND_CONSTANTS[index] + words[index]) >>> 0; + const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22); + const majority = (a & b) ^ (a & c) ^ (b & c); + const temporary2 = (sum0 + majority) >>> 0; + h = g; + g = f; + f = e; + e = (d + temporary1) >>> 0; + d = c; + c = b; + b = a; + a = (temporary1 + temporary2) >>> 0; + } + + state[0] = (state[0] + a) >>> 0; + state[1] = (state[1] + b) >>> 0; + state[2] = (state[2] + c) >>> 0; + state[3] = (state[3] + d) >>> 0; + state[4] = (state[4] + e) >>> 0; + state[5] = (state[5] + f) >>> 0; + state[6] = (state[6] + g) >>> 0; + state[7] = (state[7] + h) >>> 0; + } + + return state.map((word) => word.toString(16).padStart(8, "0")).join(""); +} + async function sha256Hex(bytes, cryptoImplementation) { if (!cryptoImplementation?.subtle || typeof cryptoImplementation.subtle.digest !== "function") { - throw new DataContractError("This browser cannot verify dashboard asset integrity."); + return sha256HexFallback(bytes); } let digest; try { diff --git a/dashboard/package.json b/dashboard/package.json index 6160fc8..fbe8cf0 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -6,6 +6,7 @@ "description": "Dependency-free static dashboard for approved Utah Vehicle Health aggregates.", "scripts": { "start": "node server.mjs", + "start:lan": "node server.mjs --lan", "test": "node --test tests/contract.test.mjs" }, "engines": { diff --git a/dashboard/server.mjs b/dashboard/server.mjs index 4cadc66..f4b23d7 100644 --- a/dashboard/server.mjs +++ b/dashboard/server.mjs @@ -1,10 +1,12 @@ import { createReadStream, realpathSync, statSync } from "node:fs"; import { createServer } from "node:http"; +import { networkInterfaces } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; const ROOT = realpathSync(path.dirname(fileURLToPath(import.meta.url))); -const HOST = process.env.HOST || "127.0.0.1"; +const LAN_MODE = process.argv.includes("--lan"); +const HOST = LAN_MODE ? "0.0.0.0" : process.env.HOST || "127.0.0.1"; const PORT = Number.parseInt(process.env.PORT || "4173", 10); const TYPES = new Map([ [".css", "text/css; charset=utf-8"], @@ -50,6 +52,28 @@ function safeFile(requestUrl) { } } +function displayUrls(host, port) { + if (host !== "0.0.0.0") { + const displayHost = host === "::" ? "::1" : host; + const urlHost = displayHost.includes(":") ? `[${displayHost}]` : displayHost; + return [{ label: "Open", url: `http://${urlHost}:${port}` }]; + } + + const entries = [{ label: "This computer", url: `http://127.0.0.1:${port}` }]; + const seen = new Set(); + for (const [interfaceName, addresses] of Object.entries(networkInterfaces())) { + for (const address of addresses || []) { + if (address.family !== "IPv4" || address.internal || seen.has(address.address)) continue; + seen.add(address.address); + entries.push({ + label: `Other devices (${interfaceName})`, + url: `http://${address.address}:${port}`, + }); + } + } + return entries; +} + const server = createServer((request, response) => { if (!request.url || !["GET", "HEAD"].includes(request.method || "")) { response.writeHead(405, { Allow: "GET, HEAD" }); @@ -66,7 +90,7 @@ const server = createServer((request, response) => { const extension = path.extname(file).toLowerCase(); response.writeHead(200, { "Content-Type": TYPES.get(extension) || "application/octet-stream", - "Cache-Control": extension === ".json" ? "no-store" : "public, max-age=300", + "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", "Referrer-Policy": "no-referrer", }); @@ -77,6 +101,22 @@ const server = createServer((request, response) => { createReadStream(file).on("error", () => response.destroy()).pipe(response); }); -server.listen(PORT, HOST, () => { - process.stdout.write(`Utah Vehicle Health dashboard: http://${HOST}:${PORT}\n`); +server.on("error", (error) => { + if (error.code === "EADDRINUSE") { + process.stderr.write( + `Dashboard port ${PORT} is already in use. Stop the existing server or set a different PORT.\n`, + ); + } else { + process.stderr.write(`Dashboard server could not start (${error.code || "unknown error"}).\n`); + } + process.exitCode = 1; +}); + +server.listen(PORT, HOST, () => { + const address = server.address(); + const actualPort = typeof address === "object" && address ? address.port : PORT; + const urls = displayUrls(HOST, actualPort) + .map(({ label, url }) => ` ${label}: ${url}`) + .join("\n"); + process.stdout.write(`Utah Vehicle Health dashboard:\n${urls}\n`); }); diff --git a/dashboard/tests/contract.test.mjs b/dashboard/tests/contract.test.mjs index 5088be5..8d8c763 100644 --- a/dashboard/tests/contract.test.mjs +++ b/dashboard/tests/contract.test.mjs @@ -214,6 +214,32 @@ test("browser loader verifies every raw asset digest before rendering", async () ); }); +test("LAN HTTP loader verifies asset digests without Web Crypto", async () => { + const validated = await loadDashboardData({ + basePath: "http://dashboard.test/public/data/", + fetchImplementation: publicDataFetch(), + cryptoImplementation: null, + }); + assert.equal(validated.manifest.schema_version, SCHEMA_VERSION); + + const changedOverview = Buffer.concat([ + readFileSync(path.join(PUBLIC_DATA, REQUIRED_ASSETS.overview)), + Buffer.from("\n"), + ]); + await assert.rejects( + loadDashboardData({ + basePath: "http://dashboard.test/public/data/", + fetchImplementation: publicDataFetch( + new Map([[REQUIRED_ASSETS.overview, changedOverview]]), + ), + cryptoImplementation: null, + }), + (error) => + error instanceof DataContractError && + /Integrity verification failed for overview_period_county\.json/.test(error.message), + ); +}); + test("sha256 manifest covers and matches every approved data asset", () => { const manifest = json(REQUIRED_ASSETS.shaManifest); const expectedNames = Object.entries(REQUIRED_ASSETS) diff --git a/docs/architecture.mmd b/docs/architecture.mmd index 2dbc3a2..594b510 100644 --- a/docs/architecture.mmd +++ b/docs/architecture.mmd @@ -63,7 +63,7 @@ flowchart TD end EXPORT -.-> GUARDRAILS - SITE --> BOLT[Separate dashboard-only
Bolt project] + SITE --> DEMO[Allowlisted local static server
same-network live demo] subgraph TESTS[Verification] direction LR @@ -80,5 +80,5 @@ flowchart TD class SOURCE,SAMPLE,STAGE,EPISODES,MART,PRIVATE,REPORT private class LOGISTIC,TREE model - class PUBLIC,CONTRACT,SITE,VIEWS,BOLT public + class PUBLIC,CONTRACT,SITE,VIEWS,DEMO public class GUARDRAILS warning diff --git a/docs/bolt_deployment.md b/docs/bolt_deployment.md index 0026502..21a320c 100644 --- a/docs/bolt_deployment.md +++ b/docs/bolt_deployment.md @@ -2,6 +2,11 @@ Last reviewed: 2026-07-21 +> **Not part of the current delivery plan.** The presentation now uses the +> same-network live-demo procedure in [demo_script.md](demo_script.md). This +> checklist is retained only as a boundary reference if public hosting is +> reconsidered later. + ## Non-negotiable publication boundary Publish a **separate Bolt project or GitHub repository containing only the diff --git a/docs/dashboard_spec.md b/docs/dashboard_spec.md index 069494c..ca0f600 100644 --- a/docs/dashboard_spec.md +++ b/docs/dashboard_spec.md @@ -121,13 +121,11 @@ developer console. - Preserve the warning banner and fail-closed unavailable state on desktop and mobile layouts. -## Deployment boundary +## Demo boundary -Bolt receives a separate dashboard-only project whose root is the contents of -`dashboard/`. Do not import or upload the full private-pipeline repository and -do not rely on a configurable subdirectory working root. The deployable project -contains no `.env`, private `data/`, model artifacts, SQL, pipeline scripts, or -repository history outside the dashboard directory. - -See [bolt_deployment.md](bolt_deployment.md) for the reviewed handoff and -post-publication checks. +The current delivery is a same-network live demo started with +`node dashboard/server.mjs --lan`. The local server resolves files only beneath +`dashboard/` and serves only its explicit shell, JavaScript, stylesheet, and +approved aggregate allowlist. It does not expose `.env`, private `data/`, model +artifacts, SQL, pipeline scripts, or repository history. Stop the server after +the demo; no public hosting step is required. diff --git a/docs/demo_script.md b/docs/demo_script.md index 02b3e06..55f636c 100644 --- a/docs/demo_script.md +++ b/docs/demo_script.md @@ -5,16 +5,22 @@ Target length: **2 minutes 45 seconds**, embedded in the ## Before the audience arrives -1. From the repository root, run `node dashboard/server.mjs`. -2. Open `http://127.0.0.1:4173/#overview`. -3. Confirm the header says the sample aggregates validated. -4. Confirm the private-sample development-preview banner is visible. -5. Visit all four routes: Overview, Sample cohorts, Model & benchmark, and Data +1. Put the presenting computer and demo computer on the same trusted network. +2. Stop any older dashboard process, then from the repository root run + `node dashboard/server.mjs --lan`. +3. On the demo computer, open the printed **Other devices** URL for the shared + Wi-Fi or Ethernet interface with `/#overview` appended. On the presenting + computer, use the printed **This computer** URL. +4. Confirm the header says **Validated sample aggregates**. If an older copy of + the JavaScript was previously loaded, hard-refresh the page once. +5. Confirm the private-sample development-preview banner is visible. +6. Visit all four routes: Overview, Sample cohorts, Model & benchmark, and Data & methods. -6. Reset Sample cohorts and leave its sort on largest support. +7. Reset Sample cohorts and leave its sort on largest support. Do not open developer tools, private files, model artifacts, database clients, -or environment variables during the presentation. +or environment variables during the presentation. Stop the server with +`Ctrl-C` when the demo is over. ## Live talk track diff --git a/docs/final_report.md b/docs/final_report.md index ccd1359..cbffaa5 100644 --- a/docs/final_report.md +++ b/docs/final_report.md @@ -172,7 +172,8 @@ The public boundary is intentionally narrow: private read-only source -> private local extraction and model pipeline -> suppression-reviewed aggregate JSON - -> dashboard-only static Bolt project + -> allowlisted local static server + -> same-network demo browser ``` The public dashboard and presentation materials contain no VIN, plate, ZIP, @@ -180,10 +181,9 @@ station, technician identifier, private vehicle token, raw JSON, credential, operational record, or row-level prediction. The browser never connects to the source database. -Bolt publication must use a separate project or repository containing only the -contents of `dashboard/`, with `index.html` at its root. The full source -repository must not be imported into Bolt. The reviewed procedure is in -[bolt_deployment.md](bolt_deployment.md). +The presentation uses `node dashboard/server.mjs --lan` only for the live demo. +The server has an explicit dashboard-file allowlist and is stopped afterward; +the prototype is not published to a public host. ## Limitations @@ -222,4 +222,4 @@ ranking, production model, diagnosis, or individual decision tool. - [Private-to-public architecture](architecture.mmd) - [10-minute presentation outline](presentation_outline.md) - [Dashboard demo script](demo_script.md) -- [Dashboard-only Bolt deployment](bolt_deployment.md) +- [Optional public-hosting notes](bolt_deployment.md) diff --git a/docs/presentation_outline.md b/docs/presentation_outline.md index 6c9aa8e..896b994 100644 --- a/docs/presentation_outline.md +++ b/docs/presentation_outline.md @@ -22,7 +22,7 @@ model** and histogram gradient boosting as the **benchmark**. | 3:10-4:20 | 5. Model comparison | Logistic final versus prevalence/previous-outcome baselines and boosted-tree benchmark | | 4:20-4:50 | 6. Model decision | Calibrated logistic wins the declared sample comparison and is easier to explain | | 4:50-7:35 | Live dashboard demo | Overview, Sample cohorts, Model & benchmark, Data & methods | -| 7:35-8:30 | 7. Privacy architecture | Private pipeline to suppressed aggregate JSON to dashboard-only Bolt project | +| 7:35-8:30 | 7. Privacy architecture | Private pipeline to suppressed aggregate JSON to an allowlisted same-network demo server | | 8:30-9:20 | 8. Limitations | Sampling, feed coverage, source/time confounding, heterogeneous non-pass, one-time holdout | | 9:20-10:00 | 9. Close | Prototype is complete and honest about what it can—and cannot—claim | @@ -124,7 +124,7 @@ Show: ```text private read-only data -> local modeling -> suppressed aggregate JSON --> dashboard-only Bolt project +-> allowlisted local static server -> same-network demo browser ``` State that the public bundle has no VINs, plates, ZIPs, stations, technician @@ -159,8 +159,11 @@ cannot establish. ## Presentation checklist -- Start the local dashboard before presenting: `node dashboard/server.mjs`. -- Open `http://127.0.0.1:4173/#overview` and close unrelated browser tabs. +- Put both computers on the same trusted network, stop any older dashboard + process, and run `node dashboard/server.mjs --lan`. +- On the demo computer, open the printed **Other devices** URL for the shared + Wi-Fi or Ethernet interface with `/#overview` appended. Close unrelated + browser tabs. - Use a fresh page load to confirm aggregate validation succeeds. - Keep a screenshot or short recording as a backup, with the sample warning visible. diff --git a/docs/project_charter.md b/docs/project_charter.md index 2b53980..c9bfcfa 100644 --- a/docs/project_charter.md +++ b/docs/project_charter.md @@ -69,8 +69,8 @@ is not a second final model and is not used to drive the product. 5. Histogram gradient boosting as a benchmark only. 6. Chronological development evaluation with an explicit one-time 2025 gate. 7. Suppression-reviewed static dashboard assets with fail-closed validation. -8. Model card, final report, presentation materials, and dashboard-only Bolt - deployment instructions. +8. Model card, final report, presentation materials, and same-network dashboard + demo instructions. ## Acceptance criteria