diff --git a/.gitignore b/.gitignore index 15c619ac1f73238e5e1703fb120e401e8db75c0a..07789a4950ca68fbc68bd6bc657fa115bb28777e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ *.tmp # Local instance config /config.ini + +# Git worktrees checked out inside the repo (both conventions seen here) +.worktrees/ +.claude/worktrees/ diff --git a/README.md b/README.md index ec21de6165b74aa8a14c87bbe37c0bd557bead15..ee411d3133c57b03fe0a3f0e9d53df3681770b5b 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,9 @@ which git.sr.ht's own loader scopes to public/unlisted repositories, so private repos are never leaked (an unauthorized or missing repo is always a 404, never a 403). Identity comes from decrypting the shared `sr.ht.unified-login.v1` cookie with the instance network key; there is no login flow of our own. Pages are -server-rendered Go templates that reproduce the SourceHut chrome (nav, -service-switcher, login block, environment banner). The diff/tree UI is one +server-rendered Go templates wrapped in the SourceHut chrome (nav, +service-switcher, login block, environment banner), which comes from the shared +[sr-ht-ecore] `chrome` package rather than from a copy of its own. The diff/tree UI is one vendored [pierre-libs] esbuild bundle driven by a JSON blob embedded in the page, so the browser does all diff rendering from a single request with no second authorization round-trip. A short-TTL in-memory cache in front of the @@ -34,6 +35,7 @@ [sourcehut-custom-service integration model]: https://man.sr.ht/ [go-git]: https://github.com/go-git/go-git [pierre-libs]: https://www.npmjs.com/org/pierre +[sr-ht-ecore]: https://git.srht.bigb.es/~bigbes/sr-ht-ecore ## Layout @@ -43,7 +45,8 @@ - `gitx/` — bare-repo access over go-git: refs, ref-to-ref diffs, single-commit diffs, and commit logs, all bounded by context timeouts and output-size caps. - `authz/` — cookie→identity and the git.sr.ht GraphQL authorizer with a short TTL cache. -- `web/` — chi router, handlers, chrome, Go templates, embedded static assets. +- `web/` — chi router, handlers, Go templates, embedded static assets. The page + chrome is not here: it is `sr-ht-ecore/chrome`, wired up in `web/server.go`. - `frontend/` + `scss/` — build-time TypeScript diff bundle and the SCSS entry. - `cmd/comparesrht/` — the daemon entry point and startup validation. - `contrib/` — nginx server block, systemd unit, and a dev GraphQL stub. diff --git a/go.mod b/go.mod index 409817556441a64c5b2887f4356d46cde4662922..739ba1fc7b768e5177a4651f9badfa79ed082a13 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,10 @@ github.com/fernet/fernet-go v0.0.0-20211208181803-9f70042a33ee github.com/go-chi/chi/v5 v5.3.1 github.com/go-git/go-git/v5 v5.19.1 github.com/sirupsen/logrus v1.9.4 + github.com/stretchr/testify v1.11.1 github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec sourcecraft.dev/bigbes/sr-ht-core v0.0.0-20260718185800-dd418a200152 + sourcecraft.dev/bigbes/sr-ht-ecore v0.0.0-20260808143603-174115990895 ) require ( @@ -25,6 +27,7 @@ github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/emersion/go-message v0.18.2 // indirect github.com/emersion/go-pgpmail v0.2.2 // indirect @@ -49,6 +52,7 @@ github.com/lib/pq v1.10.9 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pjbgf/sha1cd v0.6.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.16.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/common v0.44.0 // indirect @@ -63,4 +67,5 @@ golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 90887800bcce98ed1ed7cb3533cde24a39bef3e8..54828e3a7c71bc945f5ece338a3500060698df09 100644 --- a/go.sum +++ b/go.sum @@ -322,3 +322,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= sourcecraft.dev/bigbes/sr-ht-core v0.0.0-20260718185800-dd418a200152 h1:9kQC+tDO2CO8avlKadb9Z0if4a6vJuEK80+4zcb6/fU= sourcecraft.dev/bigbes/sr-ht-core v0.0.0-20260718185800-dd418a200152/go.mod h1:Mu1Vx39ws/OTKWGoVERXvkdRSPLBdhuFTYv0ftVV31c= +sourcecraft.dev/bigbes/sr-ht-ecore v0.0.0-20260808143603-174115990895 h1:OGZrtBtMoXhyZGXrPqMzmrNQnStoCLBVuegGo7yF1Us= +sourcecraft.dev/bigbes/sr-ht-ecore v0.0.0-20260808143603-174115990895/go.mod h1:KeoZjm+/nnsdtc1WxB7X/0EeC+Rggt2OkwJDEc6XWnw= diff --git a/web/chrome.go b/web/chrome.go deleted file mode 100644 index ca1bff30280211e9634d34675717fac554a32b0b..0000000000000000000000000000000000000000 --- a/web/chrome.go +++ /dev/null @@ -1,129 +0,0 @@ -package web - -import ( - "net/http" - "net/url" - "sort" - "strings" - - "github.com/vaughan0/go-ini" - "sourcecraft.dev/bigbes/sr-ht-core/config" - - "sourcecraft.dev/bigbes/sr-ht-compare/authz" -) - -// navCanonical is the SourceHut service-switcher order. Services not listed here -// (including our own compare) sort alphabetically after these. -var navCanonical = []string{"hub", "git", "hg", "lists", "todo", "builds", "man", "meta"} - -// navExcluded are service sections that never appear in the switcher: paste and -// pages have no top-level UI worth linking, and hub is rendered as the brand. -var navExcluded = map[string]bool{"paste": true, "pages": true, "hub": true} - -// navItem is one entry in the service switcher. -type navItem struct { - Name string // short service name, e.g. "git" - Origin string // external origin URL - Active bool // true for compare.sr.ht (this service) -} - -// buildNav derives the service switcher from the shared config: every section -// whose name ends in ".sr.ht" (with a configured origin) except paste/pages/hub, -// ordered by navCanonical then alphabetically, with compare.sr.ht marked active. -func buildNav(conf ini.File) []navItem { - var items []navItem - for section := range conf { - if !strings.HasSuffix(section, ".sr.ht") { - continue - } - short := strings.TrimSuffix(section, ".sr.ht") - if navExcluded[short] { - continue - } - origin := config.GetOrigin(conf, section, true) - if origin == "" { - continue - } - items = append(items, navItem{ - Name: short, - Origin: origin, - Active: section == "compare.sr.ht", - }) - } - sort.SliceStable(items, func(i, j int) bool { - ci, cj := canonIndex(items[i].Name), canonIndex(items[j].Name) - if ci != cj { - return ci < cj - } - return items[i].Name < items[j].Name - }) - return items -} - -// canonIndex returns a service's position in navCanonical, or a sentinel past -// the end for services that are not canonically ordered. -func canonIndex(name string) int { - for i, n := range navCanonical { - if n == name { - return i - } - } - return len(navCanonical) -} - -// viewData is the root value every template is executed against: the chrome -// fields are common to all pages; Data carries the page-specific payload. -type viewData struct { - Title string - SiteName string - HubOrigin string // non-empty ⇒ brand links to hub instead of "/" - Nav []navItem - Username string // "" for an anonymous viewer - LoginURL string - LogoutURL string - RegisterURL string - ProfileURL string - CSSHref string - BundleHref string - Environment string - ShowBanner bool - - // ContainerClass selects the width of the page's content wrapper: the - // centered Bootstrap "container" by default, or "container-fluid" for the - // full-bleed diff views (commit / compare). - ContainerClass string - - Data any -} - -// chrome builds the common chrome fields for a request. Login return_to is the -// current full URL (so the viewer lands back where they were); logout return_to -// is this service's origin. -func (s *Server) chrome(r *http.Request) viewData { - username := authz.ForContext(r.Context()) - - current := s.compareOrigin + r.URL.RequestURI() - loginURL := s.metaOrigin + "/login?return_to=" + url.QueryEscape(current) - logoutURL := s.metaOrigin + "/logout?return_to=" + url.QueryEscape(s.compareOrigin) - - profileURL := s.metaOrigin + "/profile" - if s.hubOrigin != "" && username != "" { - profileURL = s.hubOrigin + "/~" + username - } - - return viewData{ - ContainerClass: "container", - SiteName: s.siteName, - HubOrigin: s.hubOrigin, - Nav: s.nav, - Username: username, - LoginURL: loginURL, - LogoutURL: logoutURL, - RegisterURL: s.metaOrigin, - ProfileURL: profileURL, - CSSHref: s.cssHref, - BundleHref: s.bundleHref, - Environment: strings.ToUpper(s.environment), - ShowBanner: s.environment != "" && s.environment != "production", - } -} diff --git a/web/handlers.go b/web/handlers.go index 19708bbe6e03b4a490542faf82d70d493c377d20..6058177ab327bd5ec7fe5a4a611943dcf33763c6 100644 --- a/web/handlers.go +++ b/web/handlers.go @@ -11,6 +11,7 @@ "strings" "github.com/go-chi/chi/v5" "github.com/sirupsen/logrus" + "sourcecraft.dev/bigbes/sr-ht-ecore/chrome" "sourcecraft.dev/bigbes/sr-ht-compare/authz" "sourcecraft.dev/bigbes/sr-ht-compare/core" @@ -129,15 +130,22 @@ // ---- index ---------------------------------------------------------------- type indexData struct { LoggedIn bool - Repos []authz.RepoInfo + + // Repos is the viewer's own repositories in the shape ecore's + // "srht-repo-list" partial renders, so the landing's listing is the same + // event-list card the sibling services draw for their own projects. + Repos chrome.RepoList } func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { ctx := r.Context() username := authz.ForContext(ctx) - vd := s.chrome(r) - vd.Title = s.siteName + " compare" + // The title is built from the chrome's own brand fields rather than from a + // second read of site-name, so the tab and the nav cannot name the instance + // differently. + vd := s.view(r, "") + vd.Title = vd.SiteName + " " + vd.SiteLabel if username == "" { vd.Data = indexData{LoggedIn: false} @@ -150,10 +158,27 @@ if err != nil { s.fail(w, r, err) return } - vd.Data = indexData{LoggedIn: true, Repos: repos} + vd.Data = indexData{LoggedIn: true, Repos: repoList(username, repos)} s.render(w, http.StatusOK, "index", vd) } +// repoList turns the authorizer's repositories into the listing ecore renders. +// The owner is always the viewer — MyRepos answers for one account — so the +// "~owner/name" title and the link are built from the same name and cannot point +// at somebody else's repository. +func repoList(owner string, repos []authz.RepoInfo) chrome.RepoList { + items := make([]chrome.ListItem, 0, len(repos)) + for _, info := range repos { + items = append(items, chrome.ListItem{ + Href: "/~" + owner + "/" + info.Name, + Title: "~" + owner + "/" + info.Name, + Visibility: info.Visibility, + Description: info.Description, + }) + } + return chrome.RepoList{Items: items, Empty: "You have no repositories yet."} +} + // ---- repo page ------------------------------------------------------------ type repoData struct { @@ -184,8 +209,7 @@ } def, _ := g.DefaultBranch(ctx) commits, _ := recentCommits(ctx, g, def, recentCommitLimit) - vd := s.chrome(r) - vd.Title = "~" + owner + "/" + repo + vd := s.view(r, "~"+owner+"/"+repo) vd.Data = repoData{ Owner: owner, Info: info, @@ -311,9 +335,8 @@ s.fail(w, r, err) return } - vd := s.chrome(r) + vd := s.view(r, fmt.Sprintf("~%s/%s: %s...%s", owner, repo, spec.Base, spec.Head)) vd.ContainerClass = "container-fluid" - vd.Title = fmt.Sprintf("~%s/%s: %s...%s", owner, repo, spec.Base, spec.Head) vd.Data = compareView{ Owner: owner, RepoName: repo, @@ -404,9 +427,8 @@ s.fail(w, r, err) return } - vd := s.chrome(r) + vd := s.view(r, fmt.Sprintf("~%s/%s: %s", owner, repo, ci.ShortSHA)) vd.ContainerClass = "container-fluid" - vd.Title = fmt.Sprintf("~%s/%s: %s", owner, repo, ci.ShortSHA) vd.Data = commitView{ Owner: owner, RepoName: repo, diff --git a/web/server.go b/web/server.go index cab70cc2b781a1c019401ed3fd2e5e87ae154b17..ce522c28276e7e8fcde5e25430cc100a7bf69142 100644 --- a/web/server.go +++ b/web/server.go @@ -1,8 +1,7 @@ -// Package web is the HTTP layer of compare.sr.ht. It ports the SourceHut chrome -// (nav/service-switcher, login block, environment banner) to Go html/templates, -// renders the repository landing, compare (base...head) and single-commit pages -// server-side, and embeds a compact JSON payload plus the vendored esbuild -// bundle so the browser renders the diff with @pierre/diffs and @pierre/trees. +// Package web is the HTTP layer of compare.sr.ht. It renders the repository +// landing, compare (base...head) and single-commit pages server-side, and +// embeds a compact JSON payload plus the vendored esbuild bundle so the browser +// renders the diff with @pierre/diffs and @pierre/trees. // // The package owns no state of its own: identity comes from the authz cookie // middleware, authorization from an authz.Authorizer (git.sr.ht GraphQL), and @@ -10,6 +9,22 @@ // git data from gitx over bare repositories on disk. Every request that touches // a repository authorizes first (a not-found or forbidden repo is a 404, never // a 403, so private-repo existence never leaks) and only then reads the disk. // +// # The chrome is not ours +// +// The nav/service-switcher, the brand, the login block and the environment +// banner come from sourcecraft.dev/bigbes/sr-ht-ecore/chrome, which every custom +// service on the instance shares. This package builds one chrome.Service at +// startup, asks it for a chrome.Page per request, and embeds that Page in +// viewData so the fields promote into the templates. Nothing here rebuilds the +// switcher or re-derives a login URL: the copy that used to live in web/chrome.go +// is exactly what ecore exists to have deleted. +// +// Two things about the chrome remain this service's own, because they are about +// what compare renders and not about the instance: the vendored bundle's href +// (BundleHref below), and the full-bleed ContainerClass the two diff views set — +// a side-by-side diff in a centered "container" is a column of code half the +// window wide. +// // # What the cmd layer must wire // // Register only installs routes; it assumes the following middleware is already @@ -35,11 +50,18 @@ "path" "regexp" "github.com/vaughan0/go-ini" - "sourcecraft.dev/bigbes/sr-ht-core/config" + "sourcecraft.dev/bigbes/sr-ht-ecore/chrome" "sourcecraft.dev/bigbes/sr-ht-compare/authz" ) +// configSection is this service's literal section in the shared config.ini. It +// is what the switcher's "which entry is me" test compares against, so it must +// be spelled the same here, in the config file and in the middleware the cmd +// layer installs — a service that spelled it two ways would appear in the +// instance's navigation and fail to recognise itself in it. +const configSection = "compare.sr.ht" + // hashedCSSRe matches the content-addressed stylesheet name so it can be served // with an immutable cache lifetime (the hash changes whenever the bytes do). var hashedCSSRe = regexp.MustCompile(`^main\.min\.[0-9a-f]{6,}\.css$`) @@ -55,43 +77,47 @@ // once at startup and is safe for concurrent use. type Server struct { authorizer authz.Authorizer reposRoot string - conf ini.File - siteName string - environment string - metaOrigin string - compareOrigin string - hubOrigin string - cssHref string - bundleHref string + // chromeSvc is the shared page frame of sr-ht-ecore: the brand, the service + // switcher, the login block and the environment banner, built once from + // config.ini and asked for a per-request chrome.Page in view (chrome.go's + // job until this service stopped carrying its own copy). + chromeSvc *chrome.Service - nav []navItem + bundleHref string + staticFileServer http.Handler } // New assembles a Server from the shared SourceHut config. It reads // [git.sr.ht] repos, [meta.sr.ht] origin and [compare.sr.ht] origin (all -// required), the [sr.ht] site-name/environment display values, and resolves the -// hashed stylesheet name by globbing the embedded static FS. A missing required -// key is a clear error, not a panic, so the cmd layer can fail startup loudly. +// required), hands the whole file to chrome.NewService — the switcher is a +// question about every [*.sr.ht] section the instance defines, not about our own +// keys — and resolves the hashed stylesheet and bundle names by globbing the +// embedded static FS. A missing required key is a clear error, not a panic, so +// the cmd layer can fail startup loudly. func New(conf ini.File, authorizer authz.Authorizer) (*Server, error) { reposRoot, ok := conf.Get("git.sr.ht", "repos") if !ok || reposRoot == "" { return nil, fmt.Errorf("web: [git.sr.ht] repos is required") } - metaOrigin := config.GetOrigin(conf, "meta.sr.ht", true) - if metaOrigin == "" { + + // The two origins are checked through the chrome that will render them + // rather than read a second time here, so the startup refusal and the links + // on the page cannot disagree about which origins this service has. + chromeSvc := chrome.NewService(conf, configSection) + if chromeSvc.MetaOrigin() == "" { return nil, fmt.Errorf("web: [meta.sr.ht] origin is required") } - compareOrigin := config.GetOrigin(conf, "compare.sr.ht", true) - if compareOrigin == "" { - return nil, fmt.Errorf("web: [compare.sr.ht] origin is required") + if chromeSvc.SelfOrigin() == "" { + return nil, fmt.Errorf("web: [%s] origin is required", configSection) } cssHref, err := resolveCSSHref() if err != nil { return nil, err } + chromeSvc.StyleHref = cssHref bundleHref, err := resolveBundleHref() if err != nil { @@ -106,17 +132,41 @@ return &Server{ authorizer: authorizer, reposRoot: reposRoot, - conf: conf, - siteName: config.GetString(conf, "sr.ht", "site-name", "sourcehut"), - environment: config.GetString(conf, "sr.ht", "environment", "production"), - metaOrigin: metaOrigin, - compareOrigin: compareOrigin, - hubOrigin: config.GetOrigin(conf, "hub.sr.ht", true), - cssHref: cssHref, + chromeSvc: chromeSvc, bundleHref: bundleHref, - nav: buildNav(conf), staticFileServer: http.StripPrefix("/static/", http.FileServer(http.FS(staticSub))), }, nil +} + +// viewData is the root value every template is executed against. +// +// chrome.Page is embedded rather than copied field by field, so the shared +// partials — "srht-nav", "srht-env-banner", "srht-repo-list" — find the fields +// they need on the dot they are handed, and a field ecore adds later arrives here +// without an edit. The page's own payload lives under Data and is reached as +// {{.Data.Something}}, which is what keeps a page from shadowing a chrome field. +type viewData struct { + chrome.Page + + // BundleHref is the hashed front-end bundle's URL. It is chrome — every page + // may load it — but it is this service's alone: no other service on the + // instance ships a diff renderer, so it stays here and not in ecore's Page. + BundleHref string + + // Data is the page's own payload. + Data any +} + +// view builds the frame for one request: the shared chrome plus a title. +// +// The username is whatever the authz cookie middleware resolved, which is "" for +// a viewer whose cookie is missing, expired or unreadable — so the nav offers +// login to exactly the viewers the handlers treat as anonymous. +func (s *Server) view(r *http.Request, title string) viewData { + return viewData{ + Page: s.chromeSvc.Page(r, title, authz.ForContext(r.Context())), + BundleHref: s.bundleHref, + } } // resolveCSSHref globs the embedded static FS for the content-addressed diff --git a/web/templates.go b/web/templates.go index f3244d8522fcf15b912a3633a40a66b22f79d01f..086567fde4a22cce77660535dca33d3455421b88 100644 --- a/web/templates.go +++ b/web/templates.go @@ -8,6 +8,7 @@ "net/http" "time" "github.com/sirupsen/logrus" + "sourcecraft.dev/bigbes/sr-ht-ecore/chrome" ) // tmplFS holds the page templates. Each page is parsed together with the shared @@ -24,22 +25,24 @@ //go:embed static var staticFS embed.FS // funcMap holds the template helpers shared by every page. -var funcMap = template.FuncMap{ - // shortsha abbreviates an object id to 8 hex chars. - "shortsha": func(s string) string { - if len(s) > 8 { - return s[:8] - } - return s - }, +// +// It starts from chrome.Funcs — so the shared partials find the helpers they +// were written against, and so "shortsha" is the instance's one abbreviation +// rule rather than this service's copy of it — and adds compare's own on top. +// Adding after is deliberate: a name may then be shadowed on purpose rather than +// by accident of map ordering. +var funcMap = func() template.FuncMap { + m := chrome.Funcs() + // date formats a commit timestamp for display. - "date": func(t time.Time) string { + m["date"] = func(t time.Time) string { return t.UTC().Format("2006-01-02 15:04 MST") - }, + } + // statusClass maps a git file-change status letter to a CSS modifier used by // the .diff-status badge (see the diff-status rules in layout.html). Anything // unrecognized falls back to the neutral "o". - "statusClass": func(s string) string { + m["statusClass"] = func(s string) string { if s == "" { return "o" } @@ -55,9 +58,10 @@ return "r" default: return "o" } - }, + } + // statusLabel spells out a status letter for the badge's tooltip. - "statusLabel": func(s string) string { + m["statusLabel"] = func(s string) string { if s == "" { return "changed" } @@ -73,17 +77,22 @@ return "renamed" default: return "changed" } - }, -} + } + + return m +}() // pageNames are the content templates; each is parsed with layout.html. var pageNames = []string{"index", "repo", "compare", "commit", "error"} -// pages maps a page name to its parsed template set (layout + that page). +// pages maps a page name to its parsed template set: the shared chrome partials +// of sr-ht-ecore, the layout, and that one page. The partials are attached to +// every set rather than to a shared one, for the same reason the layout is — +// each page defines its own "content", and one set would let the last parsed win. var pages = func() map[string]*template.Template { m := make(map[string]*template.Template, len(pageNames)) for _, name := range pageNames { - t := template.New("layout.html").Funcs(funcMap) + t := chrome.MustAttach(template.New("layout.html").Funcs(funcMap)) t = template.Must(t.ParseFS(tmplFS, "templates/layout.html", "templates/"+name+".html")) m[name] = t } @@ -121,8 +130,7 @@ // renderError renders the chrome-wrapped error page. It never recurses into // render on failure (render falls back to http.Error itself). func (s *Server) renderError(w http.ResponseWriter, r *http.Request, status int, message string) { - vd := s.chrome(r) - vd.Title = http.StatusText(status) + vd := s.view(r, http.StatusText(status)) vd.Data = errorData{ Status: status, StatusText: http.StatusText(status), diff --git a/web/templates/index.html b/web/templates/index.html index 293464c7f8bf2bd19c22d9997418c5bd36d38b4e..003d0379b80ea89722de97f86862e2a326a94da2 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -22,31 +22,13 @@
{{.Description}}
- {{end}} -You have no repositories yet.
- {{end}} + {{template "srht-repo-list" .Data.Repos}}Compare two references — branches, tags or commits — of any git repository on this instance, and review the diff file by file. diff --git a/web/templates/layout.html b/web/templates/layout.html index 1a53b79f089260a7e1cf993aaefbf716592dbb4c..28ee73a13b7c3e093ed55046e6f064ee5dda796e 100644 --- a/web/templates/layout.html +++ b/web/templates/layout.html @@ -1,3 +1,16 @@ +{{/* + The SourceHut chrome. The brand, the service switcher, the login block and the + environment banner are NOT rendered here: they come from sr-ht-ecore's shared + partials ("srht-env-banner", "srht-nav"), which every custom service on the + instance draws from one copy. The dot is this package's viewData, which embeds + chrome.Page, so the fields those partials read promote into it. + + What is left here is the document, the diff-status colours below (compare's + own, and nobody else's), and three seams: "head" and "scripts" are blocks, so + a page that needs neither still renders; "content" is a {{template}} and must + stay one — a block would give every page an empty default, which is the chrome + around a hole served 200 for the page that forgot to define it. +*/}}
@@ -5,7 +18,10 @@