Files
llmgw/internal/web/install.go
T
Ilker ManapandClaude Opus 5.5 fea5774fe5 The install guide shows every screen of a real install
/install takes a reader from three bare Ubuntu machines to a first answer:
what each machine needs and the commands that check it, the release fetched
and its checksum checked, the eight setup screens, the system's state and
the console, and a first request with a key from the account page. Every
screen is from an install of v0.14.9 on three fresh VMs, recorded with tmux
and kept as terminal frames: the page shows them as text, and plays a
session back when asked, fetching it compressed from /install/cast/. The
administrator's password and the API key were masked before the recording
was kept, and a test fails if either shape reaches the page.

/about gains what llmgw now does beyond routing, and its install steps are
today's: the setup screens and node add, not onboard-node.sh and a model
file, with a link into the guide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:35:08 +02:00

250 lines
7.4 KiB
Go

package web
import (
"bytes"
"compress/gzip"
"crypto/sha256"
_ "embed"
"encoding/hex"
"encoding/json"
"html/template"
"io"
"net/http"
"regexp"
"strings"
)
// The install guide: every step from bare machines to a first answer, each
// shown as the screen it was on. The screens are a real install, recorded on
// three fresh Ubuntu machines, kept as terminal frames rather than pictures:
// crisp at any size, a few kilobytes, text a reader can select — and played
// back, where the reader asks for it, as the session it was.
//
// The recording is guide/guide.json.gz, made by the scripts that drove the
// install; the administrator's password and every key in it were masked
// before it was written. Like /about, the page says nothing about this
// installation, only about the software, and is served without a key.
//go:embed guide/guide.json.gz
var guideGz []byte
// guideCast is one recorded session: its frames, and its last screen to show
// before anyone presses play.
type guideCast struct {
Cols int `json:"cols"`
Rows int `json:"rows"`
Frames []json.RawMessage `json:"frames"`
Poster []string `json:"poster"`
}
// guideStill is one screen of a session, picked for the step it shows.
type guideStill struct {
Cast string `json:"cast"`
Frame int `json:"frame"`
Lines []string `json:"lines"`
}
type guideFile struct {
Casts map[string]guideCast `json:"casts"`
Stills map[string]guideStill `json:"stills"`
}
// guide is read once, when the binary starts: a recording that does not
// parse is a build that should not ship, and the tests say so first.
var guide = func() guideFile {
zr, err := gzip.NewReader(bytes.NewReader(guideGz))
if err != nil {
panic("web: the install guide's recording is damaged: " + err.Error())
}
b, err := io.ReadAll(zr)
if err != nil {
panic("web: the install guide's recording is damaged: " + err.Error())
}
var g guideFile
if err := json.Unmarshal(b, &g); err != nil {
panic("web: the install guide's recording does not parse: " + err.Error())
}
return g
}()
// castFiles are the recordings as the player fetches them, compressed once:
// the page itself carries only each one's last screen, and a session is
// downloaded when someone presses play.
var castFiles = func() map[string][]byte {
out := map[string][]byte{}
for name, c := range guide.Casts {
b, err := json.Marshal(struct {
Rows int `json:"rows"`
Frames []json.RawMessage `json:"frames"`
}{c.Rows, c.Frames})
if err != nil {
panic(err)
}
var z bytes.Buffer
zw, _ := gzip.NewWriterLevel(&z, gzip.BestCompression)
_, _ = zw.Write(b)
_ = zw.Close()
out[name] = z.Bytes()
}
return out
}()
// CastView is what the page needs of a recording: the screen it opens on,
// and where the player fetches the whole session.
type CastView struct {
Name string
Cols, Rows int
Poster template.HTML
Src string
}
// InstallData is what the guide is rendered with.
type InstallData struct {
Version, Release string
Site bool
}
// recorded is when the guide's install was made, and with which release: the
// screens are that release's, whatever version serves the page.
const recorded = "v0.14.9 · 2026-10-23"
// Recorded says which release the screens are of, and when they were taken.
func (InstallData) Recorded() string { return recorded }
// AboutURL is the page about llmgw: on a gateway its /about, on the website
// the site's front page beside this one.
func (d InstallData) AboutURL() string {
if d.Site {
return "./"
}
return "/about"
}
// Img is a picture of the portal, served by the gateway or, on the website,
// beside the page.
func (d InstallData) Img(name string) string {
if d.Site {
return "guide/" + name
}
return assetURL("guide/" + name)
}
// ShotView is one setup screen and the label over it.
type ShotView struct {
Label string
Screen template.HTML
}
// Shot is the setup screen called name, labelled for its step.
func (d InstallData) Shot(name, label string) ShotView {
return ShotView{Label: label, Screen: d.Still(name)}
}
// Cast is the recording called name, ready for the page. On a gateway it is
// fetched from /install/cast/, named by its content so a cache never plays an
// old one; on the website, from beside the page.
func (d InstallData) Cast(name string) CastView {
c, ok := guide.Casts[name]
if !ok {
panic("web: the install guide has no recording " + name)
}
v := CastView{Name: name, Cols: c.Cols, Rows: c.Rows, Poster: screen(c.Poster)}
if d.Site {
v.Src = "guide/" + name + ".json"
} else {
sum := sha256.Sum256(castFiles[name])
v.Src = "/install/cast/" + name + ".json?v=" + hex.EncodeToString(sum[:4])
}
return v
}
// CastFile is a recording as the website serves it beside the page: JSON,
// compressed, for the website's build to write out under guide/.
func CastFile(name string) ([]byte, bool) {
b, ok := castFiles[name]
return b, ok
}
// CastNames are the recordings the guide plays.
func CastNames() []string {
var names []string
for n := range guide.Casts {
names = append(names, n)
}
return names
}
// InstallCast serves one recording at /install/cast/{name}.json, compressed
// when the browser takes it so, as nearly every one does.
func InstallCast(w http.ResponseWriter, r *http.Request) {
name := strings.TrimSuffix(r.PathValue("file"), ".json")
b, ok := castFiles[name]
if !ok || !strings.HasSuffix(r.PathValue("file"), ".json") {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "public, max-age=86400")
w.Header().Set("Vary", "Accept-Encoding")
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
w.Header().Set("Content-Encoding", "gzip")
_, _ = w.Write(b)
return
}
zr, err := gzip.NewReader(bytes.NewReader(b))
if err != nil {
http.Error(w, "the recording is damaged", http.StatusInternalServerError)
return
}
_, _ = io.Copy(w, zr)
}
// Still is the screen of a step, as the recording showed it.
func (InstallData) Still(name string) template.HTML {
s, ok := guide.Stills[name]
if !ok {
panic("web: the install guide has no screen " + name)
}
return screen(s.Lines)
}
// screen joins a frame's lines. They are HTML the recording's builder made
// from the terminal's own colours — spans and escaped text, nothing a visitor
// supplied — which is why they are trusted here.
//
// Blank rows at the foot are left off: a shell session that filled a third
// of the terminal would otherwise sit above a block of black. The player
// gives the screen its full height once it plays.
func screen(lines []string) template.HTML {
n := len(lines)
for n > 0 && strings.TrimSpace(tagText.ReplaceAllString(lines[n-1], "")) == "" {
n--
}
return template.HTML(strings.Join(lines[:n], "\n"))
}
// tagText matches the markup in a screen's line, to see whether any text is left.
var tagText = regexp.MustCompile(`<[^>]*>`)
// NewInstallData fills the guide's data for a version.
func NewInstallData(version string) InstallData {
d := InstallData{Version: version}
if releaseTag.MatchString(version) {
d.Release = version
}
return d
}
// RenderInstall writes the guide, for the website's build as much as for Install.
func RenderInstall(w io.Writer, d InstallData) error {
return tmpl.ExecuteTemplate(w, "install", d)
}
// Install serves the guide at /install.
func Install(version string) http.HandlerFunc {
d := NewInstallData(version)
return func(w http.ResponseWriter, r *http.Request) {
render(w, r, "install", d)
}
}