package web import ( "context" "log/slog" "net/http" "net/url" "go.bigb.es/auxilia/scribe" "sourcecraft.dev/bigbes/sr-ht-dolt/beads" "sourcecraft.dev/bigbes/sr-ht-dolt/browse" "sourcecraft.dev/bigbes/sr-ht-dolt/core" ) // memoryView renders the memories `bd remember` writes: config rows keyed // kv.memory., each with the revision its value was last written at. The // row itself is (key, value) and carries no timestamp, so the date comes out of // the history — see beads.BuildMemories for the walk that finds it. // // The reading is not here. It lives in the beads package, which the MCP // surface's list_memories shares; this type is only the View adapter — slug, // label, template, and the hand-off of the request's ref and query. type memoryView struct { // links is the cross-database link index's cache, held for the same reasons // beadsView holds one (see web/beads.go): the view is this rendering's one // piece of per-process state, a zero value is a working cache, and what is // cached is a fact about a database rather than about who may read it. // // It is a second cache and not the board's, because it is a second page: the // two are read at different times and neither should be able to expire the // other's entries. What they share is the projection's shape, which is why // they share its type. links beads.PrefixCache } func (*memoryView) Name() string { return "memory" } func (*memoryView) Label() string { return "Memory" } func (*memoryView) Template() string { return "memory.html" } // Applies is the beads fingerprint plus a config table carrying key and value; // see beads.AppliesMemories. Applies sees table shapes and never rows, so a // tracker that has never had a memory written into it still gets the tab and // renders an empty state — the same contract Milestones has. func (*memoryView) Applies(tables []browse.TableInfo) bool { return beads.AppliesMemories(tables) } // Build reads the memories and dates them from the history. The result is a // *beads.MemoryView, handed to memory.html as its .Data. // // The clock is passed in rather than read inside the projection: staleness is // the one thing on this page that depends on when it was rendered, and timeNow // is the same swappable clock the freshness line uses, so a test pins both at // once. func (*memoryView) Build(ctx context.Context, sess BrowseSession, _ *core.Repo, ref string, query url.Values) (any, error) { data, err := beads.BuildMemories(ctx, sess, ref, query, timeNow()) if err != nil { return nil, err } return data, nil } // memoryCrossLinks builds the link index the memory bodies are rendered against, // and returns nil for every page that is not the memory view. A nil one still // renders the markdown; what it costs is the links, which is what an unreadable // listing may cost and no more (see linkableDatabases). // // It is asked for by this page rather than built into the envelope because // building it opens a store per database the caller may browse — the same reason // the issue links are asked for by the detail pane alone. // // The index answers both halves of what a memory body cites: the [[slug]] // references and the issue ids in the prose. They come out of one read of one // table per database, which is why there is one index here and not two. func (a *app) memoryCrossLinks(r *http.Request, view View, repo *core.Repo) *memoryLinks { mv, ok := view.(*memoryView) if !ok { return nil } dbs, open, ok := a.linkableDatabases(r, repo) if !ok { return nil } index := beads.PrefixesAcross(r.Context(), dbs, open, &mv.links, timeNow()) // A store that cannot be read is a fact about this deployment and belongs in // the log with its error. The page says nothing at all: the references that // database holds simply stop resolving, exactly as they do for a database this // caller may not browse. for _, f := range index.Failed { slog.Warn("reading a database for the memory link index failed", "component", "web", "database", f.Database.Slug(), scribe.Err(f.Err)) } return &memoryLinks{index: index} }