Files
fivenode_go/hbrtl_ext/labdb_static/assets.go
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

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")
}
}