package web import ( "context" "fmt" "log/slog" "net/http" "go.bigb.es/auxilia/scribe" "sourcecraft.dev/bigbes/sr-ht-ecore/chrome" "sourcecraft.dev/bigbes/sr-ht-dolt/beads" "sourcecraft.dev/bigbes/sr-ht-dolt/core" ) // handleReady renders the cross-database ready page: for every database this // caller may browse, the issues that are open, unblocked and not scaffolding, // grouped by database. // // It is the one page here that reads more than one database, and the three // things that keeps affordable live in beads: the head-hash gate over the app's // projection cache, the 60-second TTL, and the ceiling on how many databases one // request opens. This handler's own job is the other half — deciding which // databases the caller may see at all, and doing it before a single store is // opened. // // Visibility is enumerated with ListReposForViewer (the listing rule) and then // re-asked per database with core.Allowed/OpBrowse (the access rule). A database // the caller may not browse is simply absent: not a 403, not a count, not a // group with a hidden name — nothing on the page may hint that it exists. func (a *app) handleReady(w http.ResponseWriter, r *http.Request) { _, caller := callerOf(r.Context()) repos, err := a.cfg.Repos.ListReposForViewer(r.Context(), caller) if err != nil { slog.Error("listing databases for the ready page failed", "component", "web", scribe.Err(err)) a.fail(w, r, http.StatusInternalServerError, "") return } // The disk path never reaches beads: it is this service's arrangement of its // own storage, and the aggregation addresses a database by the identity the // cache is keyed on. The opener closes over the map, so a database that was // filtered out of the candidate list has no path to be opened by. paths := make(map[int]string, len(repos)) dbs := make([]beads.ReadyDatabase, 0, len(repos)) for _, repo := range repos { if !core.Allowed(caller, repo, a.effectiveACL(r, caller, repo), core.OpBrowse) { continue } paths[repo.ID] = repo.Path dbs = append(dbs, beads.ReadyDatabase{ ID: repo.ID, OwnerName: repo.OwnerName, Name: repo.Name, }) } open := func(ctx context.Context, d beads.ReadyDatabase) (beads.ReadySession, error) { path, ok := paths[d.ID] if !ok { return nil, fmt.Errorf("web: no store path for database %s", d.Slug()) } return a.cfg.Browse.Open(ctx, path) } data := beads.ReadyAcross(r.Context(), dbs, open, a.ready, beads.ParseReadyFilter(r.URL.Query()), timeNow()) data.Query = r.URL.Query() // A store that cannot be read is a fact about this deployment, and it belongs // in the log with its error. The page says only how many databases it could // not read: echoing a browse error into the response is how a store path and // a dolt internal end up in somebody's browser (sr-ht-dolt-7ta). for _, f := range data.Failed { slog.Warn("reading a database for the ready page failed", "component", "web", "database", f.Database.Slug(), scribe.Err(f.Err)) } view := struct { chrome.Page Data *beads.ReadyView }{ Page: a.page(r, "Ready — "+serviceName), Data: data, } a.render(w, http.StatusOK, "ready", view) }