Files
scrabble-game/gateway/internal/webui/webui.go
T
Ilia Denisov 8881214213 R6(a): de-stage code, docs, READMEs; split stage6_test
Mechanical, behaviour-preserving removal of Stage N / TODO-N / phase (RN)
references from comments, doc-comments, service READMEs, the current-state docs
(ARCHITECTURE, FUNCTIONAL+_ru, TESTING, UI_DESIGN), config-file comments, and the
.fbs/.proto schema comments. PLAN.md / PRERELEASE.md / CLAUDE.md keep the stage
history.

- Rename the only stage-named identifiers: registerStage8 -> registerSocialOps,
  registerStage11 -> registerLinkOps (gateway transcode).
- Split stage6_test.go: TestEmailLoginFlow -> email_test.go,
  TestGuestAutoMatchLeavesNoStats (+ provisionGuest) -> account_test.go.
- Regenerated proto bindings (push.pb.go, telegram_grpc.pb.go) from the de-staged
  .proto comments; FB Go/TS bindings unchanged (flatc strips schema comments).

go build/vet/gofmt clean across modules; integration typecheck and pnpm check green.
2026-06-10 16:56:03 +02:00

84 lines
3.2 KiB
Go

// Package webui serves the embedded static UI build over the public edge.
//
// The committed dist/ holds only a placeholder index.html so the gateway module
// compiles with a plain `go build` (and in CI) without a UI build. The production
// gateway image replaces dist/ with the real Vite build — minus landing.html, which
// ships in the separate landing container — before compiling (see
// gateway/Dockerfile), so the binary ships the UI inside it. Because Vite is built
// with a relative asset base, one build serves under any path: the game SPA is
// mounted at /app/ (web) and /telegram/ (the Telegram Mini App) — the single-origin
// model in docs/ARCHITECTURE.md §13.
//
// Caching: Vite emits hash-named files under assets/, so those are immutable and
// cached hard (a reload/relaunch is a cache hit, not a re-download); the HTML shells carry
// no-cache so a new deploy is picked up immediately.
package webui
import (
"embed"
"io/fs"
"net/http"
"path"
"strings"
)
//go:embed all:dist
var dist embed.FS
// distFS returns the embedded build rooted at dist/. The directory is embedded at
// compile time, so its absence is a build error rather than a runtime condition.
func distFS() fs.FS {
sub, err := fs.Sub(dist, "dist")
if err != nil {
panic("webui: embedded dist/ missing: " + err.Error())
}
return sub
}
// Handler serves the embedded UI. An existing file is served directly (hash-named assets get
// an immutable cache); every other path falls back to indexName (the SPA shell) so a
// client-side deep link still loads. When stripPrefix is non-empty it is removed from the
// request path before lookup, so the same build serves under a sub-path (e.g. "/app/" or
// "/telegram/").
func Handler(stripPrefix, indexName string) http.Handler {
content := distFS()
files := http.FileServer(http.FS(content))
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(path.Clean("/"+r.URL.Path), "/")
if name == "" {
serveIndex(w, content, indexName)
return
}
if info, err := fs.Stat(content, name); err != nil || info.IsDir() {
// Unknown path or a directory: serve the shell, never a listing.
serveIndex(w, content, indexName)
return
}
// Hash-named build assets are immutable — cache them for a year so reopening the
// app (notably a relaunched Telegram Mini App) is a cache hit, not a re-download.
if strings.HasPrefix(name, "assets/") {
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
}
files.ServeHTTP(w, r)
})
if p := strings.TrimSuffix(stripPrefix, "/"); p != "" {
return http.StripPrefix(p, h)
}
return h
}
// serveIndex writes the named HTML shell with a 200 status, so a client-routed deep link
// still loads the app rather than a 404. The shell is marked no-cache so a new deploy's
// shell (and the asset URLs it references) is fetched fresh.
func serveIndex(w http.ResponseWriter, content fs.FS, indexName string) {
data, err := fs.ReadFile(content, indexName)
if err != nil {
http.Error(w, "ui not built", http.StatusInternalServerError)
return
}
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(data)
}