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