Files
fivenode_go/hbrtl_ext/labdb_static/public/js/api.js
Charles KWON OhJun 47b8f0434a feat(static): embed labdb/public/* and serve from BridgeDispatch
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>
2026-05-27 17:01:09 +09:00

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
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')}`;
}