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