fixed issues w/ accessing website on other computers

This commit is contained in:
Kevin Bell 2026-07-22 16:26:12 -06:00
parent c1e453d962
commit 8bb76d8e6c
13 changed files with 267 additions and 69 deletions

View File

@ -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

View File

@ -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.

View File

@ -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 {

View File

@ -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": {

View File

@ -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`);
});

View File

@ -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)

View File

@ -63,7 +63,7 @@ flowchart TD
end
EXPORT -.-> GUARDRAILS
SITE --> BOLT[Separate dashboard-only<br/>Bolt project]
SITE --> DEMO[Allowlisted local static server<br/>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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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