deps: tidy after the uplift
chrome: the head links from ecore, and no shipped favicon layout.html hardcoded <link rel="icon" href="/static/logo.svg">, and web/static/logo.svg was this service's copy of the brand circle chrome now inlines as a data: URI. Both go: the head renders srht-head-links, which emits the stylesheet and the icon guarded on emptiness, and the icon costs no request and cannot 404. The static tree is two hashed artefacts now, so the arm of TestStaticBundleAndCSS that asserted an hour-cacheable unhashed asset has nothing left to ask about; TestFaviconIsInline takes its place and also pins that the data: URI reaches the page rather than #ZgotmplZ.
chimw: the request line in the journal, HEAD routes, and a 405 page Three things at once, all of them the chi half of the shared middleware. RequestLogger replaces chi's Logger, which wrote an unstructured line to stdout — the highest-volume record this daemon emits and the only one not beside the rest on stderr. RequestID goes above it so the request line and a panic report share an id, Recoverer below it so its own report goes through the entry instead of to stdout, and /healthz is skipped. GetHead registers every read route under HEAD as well. Until now `curl -I` and every uptime probe were answered 405 plus a kilobyte of rendered error page by pages whose whole job is to be cheap to ask about. RenderRefusals installs both routing failures against renderError; this service had the 404 alone and left the 405 to net/http's plain text. The new test that pins it also corrects what TestUnsafeMethodRefused claimed: the group's middleware runs before either refusal, so an unsafe method on a GET-only path is a 403 from the same-origin guard, not a 405.
instconf: the origin ladder and the one-pass config check validateConfig kept its shape and lost its body. instconf.Require is the same decision — every missing key in one record, one exit, so an operator edits config.ini once instead of restarting per gap — and firstConfigured was this service's copy of the four-key API-origin ladder core-go panics at the end of. NeedAny is handed APIOriginKeys() rather than the four names written out, so the startup check and InternalAPIOrigin cannot drift apart the way two literal lists eventually do.
logging: the instance's log policy instead of forty local lines initLogging held compare's own copy of the level choice, the colour probe and the mask list, and the mask list was the drifted one: three keys and a four-word pattern, against an instance whose services between them redact ten. logging.Defaults answers all of it, and the handler stays here — scribe is auxilia's and does not belong in the library every service links for its chrome. isTerminal goes to logging.ColorEnabled, which also honours NO_COLOR, and the level now reads $LOG_LEVEL after -d. Defaults(nil, "") because the handler is installed before LoadConfig: a daemon that became verbose only after startup is silent for the window -d was passed to watch.
login: ecore's cookie decoder, which validates the name ours did not authz/identity.go was the fifth copy of a decode the instance has one of, and it was one of the two that validated nothing: whatever name a sealed payload carried went on to the GraphQL viewer field, the chrome's nav and every log line the request produced. login.ValidName refuses it, and there is no spelling of the API that turns the check off. Gone with it: CookieName, UsernameFromRequest, Middleware, ForContext and the ctxKey, plus identity_test.go, whose round-trip, tilde-stripping and garbage-cookie cases are ecore's tests now. The Middleware becomes login.Optional() — this service refuses nobody, git.sr.ht decides what an anonymous viewer may see — and the default validator is kept rather than core.ValidOwner, which is lowercase-only and would log a real account out of compare alone. The one behaviour change a viewer could notice: a cookie whose name carries a '/', a control byte or a non-ASCII letter now reads as anonymous instead of as that name.
deps: the ecore of the third uplift, and auxilia's first tag sr-ht-ecore grew login, logging, instconf and chimw, which between them replace this service's identity middleware, its logging setup and its config validation. auxilia is a real v0.7.0 rather than a pseudo-version now, so the requirement can name a tag.
deps: auxilia whose scribe.Err reads the whole error chain Until now scribe.Err type-asserted the outermost error for slog.LogValuer, so a culpa error under a single fmt.Errorf logged as a flat string and lost its code, hint and stacktrace with no warning. It walks the chain now, which is what makes the culpa wrapping in this service visible in the journal.
log: slog through auxilia's scribe, not logrus This was the last service on the instance still logging through logrus, and the ecore bump made it a correctness question rather than a consistency one: the recovery middleware reports a panic through slog's DEFAULT logger, so without a SetDefault the one record carrying a stack trace would have gone to Go's plain handler while every other line went through logrus. initLogging installs a scribe tint handler on stderr — source on, colour only when stderr is a terminal, and the mask rules for token, cookie and authorization. It reads -d out of the argument vector rather than waiting for core-go's parse, because a daemon that only became verbose after it had finished starting would be silent for exactly the part of its life an operator passes -d to watch. Terminal detection is a stdlib Stat rather than golang.org/x/term, which would be a new dependency for one predicate. The startup config check keeps its shape deliberately: it still accumulates every missing key and reports them in ONE record before ONE exit, so an operator fixes the config in a single pass. The keys go in as a slice attribute rather than a joined string, so the structured sinks keep them as a list. culpa goes in at the one boundary where it pays: the errors web.New returns, which are the only errors this package produces and all of which arrive at that single startup record. Each carries a hint naming the config key or the build step that fixes it, and scribe.Err unfolds message, code, hint and stacktrace into fields of their own. The failing request path logs the same way, with ErrorContext so a cancelled request reads as cancelled rather than as an unexplained 500. TestPanicIsAnErrorPage covers the wiring end to end through Register: a panicking authorizer yields the chrome-wrapped 500, the panic value does not reach the viewer, and the report reaches slog's default logger.
deps: sr-ht-ecore whose middleware reports panics through slog The panic report is structured now (method, path, panic, stack) and goes through slog's default logger, so a service that never calls slog.SetDefault gets those reports in Go's plain stderr handler. The next commit is what makes this one land somewhere useful.
readme: name the ecore packages the web tier draws from
cmd: ecoretest keys in the startup smoke test The synthesized config.ini takes ecore's fixed keyset and origins rather than generating a keypair per run; a constant cannot be malformed by accident, which matters for a subprocess whose only way of complaining is a log.Fatal. Also notes the three middlewares Register now installs inside its own group, and why chi's Recoverer stays out here.
authz: bootstrap the tests from ecoretest Drops the fernet key generation and the ed25519 seed both test files were doing by hand, and the ini.File assembled around them. The keys are constants now, which is what makes InitCrypto idempotent across the two packages of this service that both call it.
web: draw assets, pages, csrf and the middleware from ecore Deletes this package's copies of five things the instance now shares. assets replaces hashedCSSRe, hashedBundleRe, resolveCSSHref, resolveBundleHref and the bare StripPrefix(FileServer) route. That route was a fix and not only a dedupe: an http.FileServer answers a directory with a listing, so /static/ published the whole inventory of the binary — every vendored artefact and the hashed names that fingerprint the build — as a public, hour-cacheable page. It also wrote the cache directives onto the header map before delegating, where a panic later would have carried public, max-age=3600 onto a viewer's error page, and left the Vary the private-cache policy sets, which is enough to stop any shared cache from ever reusing an asset whose name was hashed for that purpose. assets.Handler refuses the listing, stamps the policy on the bytes rather than on the map, and drops the Vary per asset. pages replaces pageNames, the package-level template map, render and errorData. Pages are discovered from the embedded tree instead of listed by hand, so templates/x.html is now the whole registration of a page, and a page that defines no content block is refused at startup rather than served as chrome around a hole with a 200 — neither of which this service checked before. Render answers the response itself; the error it returns is a log line and never reaches fail. The local error.html goes with them: ecore ships the page and the srht-error partial. renderError stays here, because building this service's view struct is this service's business, and it now passes for every status but 400 so a repository the viewer may not see and one that never existed produce the same sentence. csrf.Require and the middleware group are new rather than replacements: compare has no POST today, so the guard covers the day somebody adds one, and PrivateCache states the policy every per-viewer page here was serving without. BundleHref leaves viewData for chrome.Service.Assets, keyed bundle.js and read through an emptiness guard, next to StyleHref where the other hashed artefact already lived. The date helper goes to chrome's reltime and abstime: listings show 3 days ago and hover to the exact stamp.
deps: sr-ht-ecore with the web-tier packages Brings in assets, pages, csrf, middleware and ecoretest, plus the chrome that grew Service.Assets — the slot three services had each added a field of their own for.
web: draw the chrome from sr-ht-ecore The nav/service-switcher, the brand, the login block and the environment banner were a copy of code every custom service on this instance carries. The copies drifted, so they now come from one place: sourcecraft.dev/bigbes/sr-ht-ecore/chrome. web/chrome.go is gone. New builds one chrome.Service from the shared config.ini and hands it the discovered stylesheet; view() asks it for a chrome.Page per request, which viewData embeds rather than copies, so a field ecore adds later arrives here without an edit. The layout invokes "srht-env-banner" and "srht-nav" instead of the markup it used to spell out, the landing's repository listing goes through "srht-repo-list", and the FuncMap starts from chrome.Funcs() with the local shortsha deleted. Two ecore policies differ from what this service did and are adopted as the instance's: the brand label is the config section's short name rather than a literal "compare", and the origins are trimmed of a trailing slash. What stays local is what is compare's alone: the bundle href, the diff-status colours, and the container-fluid the two diff views ask for. Tests move to testify; the nav ordering/exclusion test goes with the code it tested, and a test that the layout invokes the chrome at all replaces it.
web: git.sr.ht-style dashboard and unified nav brand Adopt the family look the dolt service already has. The nav brand becomes circle icon + site name + red service label (dropping the hub-origin variant), matching every other service on the instance. The logged-in index turns into the two-column dashboard: a sidebar with the service blurb and the jump form, and the repositories as shared-theme event-list cards — name, visibility as small muted text (non-public only, lowercase), description underneath. The big in-page h2 lives on only for anonymous visitors, where the nav has no user context yet.
fix(apk): keep -modcacherw when overriding GOFLAGS The GOFLAGS="-trimpath" passed to make clobbers abuild's default GOFLAGS, dropping -modcacherw: the module cache then extracts read-only and the CI cache tarball made from it fails to unpack on the next build (mkdir into 0555 dirs, permission denied — warm build #266). The poisoned gomod tarball was deleted by ad-hoc job #268.
ci: cache Go module and build dirs via cacher Same scheme as sr-ht-dolt (dbd8638 there): restore ~/go/pkg/mod and ~/.cache/go-build from the Garage docker-cache bucket before abuild and seed them back after, keyed by go.sum via cacher (go.bigb.es/cacher). GOCACHE/GOMODCACHE are re-pinned inside build() because abuild redirects them into its throwaway $tmpdir and an upstream typo in abuild.in clobbers any pre-set GOMODCACHE with GOCACHE's value.
ci: commit the apk build, packaging and mirror-trigger files These three were written but never committed, so the whole publishing chain was dead from its first link: with no .build.yml on sourcecraft there is none on the git.srht.bigb.es mirror either, so the push hook has no manifest to submit, no build ever ran, and compare.sr.ht has never appeared in the apk index at repo.bigb.es — which today holds spec.sr.ht alone. The code itself was mirroring fine, which is what made this hard to see: the sourcehut side sits on the current commit, so everything looks configured until you ask git which of these files it actually knows about. .sourcecraft/webhooks.yaml is in the same state and has the same consequence one level up: the gitsync webhook was never registered, so the mirror has been riding its hourly safety-net poll rather than updating on push. The build secret apk-ci-s3 the publish task needs now exists on the account, so nothing else blocks the first build.