diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-02 12:36:54 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-02 17:37:58 -0300 |
| commit | e16f78b255bcf46d8c6f9a4b9e76dd7a3817edeb (patch) | |
| tree | de0db0c369522b5bf92ff32a8351de2224c5c9dc /vrsite/serve.go | |
| parent | 151ea12251ed876aa87bb486da1bc337e25369e9 (diff) | |
| download | notevi-e16f78b255bcf46d8c6f9a4b9e76dd7a3817edeb.tar.gz notevi-e16f78b255bcf46d8c6f9a4b9e76dd7a3817edeb.zip | |
Diffstat (limited to 'vrsite/serve.go')
| -rw-r--r-- | vrsite/serve.go | 88 |
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) |
