summaryrefslogtreecommitdiff
path: root/vrsite/serve.go
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-02 12:36:54 -0300
committerGabriel Schneider <[email protected]>2026-08-02 17:37:58 -0300
commite16f78b255bcf46d8c6f9a4b9e76dd7a3817edeb (patch)
treede0db0c369522b5bf92ff32a8351de2224c5c9dc /vrsite/serve.go
parent151ea12251ed876aa87bb486da1bc337e25369e9 (diff)
downloadnotevi-e16f78b255bcf46d8c6f9a4b9e76dd7a3817edeb.tar.gz
notevi-e16f78b255bcf46d8c6f9a4b9e76dd7a3817edeb.zip
Diffstat (limited to 'vrsite/serve.go')
-rw-r--r--vrsite/serve.go88
1 files changed, 43 insertions, 45 deletions
diff --git a/vrsite/serve.go b/vrsite/serve.go
index 31217af..746abac 100644
--- a/vrsite/serve.go
+++ b/vrsite/serve.go
@@ -3,9 +3,9 @@
// The server owns nothing. Three rules keep it that way, and app_test.go holds
// each of them:
//
-// - The log is the only thing written. Notes typed in the browser are
-// appended to it exactly as vr appends them; the repository is only ever
-// read (see jj.go), and no file of the app's own is ever created.
+// - The log sidecar is the only thing written. Notes typed in the browser are
+// appended to it exactly as vr appends them; project history and the main
+// working copy are only ever read (see jj.go).
// - Memory is a cache, never a source. The model, the per-change
// projections, file lists and rendered pages are all derived from the log
// and the repo, so a restart changes nothing anyone can see.
@@ -27,7 +27,6 @@ import (
"html/template"
"net/http"
"net/url"
- "os"
"strconv"
"strings"
"sync"
@@ -43,18 +42,18 @@ const liveWindow = 5 * time.Minute
const renderCacheMax = 64
type server struct {
- logPath, repo, title, user, session string
- tpl *template.Template
- hl *highlighter
- theme, light *Theme
- themes *themeSet
- tx *transcripts
+ log logStore
+ repo, title, user, session, base string
+ tpl *template.Template
+ hl *highlighter
+ theme, light *Theme
+ themes *themeSet
+ tx *transcripts
mu sync.Mutex
site *Site
entries []Entry
- off int64
- state logState
+ state string
gen int
def string // resolved change the log points at
models map[string]*Site // scope key -> the log projected onto it
@@ -65,12 +64,17 @@ type server struct {
// scopeAll is the key of the unscoped model — the whole log, every change.
const scopeAll = "*"
-func newServer(logPath, repo, title, user string, theme, light *Theme, themes *themeSet) *server {
+func newServer(log logStore, repo, title, user string, theme, light *Theme, themes *themeSet) *server {
+ return newServerAt(log, repo, title, user, "/", theme, light, themes)
+}
+
+func newServerAt(log logStore, repo, title, user, base string, theme, light *Theme, themes *themeSet) *server {
if themes == nil {
themes = &themeSet{}
}
+ base = (&page{Root: base}).liveRoot()
hl := newHighlighter(theme, light)
- sv := &server{logPath: logPath, repo: repo, title: title, user: user,
+ sv := &server{log: log, repo: repo, title: title, user: user, base: base,
session: "web:" + user, theme: theme, light: light, themes: themes,
tpl: newTemplates(hl), models: map[string]*Site{}, trees: map[string][]*File{},
rendered: map[string]*File{}, tx: newTranscripts(defaultTranscriptRoots())}
@@ -79,11 +83,10 @@ func newServer(logPath, repo, title, user string, theme, light *Theme, themes *t
return sv
}
-// reload re-reads the log if it changed. It appends from the last offset when
-// the file only grew — the common case, since vr and the web app both append.
+// reload re-reads the log when the sidecar commit id changes.
func (sv *server) reload(force bool) {
- st, err := statLog(sv.logPath)
- if err == nil && st == sv.state && !force {
+ state, err := sv.log.Generation()
+ if err == nil && state == sv.state && !force {
return
}
if err != nil {
@@ -92,22 +95,17 @@ func (sv *server) reload(force bool) {
}
return
}
- if st.size < sv.off {
- sv.off, sv.entries = 0, nil // truncated or rewritten
- }
- f, err := os.Open(sv.logPath)
+ b, err := sv.log.Read()
if err != nil {
+ if sv.site == nil {
+ sv.state = state
+ sv.site = sv.dress(buildModel(sv.title, nil, ""))
+ sv.gen++
+ }
return
}
- defer f.Close()
- buf := make([]byte, st.size-sv.off)
- n, _ := f.ReadAt(buf, sv.off)
- buf = buf[:n]
- if i := bytes.LastIndexByte(buf, '\n'); i >= 0 { // ignore a partial last line
- sv.off += int64(i + 1)
- sv.entries = append(sv.entries, parseLog(buf[:i+1])...)
- }
- sv.state = st
+ sv.entries = parseLog(b)
+ sv.state = state
sv.site = sv.dress(buildModel(sv.title, sv.entries, ""))
sv.gen++
sv.def = ""
@@ -257,8 +255,9 @@ func (sv *server) fileAt(site *Site, gen int, commit, change, path string) *File
func (sv *server) newPage(r *http.Request, kind, title string) (*page, *Site, error) {
full, _, def := sv.snap()
q := r.URL.Query()
- p := &page{Site: full, Kind: kind, Title: title, Root: "/", Live: true,
- User: sv.user, ChangeID: def, Default: def, Path: r.URL.Path, Scope: "change"}
+ path := sv.base + strings.TrimPrefix(r.URL.Path, "/")
+ p := &page{Site: full, Kind: kind, Title: title, Root: sv.base, Live: true,
+ User: sv.user, ChangeID: def, Default: def, Path: path, Scope: "change"}
rev := strings.TrimSpace(q.Get("rev"))
target := rev
if target == "" {
@@ -540,7 +539,7 @@ func (sv *server) addNote(w http.ResponseWriter, r *http.Request) {
}
target = fallback
}
- change, err := resolveChange(sv.repo, target)
+ change, commit, err := resolveRevision(sv.repo, target)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@@ -556,14 +555,13 @@ func (sv *server) addNote(w http.ResponseWriter, r *http.Request) {
e := Entry{
Time: time.Now().Format(time.RFC3339), Op: "note",
Session: sv.session, Agent: sv.user, Dir: sv.repo,
- Rev: rev, Change: change, File: strings.TrimSpace(r.FormValue("file")),
+ Rev: rev, Change: change, Commit: commit, File: strings.TrimSpace(r.FormValue("file")),
Start: start, End: end, ReplyTo: replyTo,
Type: strings.TrimSpace(r.FormValue("kind")), Text: text,
}
sv.mu.Lock()
- e.NoteID = nextNoteID(sv.entries, sv.session)
- err = appendEntry(sv.logPath, e)
+ err = appendEntry(sv.log, &e)
if err == nil {
sv.reload(true)
}
@@ -583,12 +581,12 @@ func (sv *server) addNote(w http.ResponseWriter, r *http.Request) {
if !htmxReq(r) {
back := r.FormValue("back")
if back == "" {
- back = "/notes"
+ back = sv.base + "notes"
}
http.Redirect(w, r, back, http.StatusSeeOther)
return
}
- p := &page{Site: site, Kind: "file", Root: "/", Live: true, User: sv.user, Facets: site.Facets()}
+ p := &page{Site: site, Kind: "file", Root: sv.base, Live: true, User: sv.user, Facets: site.Facets()}
if e.File != "" {
p.File = site.Files[e.File]
}
@@ -609,7 +607,7 @@ func (sv *server) activity(w http.ResponseWriter, r *http.Request) {
if !compact {
limit = 20
}
- p := &page{Site: site, Live: true, Compact: compact, Window: "5 min",
+ p := &page{Site: site, Root: sv.base, Live: true, Compact: compact, Window: "5 min",
Sessions: site.Sessions(time.Now(), liveWindow, limit)}
sv.frag(w, "activity", p)
}
@@ -638,7 +636,7 @@ const defaultLimit = 50
func (sv *server) jjView(r *http.Request) *JJView {
q := r.URL.Query()
- v := &JJView{Kind: q.Get("kind"), Revset: strings.TrimSpace(q.Get("revset")),
+ v := &JJView{Root: sv.base, Kind: q.Get("kind"), Revset: strings.TrimSpace(q.Get("revset")),
AtOp: strings.TrimSpace(q.Get("at_op")), Limit: atoiDefault(q.Get("limit"), defaultLimit)}
if v.Limit < 1 || v.Limit > 1000 {
v.Limit = defaultLimit
@@ -678,7 +676,7 @@ func (sv *server) jjLog(w http.ResponseWriter, r *http.Request) {
func (sv *server) jjOp(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
- v := &JJView{Kind: "op", Limit: atoiDefault(q.Get("limit"), defaultLimit)}
+ v := &JJView{Root: sv.base, Kind: "op", Limit: atoiDefault(q.Get("limit"), defaultLimit)}
if v.Limit < 1 || v.Limit > 1000 {
v.Limit = defaultLimit
}
@@ -751,15 +749,15 @@ func (sv *server) asset(w http.ResponseWriter, r *http.Request) {
w.Write(b)
}
-func serve(addr, logPath, repo, title, user string, roots []string, theme, light *Theme, themes *themeSet) {
- sv := newServer(logPath, repo, title, user, theme, light, themes)
+func serve(addr string, log logStore, repo, title, user string, roots []string, theme, light *Theme, themes *themeSet) {
+ sv := newServer(log, repo, title, user, theme, light, themes)
sv.tx = newTranscripts(roots)
site, _, def := sv.snap()
fmt.Printf("vrsite: http://%s — %d notes, %d agents, repo %s at %s\n",
addr, len(site.Notes), len(site.Agents), repo, short(def))
fmt.Printf("vrsite: themes — %s dark, %s light, %d more to pick from (%d zed, %d helix)\n",
theme.Name, light.Name, len(sv.themes.List), sv.themes.count("zed"), sv.themes.count("helix"))
- fmt.Printf("vrsite: comments are appended to %s; the repo is never written to\n", logPath)
+ fmt.Printf("vrsite: comments are appended to %s; the project working copy is never written to\n", log)
fmt.Printf("vrsite: session transcripts read from %s\n", strings.Join(sv.tx.roots, ", "))
if err := http.ListenAndServe(addr, sv.handler()); err != nil {
fatal("%v", err)