diff --git a/dashboard/README.md b/dashboard/README.md index 0a416b5..13e5790 100644 --- a/dashboard/README.md +++ b/dashboard/README.md @@ -32,3 +32,20 @@ npm run dev # Start dev server npm run build # Production bundle npm run lint # ESLint ``` + +## Deployment (GitHub Pages) +When deploying to `https://.github.io//` you must build with the correct base path so that static JSON (in `public/data/`) and bundled assets resolve properly. + +```bash +# Example for this repository if the dashboard lives at /slcomp/ +export BASE_PATH=/slcomp/ +npm run build +``` + +Then publish the contents of `dist/` to the `gh-pages` branch (or use an action). The data loader code uses `import.meta.env.BASE_URL` to construct paths like `data/database.json`, avoiding the common `Unexpected token '<'` JSON parse error that happens when a 404 HTML page is fetched instead of the JSON file. + +If you see that error after deployment, confirm: +1. The JSON files exist in `dist/data/` (they are copied from `public/data/`). +2. `BASE_PATH` matched the repository subpath and ends with a trailing slash. +3. Browser network panel requests resolve to `200` and not `404`/`301`. + diff --git a/dashboard/index.html b/dashboard/index.html index 2b39526..1e556f6 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -23,6 +23,6 @@
- + diff --git a/dashboard/src/api.ts b/dashboard/src/api.ts index fcad51f..4b6a76e 100644 --- a/dashboard/src/api.ts +++ b/dashboard/src/api.ts @@ -1,28 +1,41 @@ import type { DataRecord, ConsolidatedRecord, Dictionary, CutoutRecord } from './types'; -// In absence of a backend we will load static JSON via fetch (can be swapped later) +// NOTE: When deployed on GitHub Pages the app is usually served from //. +// Using absolute paths (starting with "/") causes fetches to point at the domain root +// (e.g. https://.github.io/data/...) which 404s and returns the HTML index page. +// That HTML then triggers: "SyntaxError: Unexpected token '<'" when res.json() is called. +// We instead build URLs relative to Vite's injected BASE_URL (import.meta.env.BASE_URL). -export const loadDatabase = async (): Promise => { - const res = await fetch('/data/database.json'); - return res.json(); -}; +const BASE_URL: string = (import.meta as { env?: Record }).env?.BASE_URL || '/'; -export const loadConsolidated = async (): Promise => { - const res = await fetch('/data/consolidated_database.json'); - return res.json(); +const buildDataUrl = (file: string) => { + // Ensure exactly one trailing slash for BASE_URL then append data/ + const base = BASE_URL.endsWith('/') ? BASE_URL : `${BASE_URL}/`; + return `${base}data/${file}`; }; -export const loadDictionary = async (): Promise => { - // dictionary.npy not easily consumable directly; expect a JSON conversion later. - // Placeholder expects a json representation placed at /data/dictionary.json - const res = await fetch('/data/dictionary.json'); - return res.json(); -}; +async function fetchJson(file: string): Promise { + const url = buildDataUrl(file); + const res = await fetch(url, { cache: 'no-cache' }); + if (!res.ok) { + // Surface clearer diagnostics when something goes wrong (like path issues on Pages) + const text = await res.text(); + throw new Error(`Failed to fetch ${url} (HTTP ${res.status}) - First 120 chars: ${text.slice(0, 120)}`); + } + try { + return await res.json(); + } catch (err) { + // Provide snippet of body to aid debugging of unexpected HTML responses + const body = await res.clone().text().catch(() => ''); + throw new Error(`Invalid JSON at ${url}: ${(err as Error).message}. Snippet: ${body.slice(0, 120)}`); + } +} -export const loadCutouts = async (): Promise => { - const res = await fetch('/data/cutouts.json'); - return res.json(); -}; +// Public data loaders (can be swapped for real API later) +export const loadDatabase = (): Promise => fetchJson('database.json'); +export const loadConsolidated = (): Promise => fetchJson('consolidated_database.json'); +export const loadDictionary = (): Promise => fetchJson('dictionary.json'); +export const loadCutouts = (): Promise => fetchJson('cutouts.json'); // MinIO direct image retrieval (signed URL pattern) - placeholder using fetch of gateway // Direct public path construction (no proxy). Expect objectKey like