package web
import (
"context"
"errors"
"net/http"
"github.com/go-chi/chi/v5"
"sourcecraft.dev/bigbes/sr-ht-ecore/chrome"
"sourcecraft.dev/bigbes/sr-ht-dolt/browse"
"sourcecraft.dev/bigbes/sr-ht-dolt/core"
)
// handleView renders a specialized alternative view of a repository. The view
// is selected by the {view} URL slug and must both exist in a.views and Apply
// to the tables at the resolved ref; otherwise the request is a 404 (the view
// simply does not exist for this repo). The generic table browser remains
// reachable via the tree/table routes regardless.
//
// The ref is taken from ?ref= (default: the repo's default branch). The view's
// own Template() is rendered with a fixed envelope: the chrome, the repo, the
// ref and branch list, the applicable view tabs, and the opaque value the view
// produced from Build (as .Data).
func (a *app) handleView(w http.ResponseWriter, r *http.Request) {
repo, _, _, ok := a.loadRepoForBrowse(w, r)
if !ok {
return
}
sess, ok := a.openBrowse(w, r, repo)
if !ok {
return
}
defer sess.Close()
ref := r.URL.Query().Get("ref")
if ref == "" {
branches, err := sess.Branches(r.Context())
if err != nil {
http.Error(w, "failed to list branches", http.StatusInternalServerError)
return
}
ref = browse.DefaultBranch(branches)
}
tables, err := sess.Tables(r.Context(), ref)
if err != nil {
if errors.Is(err, browse.ErrRefNotFound) {
a.notFound(w, r)
return
}
http.Error(w, "failed to read tables", http.StatusInternalServerError)
return
}
// Find the requested view; an unknown slug or a view that does not fingerprint
// these tables is reported as not found — the view does not exist here.
slug := chi.URLParam(r, "view")
var view View
for _, v := range a.views {
if v.Name() == slug {
view = v
break
}
}
if view == nil || !view.Applies(tables) {
a.notFound(w, r)
return
}
data, err := view.Build(r.Context(), sess, repo, ref, r.URL.Query())
if err != nil {
http.Error(w, "failed to build view", http.StatusInternalServerError)
return
}
// Re-list branches for the chrome/tab bar. Cheap and keeps the ref selector
// consistent whether or not ?ref= was supplied.
branches, _ := sess.Branches(r.Context())
envelope := struct {
chrome.Page
Repo *core.Repo
Ref string
Branches []browse.Branch
Views []View
Head *browse.CommitInfo
Data any
// Links renders the issue ids inside stored prose as links to the database
// that owns them. It is built by the one rendering that needs it — the
// beads detail pane — and is nil for every other page here, because
// building it opens a store per database the caller may browse. A nil one
// still renders text; see beadLinks.Text.
Links *beadLinks
// Memories renders a memory's stored markdown, with its [[slug]]
// references and the issue ids in its prose linked to the databases that
// hold them. It is built by the memory view alone, for the reason Links is
// built by the detail pane alone: it opens a store per database this
// caller may browse. A nil one still renders the markdown, with every
// reference left as text; see memoryLinks.Body.
Memories *memoryLinks
}{
Page: a.page(r, view.Label()+" — "+repo.OwnerName+"/"+repo.Name),
Repo: repo,
Ref: ref,
Branches: branches,
Views: applicableViews(a.views, tables),
Head: headCommit(r.Context(), sess, ref),
Data: data,
Links: a.beadCrossLinks(r, view, repo, data),
Memories: a.memoryCrossLinks(r, view, repo),
}
a.render(w, http.StatusOK, pageName(view.Template()), envelope)
}
// headCommit reads the head commit of ref for the envelope's freshness line: a
// board rendered from a store that stopped receiving pushes yesterday is
// otherwise indistinguishable from a current one.
//
// It returns nil rather than an error, and that is the whole contract. A
// database with no commits, or a Log that fails, must still render the view —
// this is decoration on top of an answer, and it may never be the reason a
// reader gets a 500 instead of a board. The partial renders nothing for nil.
func headCommit(ctx context.Context, sess BrowseSession, ref string) *browse.CommitInfo {
commits, _, err := sess.Log(ctx, ref, "", 1)
if err != nil || len(commits) == 0 {
return nil
}
return &commits[0]
}