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>
87 lines
2.0 KiB
Go
87 lines
2.0 KiB
Go
// Package labdb_static embeds labdb's HTML/CSS/JS into the binary so
|
|
// the fivenode_go process ships every byte the browser needs —
|
|
// no separate web server, no filesystem dependency.
|
|
//
|
|
// PRG surface
|
|
//
|
|
// cBody := LABDB_STATIC_FILE(cPath) -> file contents, or "" when missing
|
|
// cMime := LABDB_STATIC_MIME(cPath) -> Content-Type guessed from extension
|
|
//
|
|
// cPath is taken verbatim from hReq["path"], so "/" maps to index.html
|
|
// and a missing leading slash is fine.
|
|
package labdb_static
|
|
|
|
import (
|
|
"embed"
|
|
"path"
|
|
"strings"
|
|
|
|
"five/hbrt"
|
|
)
|
|
|
|
//go:embed public
|
|
var fs embed.FS
|
|
|
|
func init() {
|
|
hbrt.HB_FUNC("LABDB_STATIC_FILE", staticFile)
|
|
hbrt.HB_FUNC("LABDB_STATIC_MIME", staticMime)
|
|
}
|
|
|
|
// resolve turns "/" -> "public/index.html",
|
|
// "/css/app.css" -> "public/css/app.css",
|
|
// "css/app.css" -> "public/css/app.css".
|
|
func resolve(p string) string {
|
|
p = strings.TrimPrefix(p, "/")
|
|
if p == "" {
|
|
p = "index.html"
|
|
}
|
|
// Normalize any "..", "//" so an attacker can't escape the embed root.
|
|
cleaned := path.Clean("/" + p)
|
|
if cleaned == "/" {
|
|
return "public/index.html"
|
|
}
|
|
return "public" + cleaned
|
|
}
|
|
|
|
func staticFile(ctx *hbrt.HBContext) {
|
|
if ctx.PCount() < 1 || !ctx.IsChar(1) {
|
|
ctx.RetC("")
|
|
return
|
|
}
|
|
data, err := fs.ReadFile(resolve(ctx.ParC(1)))
|
|
if err != nil {
|
|
ctx.RetC("")
|
|
return
|
|
}
|
|
ctx.RetC(string(data))
|
|
}
|
|
|
|
func staticMime(ctx *hbrt.HBContext) {
|
|
if ctx.PCount() < 1 || !ctx.IsChar(1) {
|
|
ctx.RetC("application/octet-stream")
|
|
return
|
|
}
|
|
// Use the resolved path so "/" -> .html, not "" -> octet-stream.
|
|
ext := strings.ToLower(path.Ext(resolve(ctx.ParC(1))))
|
|
switch ext {
|
|
case ".html", ".htm":
|
|
ctx.RetC("text/html; charset=utf-8")
|
|
case ".css":
|
|
ctx.RetC("text/css; charset=utf-8")
|
|
case ".js", ".mjs":
|
|
ctx.RetC("application/javascript; charset=utf-8")
|
|
case ".json":
|
|
ctx.RetC("application/json; charset=utf-8")
|
|
case ".svg":
|
|
ctx.RetC("image/svg+xml")
|
|
case ".png":
|
|
ctx.RetC("image/png")
|
|
case ".jpg", ".jpeg":
|
|
ctx.RetC("image/jpeg")
|
|
case ".ico":
|
|
ctx.RetC("image/x-icon")
|
|
default:
|
|
ctx.RetC("application/octet-stream")
|
|
}
|
|
}
|