fixed issues w/ accessing website on other computers
This commit is contained in:
parent
c1e453d962
commit
8bb76d8e6c
37
README.md
37
README.md
@ -31,7 +31,7 @@ Start with the [final report](docs/final_report.md). Supporting deliverables:
|
|||||||
- [Private-to-public architecture](docs/architecture.mmd)
|
- [Private-to-public architecture](docs/architecture.mmd)
|
||||||
- [10-minute presentation outline](docs/presentation_outline.md)
|
- [10-minute presentation outline](docs/presentation_outline.md)
|
||||||
- [Dashboard demo script](docs/demo_script.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
|
The [data inventory](docs/data_inventory.md) and archived
|
||||||
[project options](docs/project_options.md) provide source-discovery history;
|
[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
|
contract. Every checked-in asset is marked as a development preview and not a
|
||||||
population estimate.
|
population estimate.
|
||||||
|
|
||||||
## Publish through Bolt
|
### Live demo from another computer
|
||||||
|
|
||||||
Do **not** import this full source repository into Bolt. Create a separate
|
When both computers are on the same network, start the LAN demo from the
|
||||||
deployment project or repository containing only the contents of `dashboard/`,
|
repository root. Stop any older dashboard process with `Ctrl-C` first:
|
||||||
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.
|
|
||||||
|
|
||||||
Follow the complete preflight, import, Bolt Hosting, and post-publication checks
|
```bash
|
||||||
in [docs/bolt_deployment.md](docs/bolt_deployment.md). No publishing action is
|
node dashboard/server.mjs --lan
|
||||||
performed by this repository.
|
```
|
||||||
|
|
||||||
|
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
|
## Author
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,28 @@ URL; browsers will block the JSON module requests. To use another port:
|
|||||||
PORT=8080 node dashboard/server.mjs
|
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:
|
Run the dependency-free contract checks with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -68,28 +90,9 @@ operational connection, or row-level prediction output. VINs, plates, ZIPs,
|
|||||||
stations, technician identifiers, raw source JSON, credentials, and operational
|
stations, technician identifiers, raw source JSON, credentials, and operational
|
||||||
records must never enter this directory.
|
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
|
The current delivery path is the same-network live demo. No Bolt project,
|
||||||
not import this source repository into Bolt.
|
public URL, dependency install, or build step is needed. If public hosting is
|
||||||
|
ever reconsidered, publish only a separately reviewed copy of this dashboard
|
||||||
1. Run `npm test` from this directory and confirm every contract check passes.
|
directory—never the private source repository.
|
||||||
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.
|
|
||||||
|
|||||||
@ -10,6 +10,34 @@ const ENVELOPE_KEYS = Object.freeze([
|
|||||||
"population_estimate_allowed",
|
"population_estimate_allowed",
|
||||||
"schema_version",
|
"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({
|
export const REQUIRED_ASSETS = Object.freeze({
|
||||||
manifest: "data_manifest.json",
|
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) {
|
async function sha256Hex(bytes, cryptoImplementation) {
|
||||||
if (!cryptoImplementation?.subtle || typeof cryptoImplementation.subtle.digest !== "function") {
|
if (!cryptoImplementation?.subtle || typeof cryptoImplementation.subtle.digest !== "function") {
|
||||||
throw new DataContractError("This browser cannot verify dashboard asset integrity.");
|
return sha256HexFallback(bytes);
|
||||||
}
|
}
|
||||||
let digest;
|
let digest;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
"description": "Dependency-free static dashboard for approved Utah Vehicle Health aggregates.",
|
"description": "Dependency-free static dashboard for approved Utah Vehicle Health aggregates.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.mjs",
|
"start": "node server.mjs",
|
||||||
|
"start:lan": "node server.mjs --lan",
|
||||||
"test": "node --test tests/contract.test.mjs"
|
"test": "node --test tests/contract.test.mjs"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { createReadStream, realpathSync, statSync } from "node:fs";
|
import { createReadStream, realpathSync, statSync } from "node:fs";
|
||||||
import { createServer } from "node:http";
|
import { createServer } from "node:http";
|
||||||
|
import { networkInterfaces } from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
const ROOT = realpathSync(path.dirname(fileURLToPath(import.meta.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 PORT = Number.parseInt(process.env.PORT || "4173", 10);
|
||||||
const TYPES = new Map([
|
const TYPES = new Map([
|
||||||
[".css", "text/css; charset=utf-8"],
|
[".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) => {
|
const server = createServer((request, response) => {
|
||||||
if (!request.url || !["GET", "HEAD"].includes(request.method || "")) {
|
if (!request.url || !["GET", "HEAD"].includes(request.method || "")) {
|
||||||
response.writeHead(405, { Allow: "GET, HEAD" });
|
response.writeHead(405, { Allow: "GET, HEAD" });
|
||||||
@ -66,7 +90,7 @@ const server = createServer((request, response) => {
|
|||||||
const extension = path.extname(file).toLowerCase();
|
const extension = path.extname(file).toLowerCase();
|
||||||
response.writeHead(200, {
|
response.writeHead(200, {
|
||||||
"Content-Type": TYPES.get(extension) || "application/octet-stream",
|
"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",
|
"X-Content-Type-Options": "nosniff",
|
||||||
"Referrer-Policy": "no-referrer",
|
"Referrer-Policy": "no-referrer",
|
||||||
});
|
});
|
||||||
@ -77,6 +101,22 @@ const server = createServer((request, response) => {
|
|||||||
createReadStream(file).on("error", () => response.destroy()).pipe(response);
|
createReadStream(file).on("error", () => response.destroy()).pipe(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
server.listen(PORT, HOST, () => {
|
server.on("error", (error) => {
|
||||||
process.stdout.write(`Utah Vehicle Health dashboard: http://${HOST}:${PORT}\n`);
|
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`);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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", () => {
|
test("sha256 manifest covers and matches every approved data asset", () => {
|
||||||
const manifest = json(REQUIRED_ASSETS.shaManifest);
|
const manifest = json(REQUIRED_ASSETS.shaManifest);
|
||||||
const expectedNames = Object.entries(REQUIRED_ASSETS)
|
const expectedNames = Object.entries(REQUIRED_ASSETS)
|
||||||
|
|||||||
@ -63,7 +63,7 @@ flowchart TD
|
|||||||
end
|
end
|
||||||
EXPORT -.-> GUARDRAILS
|
EXPORT -.-> GUARDRAILS
|
||||||
|
|
||||||
SITE --> BOLT[Separate dashboard-only<br/>Bolt project]
|
SITE --> DEMO[Allowlisted local static server<br/>same-network live demo]
|
||||||
|
|
||||||
subgraph TESTS[Verification]
|
subgraph TESTS[Verification]
|
||||||
direction LR
|
direction LR
|
||||||
@ -80,5 +80,5 @@ flowchart TD
|
|||||||
|
|
||||||
class SOURCE,SAMPLE,STAGE,EPISODES,MART,PRIVATE,REPORT private
|
class SOURCE,SAMPLE,STAGE,EPISODES,MART,PRIVATE,REPORT private
|
||||||
class LOGISTIC,TREE model
|
class LOGISTIC,TREE model
|
||||||
class PUBLIC,CONTRACT,SITE,VIEWS,BOLT public
|
class PUBLIC,CONTRACT,SITE,VIEWS,DEMO public
|
||||||
class GUARDRAILS warning
|
class GUARDRAILS warning
|
||||||
|
|||||||
@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
Last reviewed: 2026-07-21
|
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
|
## Non-negotiable publication boundary
|
||||||
|
|
||||||
Publish a **separate Bolt project or GitHub repository containing only the
|
Publish a **separate Bolt project or GitHub repository containing only the
|
||||||
|
|||||||
@ -121,13 +121,11 @@ developer console.
|
|||||||
- Preserve the warning banner and fail-closed unavailable state on desktop and
|
- Preserve the warning banner and fail-closed unavailable state on desktop and
|
||||||
mobile layouts.
|
mobile layouts.
|
||||||
|
|
||||||
## Deployment boundary
|
## Demo boundary
|
||||||
|
|
||||||
Bolt receives a separate dashboard-only project whose root is the contents of
|
The current delivery is a same-network live demo started with
|
||||||
`dashboard/`. Do not import or upload the full private-pipeline repository and
|
`node dashboard/server.mjs --lan`. The local server resolves files only beneath
|
||||||
do not rely on a configurable subdirectory working root. The deployable project
|
`dashboard/` and serves only its explicit shell, JavaScript, stylesheet, and
|
||||||
contains no `.env`, private `data/`, model artifacts, SQL, pipeline scripts, or
|
approved aggregate allowlist. It does not expose `.env`, private `data/`, model
|
||||||
repository history outside the dashboard directory.
|
artifacts, SQL, pipeline scripts, or repository history. Stop the server after
|
||||||
|
the demo; no public hosting step is required.
|
||||||
See [bolt_deployment.md](bolt_deployment.md) for the reviewed handoff and
|
|
||||||
post-publication checks.
|
|
||||||
|
|||||||
@ -5,16 +5,22 @@ Target length: **2 minutes 45 seconds**, embedded in the
|
|||||||
|
|
||||||
## Before the audience arrives
|
## Before the audience arrives
|
||||||
|
|
||||||
1. From the repository root, run `node dashboard/server.mjs`.
|
1. Put the presenting computer and demo computer on the same trusted network.
|
||||||
2. Open `http://127.0.0.1:4173/#overview`.
|
2. Stop any older dashboard process, then from the repository root run
|
||||||
3. Confirm the header says the sample aggregates validated.
|
`node dashboard/server.mjs --lan`.
|
||||||
4. Confirm the private-sample development-preview banner is visible.
|
3. On the demo computer, open the printed **Other devices** URL for the shared
|
||||||
5. Visit all four routes: Overview, Sample cohorts, Model & benchmark, and Data
|
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.
|
& 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,
|
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
|
## Live talk track
|
||||||
|
|
||||||
|
|||||||
@ -172,7 +172,8 @@ The public boundary is intentionally narrow:
|
|||||||
private read-only source
|
private read-only source
|
||||||
-> private local extraction and model pipeline
|
-> private local extraction and model pipeline
|
||||||
-> suppression-reviewed aggregate JSON
|
-> 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,
|
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
|
operational record, or row-level prediction. The browser never connects to the
|
||||||
source database.
|
source database.
|
||||||
|
|
||||||
Bolt publication must use a separate project or repository containing only the
|
The presentation uses `node dashboard/server.mjs --lan` only for the live demo.
|
||||||
contents of `dashboard/`, with `index.html` at its root. The full source
|
The server has an explicit dashboard-file allowlist and is stopped afterward;
|
||||||
repository must not be imported into Bolt. The reviewed procedure is in
|
the prototype is not published to a public host.
|
||||||
[bolt_deployment.md](bolt_deployment.md).
|
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
@ -222,4 +222,4 @@ ranking, production model, diagnosis, or individual decision tool.
|
|||||||
- [Private-to-public architecture](architecture.mmd)
|
- [Private-to-public architecture](architecture.mmd)
|
||||||
- [10-minute presentation outline](presentation_outline.md)
|
- [10-minute presentation outline](presentation_outline.md)
|
||||||
- [Dashboard demo script](demo_script.md)
|
- [Dashboard demo script](demo_script.md)
|
||||||
- [Dashboard-only Bolt deployment](bolt_deployment.md)
|
- [Optional public-hosting notes](bolt_deployment.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 |
|
| 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: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 |
|
| 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 |
|
| 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 |
|
| 9:20-10:00 | 9. Close | Prototype is complete and honest about what it can—and cannot—claim |
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ Show:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
private read-only data -> local modeling -> suppressed aggregate JSON
|
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
|
State that the public bundle has no VINs, plates, ZIPs, stations, technician
|
||||||
@ -159,8 +159,11 @@ cannot establish.
|
|||||||
|
|
||||||
## Presentation checklist
|
## Presentation checklist
|
||||||
|
|
||||||
- Start the local dashboard before presenting: `node dashboard/server.mjs`.
|
- Put both computers on the same trusted network, stop any older dashboard
|
||||||
- Open `http://127.0.0.1:4173/#overview` and close unrelated browser tabs.
|
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.
|
- Use a fresh page load to confirm aggregate validation succeeds.
|
||||||
- Keep a screenshot or short recording as a backup, with the sample warning
|
- Keep a screenshot or short recording as a backup, with the sample warning
|
||||||
visible.
|
visible.
|
||||||
|
|||||||
@ -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.
|
5. Histogram gradient boosting as a benchmark only.
|
||||||
6. Chronological development evaluation with an explicit one-time 2025 gate.
|
6. Chronological development evaluation with an explicit one-time 2025 gate.
|
||||||
7. Suppression-reviewed static dashboard assets with fail-closed validation.
|
7. Suppression-reviewed static dashboard assets with fail-closed validation.
|
||||||
8. Model card, final report, presentation materials, and dashboard-only Bolt
|
8. Model card, final report, presentation materials, and same-network dashboard
|
||||||
deployment instructions.
|
demo instructions.
|
||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user