fixed issues w/ accessing website on other computers

This commit is contained in:
2026-07-22 16:26:12 -06:00
parent c1e453d962
commit 8bb76d8e6c
13 changed files with 266 additions and 68 deletions
+27 -24
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.
+100 -1
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 {
+1
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": {
+43 -3
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.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, () => {
process.stdout.write(`Utah Vehicle Health dashboard: http://${HOST}:${PORT}\n`);
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`);
});
+26
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)