Drops labdb's index.html / login.html / css / js into the binary via
embed.FS so the single 24 MB fivenode_go binary now ships both the
HTML/JS frontend AND the JSON API. No web server, no Apache,
no asset bundler.
hbrtl_ext/labdb_static/
public/ mirror of fivenode/labdb/public/
assets.go //go:embed public + two PRG-callable
HB_FUNCs: LABDB_STATIC_FILE(cPath) returns
the bytes, LABDB_STATIC_MIME(cPath)
returns the Content-Type derived from
the resolved (not raw) extension so "/"
-> text/html, not application/octet-stream.
app/bridge_server.prg
BridgeDispatch now falls through to the static FS for any path
that isn't /api/*. Missing assets get a 404 instead of being
routed to the path-to-symbol dispatcher.
Verified:
GET / -> 200 text/html, full index.html body
GET /login.html -> 200 text/html, 1850 bytes
GET /css/app.css -> 200 text/css
GET /api/admin-stats -> 200 JSON {devices:2,...} (still live PG)
GET /nonexistent.html -> 404 text/plain
Phase 1a complete: HTTP serves both the labdb frontend and the
real-data labdb API from one binary, end to end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
44 lines
1.8 KiB
JavaScript
44 lines
1.8 KiB
JavaScript
// labdb — Admin web client
|
|
const api = {
|
|
async _fetch(path, opts) {
|
|
const o = Object.assign({ credentials: 'same-origin' }, opts || {});
|
|
o.headers = Object.assign({ 'Content-Type': 'application/json' }, o.headers || {});
|
|
const res = await fetch(path, o);
|
|
if (res.status === 401 && !location.pathname.endsWith('/login.html')) {
|
|
location.href = '/login.html';
|
|
return { error: 'unauthorized' };
|
|
}
|
|
try { return await res.json(); } catch (_) { return { error: 'invalid response' }; }
|
|
},
|
|
get(p) { return this._fetch(p); },
|
|
post(p, body) { return this._fetch(p, { method: 'POST', body: JSON.stringify(body || {}) }); },
|
|
patch(p, body) { return this._fetch(p, { method: 'PATCH', body: JSON.stringify(body || {}) }); },
|
|
del(p) { return this._fetch(p, { method: 'DELETE' }); },
|
|
};
|
|
|
|
function esc(s) {
|
|
if (s === null || s === undefined) return '';
|
|
return String(s)
|
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
.replace(/"/g, '"').replace(/'/g, ''');
|
|
}
|
|
|
|
function formatDate(iso) {
|
|
if (!iso) return '—';
|
|
const d = new Date(iso);
|
|
if (isNaN(d)) return '—';
|
|
const pad = (n) => String(n).padStart(2, '0');
|
|
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
}
|
|
|
|
function formatDuration(start, end) {
|
|
if (!start || !end) return '—';
|
|
const ms = new Date(end) - new Date(start);
|
|
if (isNaN(ms) || ms < 0) return '—';
|
|
const sec = Math.floor(ms / 1000);
|
|
const h = Math.floor(sec / 3600);
|
|
const m = Math.floor((sec % 3600) / 60);
|
|
const s = sec % 60;
|
|
return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
|
|
}
|