diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-02 23:43:08 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-03 09:54:39 -0300 |
| commit | 5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e (patch) | |
| tree | f12f4b700fff4ff6f7e818d2a542672ba0b8d76e /web/templates | |
| parent | a9263daee9413c5eff3c1f1bebcd224442fd8685 (diff) | |
| download | notevi-5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e.tar.gz notevi-5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e.zip | |
rebrand to notevi: one CLI over the jj sidecar
vr and vrsite become a single binary. vrsite/ folds into a web package in
one module (0x4200.cafe/notevi); "notevi web" serves and exports exactly
what vrsite did, and "notevi read/grep/note/query" is unchanged.
The sidecar is renamed with it: notevi_log, notevi-log.jsonl, and the
description "private: notevi log". The pre-rebrand names are still
recognized, so an old repository opens and reads; it is renamed in place
on the first write, or up front with "notevi migrate DIR...".
That rename cannot be a single mv inside jj run. jj only auto-tracks a
*new* file in the run working copy below a size limit it does not take
from the command line, so writing a whole log under a name the change has
never held is silently dropped while jj reports success. ensureLogFile
creates the file empty first and lets every later byte be a modification
of a tracked file, which snapshots at any size; that also fixes the same
latent bug when importing a large legacy vr-log.jsonl.
Adds a bem-te-vi mark (favicon and nav brand) and a README.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Diffstat (limited to 'web/templates')
| -rw-r--r-- | web/templates/frag-activity.tmpl | 36 | ||||
| -rw-r--r-- | web/templates/frag-change.tmpl | 41 | ||||
| -rw-r--r-- | web/templates/frag-jj.tmpl | 33 | ||||
| -rw-r--r-- | web/templates/frag-note.tmpl | 105 | ||||
| -rw-r--r-- | web/templates/layout.tmpl | 94 | ||||
| -rw-r--r-- | web/templates/page-change.tmpl | 38 | ||||
| -rw-r--r-- | web/templates/page-file.tmpl | 20 | ||||
| -rw-r--r-- | web/templates/page-files.tmpl | 13 | ||||
| -rw-r--r-- | web/templates/page-index.tmpl | 56 | ||||
| -rw-r--r-- | web/templates/page-jj.tmpl | 25 | ||||
| -rw-r--r-- | web/templates/page-note.tmpl | 54 | ||||
| -rw-r--r-- | web/templates/page-notes.tmpl | 52 | ||||
| -rw-r--r-- | web/templates/page-session.tmpl | 70 | ||||
| -rw-r--r-- | web/templates/projects.tmpl | 113 |
14 files changed, 750 insertions, 0 deletions
diff --git a/web/templates/frag-activity.tmpl b/web/templates/frag-activity.tmpl new file mode 100644 index 0000000..071c556 --- /dev/null +++ b/web/templates/frag-activity.tmpl @@ -0,0 +1,36 @@ +{{/* Who is reading the codebase right now, straight off the log tail. */}} +{{define "activity"}} +{{if .Compact}} + <div class="actlist"> + {{range .Sessions}} + <div class="act{{if .Live}} on{{end}}" title="{{.Agent}} · {{.Model}} · {{.Dir}}"> + {{if and $.Live .ID}}<a class="chip am{{.Bit}}" href="{{$.Href "session" .ID}}">{{.Short}}</a> + {{else}}<span class="chip am{{.Bit}}">{{.Short}}</span>{{end}} + <span class="what">{{.LastOp}}</span> + <span class="when">{{.Ago}}</span> + </div> + {{end}} + {{if not .Sessions}}<p class="meta hint">nothing logged yet</p>{{end}} + </div> +{{else}} + <table class="acttable"> + <thead><tr><th>agent</th><th>model</th><th>session</th><th>last op</th><th>when</th><th>r/g/n</th><th>dir</th></tr></thead> + <tbody> + {{range .Sessions}} + <tr class="{{if .Live}}on{{end}}"> + <td><span class="chip am{{.Bit}}">{{.Short}}</span>{{if .Live}}<span class="pulse" title="active">●</span>{{end}}</td> + <td>{{.Model}}{{if .Reason}}<span class="meta">/{{.Reason}}</span>{{end}}</td> + <td>{{if and $.Live .ID}}<a href="{{$.Href "session" .ID}}" title="{{.ID}}"><code>{{short .ID}}</code></a> + {{else}}<code title="{{.ID}}">{{short .ID}}</code>{{end}}</td> + <td class="what">{{.LastOp}}</td> + <td>{{.Ago}}</td> + <td class="meta">{{.Reads}}/{{.Greps}}/{{.Notes}}</td> + <td class="meta dir" title="{{.Dir}}">{{base .Dir}}</td> + </tr> + {{end}} + </tbody> + </table> + {{if not .Sessions}}<p class="meta hint">nothing logged yet</p>{{end}} + <p class="meta">live = active in the last {{.Window}}</p> +{{end}} +{{end}} diff --git a/web/templates/frag-change.tmpl b/web/templates/frag-change.tmpl new file mode 100644 index 0000000..b33d746 --- /dev/null +++ b/web/templates/frag-change.tmpl @@ -0,0 +1,41 @@ +{{/* The change everything on screen is read at, and how wide the view is. + Every page carries it, because every read, note and file listing below + is relative to it. */}} +{{define "changebar"}} +<section class="changebar{{if .Rev}} away{{end}}" aria-label="current change"> + <div class="cid"> + <span class="lbl">change</span> + {{if .Live}}<a href="{{.Href "change" .ChangeID}}" title="{{.ChangeID}}"><code>{{short .ChangeID}}</code></a> + {{else}}<code title="{{.ChangeID}}">{{short .ChangeID}}</code>{{end}} + {{with .ChangeInfo}}{{if .Flags}}<span class="flags">{{.Flags}}</span>{{end}}{{end}} + </div> + {{with .ChangeInfo}} + <p class="desc" title="{{.Desc}}">{{if .Desc}}{{.Desc}}{{else}}(no description){{end}}</p> + {{end}} + + <p class="counts"> + {{if eq .Scope "all"}} + <b>{{.Site.NotesTotal}}</b> notes across every change{{if .Here}} · {{.Here}} here{{end}} + {{else if .ScopeEmpty}} + nothing recorded at this change — showing all <b>{{.Site.NotesTotal}}</b> + {{else}} + <b>{{.Here}}</b> notes here{{if .Elsewhere}} · <span class="away">{{.Elsewhere}} elsewhere</span>{{end}} + {{end}} + </p> + + {{if .Live}} + <div class="scope" role="group" aria-label="what this view covers"> + <a href="{{.ScopeHref "change"}}"{{if eq .Scope "change"}} class="on" aria-current="true"{{end}} + title="only what was recorded at this change">this change</a> + <a href="{{.ScopeHref "all"}}"{{if eq .Scope "all"}} class="on" aria-current="true"{{end}} + title="every change in the log">all</a> + </div> + <form class="revjump" method="get" action="{{.JumpPath}}"> + <input type="search" name="rev" value="{{.Rev}}" placeholder="@, id, revset…" + aria-label="read at another change" autocomplete="off" spellcheck="false"> + {{if eq .Scope "all"}}<input type="hidden" name="scope" value="all">{{end}} + </form> + {{if .Rev}}<p class="back"><a href="{{.BackHref}}">← back to <code>{{short .Default}}</code>, where the log points</a></p>{{end}} + {{end}} +</section> +{{end}} diff --git a/web/templates/frag-jj.tmpl b/web/templates/frag-jj.tmpl new file mode 100644 index 0000000..61040dc --- /dev/null +++ b/web/templates/frag-jj.tmpl @@ -0,0 +1,33 @@ +{{/* jj query results. Live-only, so routes are absolute. */}} +{{define "jjrows"}} +{{if .Err}}<p class="err">{{.Err}}</p>{{end}} +{{if .Rows}} +<div class="jjlog"> + {{range .Rows}} + {{if .Blank}}<div class="jjrow blank"><span class="graph">{{.Graph}}</span></div> + {{else if .Op}} + <div class="jjrow"> + <span class="graph">{{.Graph}}</span> + <a class="id" href="{{$.Root}}jj?at_op={{.ID}}" title="run the commit log as of this operation">{{.Short}}</a> + <span class="when">{{.When}}</span> + <span class="who">{{.Author}}</span> + <span class="desc">{{.Desc}}</span> + <span class="flags">{{.End}}</span> + </div> + {{else}} + <div class="jjrow"> + <span class="graph">{{.Graph}}</span> + <a class="id" href="{{$.Root}}change/{{.ID}}" title="{{.ID}}">{{.Short}}</a> + <code class="commit">{{.CommitShort}}</code> + <span class="when">{{.When}}</span> + <span class="who">{{.Author}}</span> + {{if .Refs}}<span class="refs">{{.Refs}}</span>{{end}} + {{if .Flags}}<span class="flags">{{.Flags}}</span>{{end}} + <a class="desc" href="{{$.Root}}files?rev={{.ID}}" title="browse the tree at this change">{{if .Desc}}{{.Desc}}{{else}}(no description){{end}}</a> + </div> + {{end}} + {{end}} +</div> +{{else if not .Err}}<p class="meta hint">no revisions matched</p>{{end}} +{{if .AtOp}}<p class="meta">as of operation <code>{{short .AtOp}}</code> · <a href="{{.Root}}jj?kind=op">back to the op log</a></p>{{end}} +{{end}} diff --git a/web/templates/frag-note.tmpl b/web/templates/frag-note.tmpl new file mode 100644 index 0000000..3b04e11 --- /dev/null +++ b/web/templates/frag-note.tmpl @@ -0,0 +1,105 @@ +{{define "note"}}{{$n := .N}}{{$p := .P}}{{$away := and $n.Change $p.ChangeID (ne $n.Change $p.ChangeID)}} +<article class="note am{{$n.Agent.Bit}}{{if $away}} away{{end}}" id="note-{{$n.ID}}" + data-agent="{{$n.Agent.Name}}" data-model="{{$n.Model}}" data-session="{{$n.Session}}" + data-kind="{{$n.Kind}}" data-file="{{$n.File}}" data-date="{{if not $n.At.IsZero}}{{$n.At.Format "2006-01-02"}}{{end}}" + data-start="{{$n.Start}}" data-end="{{$n.End}}" data-reply-to="{{$n.ReplyTo}}"> + <header> + {{if $n.File}}<a href="{{$p.NoteHref $n}}" title="{{$n.File}}">{{base $n.File}}{{lines $n}}</a>{{end}} + {{if $n.Kind}}<em>{{$n.Kind}}</em>{{end}} + <b class="who">{{$n.Agent.Short}}</b> + </header> + <div class="body">{{md $n.Text $p}}</div> + <footer class="meta"> + {{with $p.PermaHref $n}}<a class="perma" href="{{.}}" + title="permalink — this note on its own page, with the code it is about">#{{$n.ID}}</a> + {{else}}<span class="perma self">#{{$n.ID}}</span>{{end}} + {{if not $n.At.IsZero}}<time datetime="{{$n.At.Format "2006-01-02T15:04:05Z07:00"}}" title="{{$n.Ago}}">{{$n.When}}</time>{{end}} + {{if $n.Model}}<span class="model">{{$n.Model}}{{if $n.Reason}}/{{$n.Reason}}{{end}}</span>{{end}} + {{if $n.Session}}<code class="sess" title="{{$n.Session}}">{{short $n.Session}}</code>{{end}} + {{if $n.ReplyTo}}{{with $p.ReplyHref $n}}<a class="parent" href="{{.}}" + title="the note this explicitly replies to">reply to #{{$n.ReplyTo}}</a>{{else}}<span class="parent">reply to #{{$n.ReplyTo}}</span>{{end}}{{end}} + {{with $p.ThreadHref $n}}<a class="relations" href="{{.}}" title="notes replying to or mentioning this note"> + {{if $n.Replies}}↳ {{len $n.Replies}} repl{{if eq (len $n.Replies) 1}}y{{else}}ies{{end}}{{end}}{{if and $n.Replies $n.Mentions}} · {{end}}{{if $n.Mentions}}↶ {{len $n.Mentions}} mention{{if ne (len $n.Mentions) 1}}s{{end}}{{end}}</a>{{end}} + {{with $p.ConvHref $n}}<a class="conv" href="{{.}}" title="open the conversation this note was written in">conversation</a>{{end}} + {{if $n.Change}} + {{if $p.Live}}<a class="atchg{{if $away}} away{{end}}" href="{{$p.Href "change" $n.Change}}" + title="{{if $away}}written at another change{{else}}written at this change{{end}}"><code>{{short $n.Change}}</code></a> + {{else}}<code class="atchg{{if $away}} away{{end}}">{{short $n.Change}}</code>{{end}} + {{end}} + </footer> +</article> +{{end}} + +{{define "notelist"}}{{range .Notes}}{{template "note" (dict "N" . "P" $)}}{{end}}{{if not .Notes}}<p class="meta hint empty">no notes match these filters</p>{{end}}{{end}} + +{{/* htmx responses: the list plus an out-of-band count update */}} +{{define "notelist-oob"}}<span id="notecount" hx-swap-oob="true">{{len .Notes}}</span>{{template "notelist" .}}{{end}} + +{{define "note-added"}}{{if .File}}<span id="notecount" hx-swap-oob="true">{{len .File.Notes}}</span>{{end}}{{range .Notes}}{{template "note" (dict "N" . "P" $)}}{{end}}{{end}} + +{{/* A child-note form appears below every full card in a focused thread. It + deliberately uses a normal POST: one append and one redirect rebuild the + entire DAG from the log, including shared mention paths. */}} +{{define "replyform"}}{{$n := .N}}{{$p := .P}} +<details class="replybox"> + <summary>reply to #{{$n.ID}}</summary> + <form class="notform replyform" method="post" action="{{$p.Href "notes"}}"> + <input type="hidden" name="reply_to" value="{{$n.ID}}"> + <input type="hidden" name="rev" value="{{$p.Rev}}"> + <input type="hidden" name="change" value="{{$p.ChangeID}}"> + <input type="hidden" name="scope" value="all"> + <input type="hidden" name="back" value="{{$p.Href "note" (printf "%d" $p.Note.ID)}}#discussion"> + <textarea name="text" rows="2" placeholder="reply… (⌘/ctrl+enter to post)" aria-label="reply to note {{$n.ID}}" required></textarea> + <div class="row end"> + <span class="meta">as <b>{{$p.User}}</b> at <code title="{{$p.ChangeID}}">{{short $p.ChangeID}}</code> → the log</span> + <button type="submit">reply</button> + </div> + </form> +</details> +{{end}} + +{{/* Recursive at arbitrary depth. A DAG join/cycle is a reference rather than + a second card, which keeps ids unique and recursion finite. */}} +{{define "threadnode"}}{{$x := .Node}}{{$p := .P}} +<div class="threadnode via-{{$x.Via}}"> + <div class="threadedge" aria-label="{{$x.Via}}"><span>{{if eq $x.Via "reply"}}↳{{else}}↶{{end}}</span></div> + <div class="threadcontent"> + {{if $x.Reference}} + <p class="threadref meta">{{$x.Via}} also reaches <a href="{{$p.Href "note" (printf "%d" $x.Note.ID)}}">note #{{$x.Note.ID}}</a></p> + {{else}} + {{template "note" (dict "N" $x.Note "P" $p)}} + {{if $p.Live}}{{template "replyform" (dict "N" $x.Note "P" $p)}}{{end}} + {{if $x.Children}}<div class="threadchildren">{{range $x.Children}}{{template "threadnode" (dict "Node" . "P" $p)}}{{end}}</div>{{end}} + {{end}} + </div> +</div> +{{end}} + +{{/* the add-a-comment form; live pages only, since it writes to the log */}} +{{define "noteform"}} +<form class="notform" hx-post="{{.Href "notes"}}" hx-target="#notelist" hx-swap="afterbegin" + hx-on::after-request="if(event.detail.successful){this.reset();{{if eq .Kind "notes"}}htmx.trigger('#notefilters','change');{{end}}}" + method="post" action="{{.Href "notes"}}"> + <input type="hidden" name="rev" value="{{.Rev}}"> + <input type="hidden" name="change" value="{{.ChangeID}}"> + <input type="hidden" name="scope" value="{{.Scope}}"> + <input type="hidden" name="back" value="{{if .File}}{{.Href "code" .File.Path}}{{else}}{{.Href "notes"}}{{end}}"> + <div class="row"> + {{if .File}} + <input type="hidden" name="file" value="{{.File.Path}}"> + <span class="meta on">on <code>{{base .File.Path}}</code></span> + {{else}} + <input type="search" name="file" placeholder="file (optional)" list="filelist" autocomplete="off"> + {{end}} + <input type="number" id="nstart" name="start" min="1" placeholder="from" aria-label="first line"> + <input type="number" id="nend" name="end" min="1" placeholder="to" aria-label="last line"> + <input type="text" name="kind" placeholder="kind" aria-label="kind" list="kindlist" size="6"> + </div> + <textarea name="text" rows="3" placeholder="comment… (⌘/ctrl+enter to post)" required></textarea> + <div class="row end"> + <span class="meta">as <b>{{.User}}</b> at <code title="{{.ChangeID}}">{{short .ChangeID}}</code> → the log</span> + <button type="submit">add note</button> + </div> +</form> +<datalist id="kindlist">{{range .Facets.Kinds}}<option value="{{.}}">{{end}}</datalist> +{{end}} diff --git a/web/templates/layout.tmpl b/web/templates/layout.tmpl new file mode 100644 index 0000000..969cae6 --- /dev/null +++ b/web/templates/layout.tmpl @@ -0,0 +1,94 @@ +<!doctype html> +<html lang="en"{{if .Live}} data-live="1"{{end}}> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>{{.Title}}</title> +<link rel="icon" type="image/svg+xml" href="{{.Href "asset" "bemtevi.svg"}}"> +<link rel="stylesheet" href="{{.Href "asset" "style.css"}}"> +<script> +// the theme is worn before the first paint. A theme that is not one of the +// two the site was built with lives in themes.css, so pull that in too — +// from here, after style.css, so its :root block is the one that wins. +{const d = document.documentElement, t = localStorage.getItem("notevi-theme"); + if (t && t !== "system") d.dataset.theme = t; + if (t && !/^(system|dark|light)$/.test(t)) { + const l = document.createElement("link"); + l.rel = "stylesheet"; l.id = "themescss"; l.href = "{{.Href "asset" "themes.css"}}"; + document.head.append(l); + } + d.dataset.mode = localStorage.getItem("notevi-mode") || "trace"; + d.dataset.files = localStorage.getItem("notevi-files-hidden") === "1" ? "hidden" : "shown";} +</script> +{{if .Live}}<script src="{{.Href "asset" "htmx.min.js"}}"></script>{{end}} +<script src="{{.Href "asset" "app.js"}}" defer></script> +</head> +<body{{if eq .Kind "file"}} class="with-aside"{{end}} data-kind="{{.Kind}}"> + +<nav aria-label="repository files" id="nav" data-root="{{.Root}}" data-current="{{.Current}}" data-tree="{{.Href "tree"}}"> + <header> + <a class="brand" href="{{.Href ""}}"><img class="mark" src="{{.Href "asset" "bemtevi.svg"}}" alt="" width="20" height="20">{{.Site.Title}}</a> + <div class="modes" role="group" aria-label="focus"> + <button type="button" class="mode" data-mode="trace" title="reading coverage (t)">trace</button> + <button type="button" class="mode" data-mode="notes" title="notes and comments (t)">notes</button> + </div> + </header> + {{template "changebar" .}} + {{if .Live}} + <div id="activity" class="activity" hx-get="{{.Href "activity"}}?compact=1" hx-trigger="load, every 3s" hx-swap="innerHTML"></div> + {{end}} + <button type="button" id="filetoggle" aria-controls="filelist" aria-expanded="true">hide files</button> + <div id="filelist"> + <input id="filefind" class="find" type="search" placeholder="find file…" aria-label="find file" autocomplete="off" spellcheck="false"> + <div id="tree"><p class="meta hint">loading tree…</p></div> + </div> + <footer> + <span> + {{if and .Live (ne .Root "/")}}<a href="/">projects</a> · {{end}} + <a href="{{.Href "notes"}}">notes ({{len .Site.Notes}})</a> · + <a href="{{.Href "files"}}">files</a>{{if .Live}} · + <a href="{{.Href "jj"}}">jj</a>{{end}} + </span> + <div class="themepick" id="themepick" + data-js="{{.Href "asset" "themes.js"}}" data-css="{{.Href "asset" "themes.css"}}" + data-dark="{{.Site.Theme.Name}}" data-light="{{.Site.Light.Name}}" + data-darkid="{{.Site.Theme.ID}}" data-lightid="{{.Site.Light.ID}}"> + <button type="button" id="themebtn" aria-haspopup="listbox" aria-expanded="false" + title="theme — every one installed here (T)">theme</button> + <div class="thememenu" id="thememenu" hidden> + <input id="themefind" type="search" placeholder="filter themes…" aria-label="filter themes" + role="combobox" aria-controls="themelist" aria-autocomplete="list" + autocomplete="off" spellcheck="false"> + <ul id="themelist" role="listbox" aria-label="themes"></ul> + <p class="meta hint">↑↓ preview · enter keeps · esc puts it back</p> + </div> + </div> + </footer> +</nav> + +<main> +{{if .Err}}<p class="err">{{.Err}}</p>{{end}} +{{if eq .Kind "file"}}{{template "page-file" .}} +{{else if eq .Kind "notes"}}{{template "page-notes" .}} +{{else if eq .Kind "note"}}{{template "page-note" .}} +{{else if eq .Kind "files"}}{{template "page-files" .}} +{{else if eq .Kind "jj"}}{{template "page-jj" .}} +{{else if eq .Kind "change"}}{{template "page-change" .}} +{{else if eq .Kind "session"}}{{template "page-session" .}} +{{else}}{{template "page-index" .}}{{end}} +</main> + +{{if eq .Kind "file"}} +<aside aria-label="notes" class="notes-only"> + <header class="asidehead"> + <b>Notes (<span id="notecount">{{len .File.Notes}}</span>)</b> + {{if .Live}}<span class="meta">click a line to attach</span>{{end}} + </header> + {{if .Live}}{{template "noteform" .}}{{end}} + <div id="notelist">{{range .File.Notes}}{{template "note" (dict "N" . "P" $)}}{{end}}</div> + {{if not .File.Notes}}<p class="meta hint">no notes on this file yet</p>{{end}} +</aside> +{{end}} + +</body> +</html> diff --git a/web/templates/page-change.tmpl b/web/templates/page-change.tmpl new file mode 100644 index 0000000..8641da7 --- /dev/null +++ b/web/templates/page-change.tmpl @@ -0,0 +1,38 @@ +{{define "page-change"}} +{{$c := .Change}} +{{$p := .}} +<header class="filehead"> + <h1>{{$c.Row.Short}} <span class="meta">{{$c.Row.Flags}}</span></h1> + <p class="meta"> + commit <code>{{$c.Row.CommitShort}}</code> · {{$c.Row.Author}} · {{$c.Row.When}} + {{if $c.Row.Refs}} · <span class="refs">{{$c.Row.Refs}}</span>{{end}} · + <a href="{{.Href "files" }}">browse the tree here</a> + </p> +</header> + +<section> + {{if $c.Desc}}<pre class="desc">{{$c.Desc}}</pre>{{else}}<p class="meta hint">(no description set)</p>{{end}} + + {{if $c.Parents}} + <p class="meta">parents: + {{range $c.Parents}}<a href="{{$p.Href "change" .ID}}"><code>{{.Short}}</code></a> {{.Desc}} {{end}} + </p> + {{end}} + + <h2>Changed files <span class="meta">{{$c.Stat}}</span></h2> + {{if $c.Files}} + <table class="changed"> + <tbody> + {{range $c.Files}} + <tr><td class="st st-{{.Status}}">{{.Status}}</td> + <td><a href="{{$p.Href "code" .Path}}">{{.Path}}</a></td></tr> + {{end}} + </tbody> + </table> + {{else}}<p class="meta hint">no file changes</p>{{end}} + + <h2>Notes recorded at this change <span class="meta">{{len .Notes}}</span></h2> + <div class="notecols">{{range .Notes}}{{template "note" (dict "N" . "P" $)}}{{end}}</div> + {{if not .Notes}}<p class="meta hint">nothing in the log was written while this change was checked out</p>{{end}} +</section> +{{end}} diff --git a/web/templates/page-file.tmpl b/web/templates/page-file.tmpl new file mode 100644 index 0000000..39a8c44 --- /dev/null +++ b/web/templates/page-file.tmpl @@ -0,0 +1,20 @@ +{{define "page-file"}} +<header class="filehead"> + <h1>{{.File.Path}}</h1> + <p class="meta"> + {{if .File.Traced}} + {{range .Site.Agents}}{{if band $.File.Mask .Bit}}<span class="chip am{{.Bit}}">{{.Short}}</span> {{end}}{{end}} + <span class="trace-only"><data value="{{.File.Pct}}">{{.File.Pct}}% read</data> · + {{.File.Reads}} reads · {{.File.Greps}} greps · </span> + <span class="notes-only">{{len .File.Notes}} note{{if ne (len .File.Notes) 1}}s{{end}} · </span> + {{else}}not traced · {{end}}change + {{if .Live}}<a href="{{.Href "change" .File.Change}}"><code>{{short .File.Change}}</code></a>{{else}}<code>{{short .File.Change}}</code>{{end}} + </p> + {{if .File.Patterns}}<p class="meta greps trace-only">grepped: {{range .File.Patterns}}<code>{{.}}</code> {{end}}</p>{{end}} +</header> +{{if .File.Stub}} +<p class="meta stub">{{.File.Stub}}</p> +{{else}} +<div class="codewrap">{{.File.Body}}</div> +{{end}} +{{end}} diff --git a/web/templates/page-files.tmpl b/web/templates/page-files.tmpl new file mode 100644 index 0000000..3f158fe --- /dev/null +++ b/web/templates/page-files.tmpl @@ -0,0 +1,13 @@ +{{define "page-files"}} +<header class="filehead"> + <h1>Files</h1> + <p class="meta"> + everything at change <code title="{{.ChangeID}}">{{short .ChangeID}}</code> · + <span class="trace-only">traced files carry a coverage bar{{if eq .Scope "change"}}, from reads at this change{{end}}</span> + <span class="notes-only">◆ marks files with notes</span> + </p> +</header> +<section class="filetree"> +{{.Tree}} +</section> +{{end}} diff --git a/web/templates/page-index.tmpl b/web/templates/page-index.tmpl new file mode 100644 index 0000000..93cad6b --- /dev/null +++ b/web/templates/page-index.tmpl @@ -0,0 +1,56 @@ +{{define "page-index"}} +<header class="filehead"> + <h1>{{.Site.Title}}</h1> + <p class="meta"> + {{len .Files}} files at <code title="{{.ChangeID}}">{{short .ChangeID}}</code> · + {{len .Site.Order}} traced, {{len .Site.Notes}} notes + {{if or (eq .Scope "all") .ScopeEmpty}}<span class="away">across every change</span>{{else}}here{{end}} · + theme {{.Site.Theme.Name}} + </p> +</header> + +{{if .Live}} +<section> + <h2>Live <span class="meta">— agents reading this codebase</span></h2> + <div id="activity-full" hx-get="{{.Href "activity"}}" hx-trigger="load, every 3s" hx-swap="innerHTML"> + <p class="meta hint">…</p> + </div> +</section> +{{end}} + +<section class="trace-only"> + <h2>Agents</h2> + <table> + <thead><tr><th>agent</th><th>model</th><th>effort</th><th>session</th><th>reads</th><th>greps</th><th>notes</th></tr></thead> + <tbody> + {{range .Site.Agents}} + <tr><td><span class="chip am{{.Bit}}">{{.Short}}</span></td> + <td>{{.Model}}</td><td>{{.Reason}}</td> + <td>{{if and $.Live .Session}}<a href="{{$.Href "session" .Session}}" title="{{.Session}}"><code>{{short .Session}}</code></a> + {{else}}<code title="{{.Session}}">{{short .Session}}</code>{{end}}</td> + <td>{{.Reads}}</td><td>{{.Greps}}</td><td>{{.Notes}}</td></tr> + {{end}} + </tbody> + </table> +</section> + +<section class="trace-only"> + <h2>Coverage</h2> + <table> + <thead><tr><th>file</th><th>read</th><th>lines</th><th>notes</th></tr></thead> + <tbody> + {{range .Site.Order}}{{with index $.Site.Files .}} + <tr><td><a href="{{$.Href "code" .Path}}">{{.Path}}</a></td> + <td><data class="bar am{{.Mask}}" value="{{.Pct}}" style="--pct:{{.Pct}}%">{{.Pct}}%</data></td> + <td>{{.Covered}}/{{.Total}}</td><td>{{len .Notes}}</td></tr> + {{end}}{{end}} + </tbody> + </table> +</section> + +<section class="notes-only"> + <h2>Latest notes <span class="meta">— <a href="{{.Href "notes"}}">all {{len .Site.Notes}}, with filters</a></span></h2> + <div class="notecols">{{range .Notes}}{{template "note" (dict "N" . "P" $)}}{{end}}</div> + {{if not .Notes}}<p class="meta hint">no notes in the log yet</p>{{end}} +</section> +{{end}} diff --git a/web/templates/page-jj.tmpl b/web/templates/page-jj.tmpl new file mode 100644 index 0000000..24df596 --- /dev/null +++ b/web/templates/page-jj.tmpl @@ -0,0 +1,25 @@ +{{define "page-jj"}} +<header class="filehead"> + <h1>jj</h1> + <p class="meta"> + read-only queries against the repo — <code>--ignore-working-copy</code>, inline templates, nothing written to disk + </p> +</header> + +<section class="jjpage"> + <form class="jjform" method="get" action="{{.Href "jj"}}" + hx-get="{{.Href "jj-log"}}" hx-target="#jjout" hx-swap="innerHTML" hx-indicator="#jjbusy"> + <input type="hidden" name="kind" value="log"> + <input type="search" name="revset" value="{{.JJ.Revset}}" placeholder="revset — e.g. @ | ancestors(trunk(), 10) | description(glob:'*fix*')" autocomplete="off" spellcheck="false"> + <input type="number" name="limit" value="{{.JJ.Limit}}" min="1" max="1000" aria-label="limit" title="limit"> + <button type="submit">jj log</button> + <button type="button" hx-get="{{.Href "jj-op"}}" hx-target="#jjout" hx-include="closest form" hx-indicator="#jjbusy" title="operation log">op log</button> + <span id="jjbusy" class="busy">·</span> + </form> + <p class="meta hint"> + change ids open the change · descriptions browse the tree at that change · + op ids re-run the query <code>--at-operation</code>, which is how you see the repo as it was + </p> + <div id="jjout">{{template "jjrows" .JJ}}</div> +</section> +{{end}} diff --git a/web/templates/page-note.tmpl b/web/templates/page-note.tmpl new file mode 100644 index 0000000..8cd0e64 --- /dev/null +++ b/web/templates/page-note.tmpl @@ -0,0 +1,54 @@ +{{/* One note, on its own — the page a shared permalink opens. + + It has to stand up for a reader who has never seen this site: what was + said, about which lines of which file, at which change, and the code + itself, right there. Everything else on the page is a way further in. */}} +{{define "page-note"}} +{{$n := .Note}} +<header class="filehead"> + <h1>{{if $n.File}}{{base $n.File}}<span class="lns">{{lines $n}}</span>{{else}}Note{{end}}</h1> + <p class="meta"> + note <b>#{{$n.ID}}</b> by <b>{{$n.Agent.Short}}</b>{{if $n.Kind}} · <em class="kind">{{$n.Kind}}</em>{{end}} + {{if not $n.At.IsZero}} · <time datetime="{{$n.At.Format "2006-01-02T15:04:05Z07:00"}}">{{$n.When}}</time> + <span class="ago">({{$n.Ago}})</span>{{end}} + {{if $n.Change}} · read at + {{if .Live}}<a href="{{.Href "change" $n.Change}}"><code>{{short $n.Change}}</code></a> + {{else}}<code title="{{$n.Change}}">{{short $n.Change}}</code>{{end}} + {{end}} + </p> + {{if ne $n.File (base $n.File)}}<p class="meta"><code>{{$n.File}}</code></p>{{end}} +</header> + +<section class="notepage"> + <div class="onenote">{{template "note" (dict "N" $n "P" $)}}</div> + + {{$open := .HasCode $n.File}} + {{if .Excerpt}} + <p class="meta exhead">lines {{.ExFrom}}–{{.ExTo}} of <code>{{$n.File}}</code>{{if $open}} · + <a href="{{.NoteHref $n}}">open the whole file →</a>{{end}}</p> + <div class="codewrap excerpt">{{.Excerpt}}</div> + {{else if .ExErr}} + <p class="meta stub">{{.ExErr}}</p> + {{else if $n.File}} + <p class="meta hint">this note is about the whole file{{if $open}} — + <a href="{{.NoteHref $n}}">open <code>{{base $n.File}}</code> →</a>{{end}}</p> + {{else}} + <p class="meta hint">this note is not pinned to a file</p> + {{end}} + + {{if .Live}}{{template "replyform" (dict "N" $n "P" $)}}{{end}} + + <section class="discussion" id="discussion"> + <h2>Replies & mentions + <span class="meta">{{len $n.Replies}} repl{{if eq (len $n.Replies) 1}}y{{else}}ies{{end}} · {{len $n.Mentions}} mention{{if ne (len $n.Mentions) 1}}s{{end}}</span> + </h2> + {{if .Thread.Children}} + <div class="threadchildren rootchildren">{{range .Thread.Children}}{{template "threadnode" (dict "Node" . "P" $)}}{{end}}</div> + {{else}} + <p class="meta hint">no notes reply to or mention this one yet</p> + {{end}} + </section> + + <p class="meta more"><a href="{{.Href "notes"}}">← every note in the log</a></p> +</section> +{{end}} diff --git a/web/templates/page-notes.tmpl b/web/templates/page-notes.tmpl new file mode 100644 index 0000000..ebd5865 --- /dev/null +++ b/web/templates/page-notes.tmpl @@ -0,0 +1,52 @@ +{{define "facet"}}<select name="{{.Name}}" aria-label="{{.Label}}"> + <option value="">{{.Label}}</option> + {{range .Values}}<option value="{{.}}"{{if eq . $.Sel}} selected{{end}}>{{if $.Short}}{{short .}}…{{else}}{{.}}{{end}}</option> + {{end}}</select>{{end}} + +{{define "page-notes"}} +<header class="filehead"> + <h1>Notes</h1> + <p class="meta"> + {{if eq .Scope "all"}} + all {{.Site.NotesTotal}} in the log, every change + {{else if .ScopeEmpty}} + all {{.Site.NotesTotal}} in the log — nothing was recorded at <code>{{short .ChangeID}}</code> + {{else}} + {{len .Site.Notes}} written at <code title="{{.ChangeID}}">{{short .ChangeID}}</code>{{if .Elsewhere}} · + {{if .Live}}<a href="{{.ScopeHref "all"}}">{{.Elsewhere}} more at other changes</a> + {{else}}{{.Elsewhere}} more at other changes are not in this export{{end}}{{end}} + {{end}} + {{if .Live}} · anything you add here is appended to the private log sidecar at this change — the project working copy is never touched{{end}} + </p> +</header> + +<section class="notespage"> + {{if .Live}} + <details class="addnote"> + <summary>add a note</summary> + {{template "noteform" .}} + </details> + {{end}} + + <form class="filters" id="notefilters" method="get" action="{{.Href "notes"}}" + {{if .Live}}hx-get="{{.Href "notes-list"}}" hx-target="#notelist" hx-swap="innerHTML" + hx-trigger="input delay:250ms, change, search" hx-indicator="#notesbusy"{{end}}> + <input type="search" name="text" value="{{.Filter.Text}}" placeholder="search note text…" autocomplete="off"> + <input type="search" name="file" value="{{.Filter.File}}" placeholder="file path…" list="filelist" autocomplete="off"> + {{template "facet" (dict "Name" "agent" "Label" "any agent" "Values" .Facets.Agents "Sel" .Filter.Agent)}} + {{template "facet" (dict "Name" "model" "Label" "any model" "Values" .Facets.Models "Sel" .Filter.Model)}} + {{template "facet" (dict "Name" "session" "Label" "any session" "Values" .Facets.Sessions "Sel" .Filter.Session "Short" true)}} + {{template "facet" (dict "Name" "kind" "Label" "any kind" "Values" .Facets.Kinds "Sel" .Filter.Kind)}} + <span class="dates"> + <label class="datef">from <input type="date" name="from" value="{{.Filter.From}}"></label> + <label class="datef">to <input type="date" name="to" value="{{.Filter.To}}"></label> + </span> + <button type="submit" class="nohx">filter</button> + <button type="button" id="clearfilters">clear</button> + </form> + <datalist id="filelist">{{range .Facets.Files}}<option value="{{.}}">{{end}}</datalist> + + <p class="meta count"><span id="notesbusy" class="busy">·</span><span id="notecount">{{len .Notes}}</span> notes shown</p> + <div id="notelist">{{template "notelist" .}}</div> +</section> +{{end}} diff --git a/web/templates/page-session.tmpl b/web/templates/page-session.tmpl new file mode 100644 index 0000000..1155b67 --- /dev/null +++ b/web/templates/page-session.tmpl @@ -0,0 +1,70 @@ +{{/* One session's conversation, with its notevi log woven in by time. */}} +{{define "page-session"}} +{{$c := .Conv}} +<header class="filehead"> + <h1> + {{if $c.Agent}}<span class="chip am{{$c.Bit}}">{{$c.Agent}}</span> {{end}} + <code title="{{$c.Session}}">{{short $c.Session}}</code> + </h1> + <p class="meta"> + {{if $c.Model}}{{$c.Model}} · {{end}}{{$c.Reads}} reads · {{$c.Greps}} greps · {{$c.Notes}} notes + {{if $c.Dir}} · <span title="{{$c.Dir}}">{{base $c.Dir}}</span>{{end}} + {{if $c.Harness}} · {{$c.Harness}}{{end}} + </p> + {{if $c.Path}} + <p class="meta"><code>{{$c.Path}}</code></p> + {{else}} + <p class="meta hint">no transcript found for this session — searched + {{if $c.Roots}}{{range $i, $r := $c.Roots}}{{if $i}}, {{end}}<code>{{$r}}</code>{{end}}{{else}}nowhere: <code>-transcripts</code> is empty{{end}} · + showing the notevi log alone</p> + {{end}} + {{if $c.Err}}<p class="err">{{$c.Err}}</p>{{end}} +</header> + +<section class="convpage"> + {{if $c.Windowed}} + <p class="meta"> + moments {{$c.From}}–{{$c.To}} of {{$c.Total}}{{if $c.Anchor}}, around the note{{end}} · + {{if $c.Incomplete}}the rest is beyond what one page renders{{else}}<a href="{{$c.AllHref}}">show the whole conversation</a>{{end}} + </p> + {{end}} + + <div class="conv" data-anchor="{{$c.Anchor}}"> + {{range $c.Turns}} + {{if .Note}} + <div class="turn t-note{{if .Here}} here{{end}}"> + <span class="role">note</span><time>{{.Clock}}</time> + {{if .Note.File}} + <div class="notewrap" data-href="{{$.NoteHref .Note}}" tabindex="0" role="link" + title="open this note on the code it is about">{{template "note" (dict "N" .Note "P" $)}}</div> + {{else}} + <div class="notewrap">{{template "note" (dict "N" .Note "P" $)}}</div> + {{end}} + </div> + {{else if eq .Role "notevi"}} + <div class="turn t-notevi"> + <span class="role">notevi</span><time>{{.Clock}}</time> + {{if .File}}<a class="what" href="{{$.Href "code" .File}}">{{.Text}}</a> + {{else}}<span class="what">{{.Text}}</span>{{end}} + </div> + {{else if eq .Role "tool"}} + <div class="turn t-tool"> + <span class="role">{{.Tool}}</span><time>{{.Clock}}</time> + <code class="what">{{.Detail}}</code> + </div> + {{else if eq .Role "result"}} + <div class="turn t-result{{if eq .Kind "error"}} bad{{end}}"> + <span class="role">→{{if .Tool}} {{.Tool}}{{end}}</span><time></time> + <code class="what">{{.Detail}}</code> + </div> + {{else}} + <div class="turn t-say t-{{.Role}}"> + <span class="role">{{.Role}}</span><time>{{.Clock}}</time> + <p>{{.Text}}</p> + </div> + {{end}} + {{end}} + {{if not $c.Turns}}<p class="meta hint">nothing recorded for this session</p>{{end}} + </div> +</section> +{{end}} diff --git a/web/templates/projects.tmpl b/web/templates/projects.tmpl new file mode 100644 index 0000000..ce75454 --- /dev/null +++ b/web/templates/projects.tmpl @@ -0,0 +1,113 @@ +{{define "projects"}}<!doctype html> +<html lang="en" data-live="1"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>{{.Title}}</title> +<link rel="icon" type="image/svg+xml" href="/bemtevi.svg"> +<link rel="stylesheet" href="/style.css"> +<script> +// Match project pages: wear the saved theme before first paint, and load the +// installed-theme stylesheet now only when that saved choice requires it. +{const d = document.documentElement, t = localStorage.getItem("notevi-theme"); + if (t && t !== "system") d.dataset.theme = t; + if (t && !/^(system|dark|light)$/.test(t)) { + const l = document.createElement("link"); + l.rel = "stylesheet"; l.id = "themescss"; l.href = "/themes.css"; + document.head.append(l); + } + d.dataset.mode = localStorage.getItem("notevi-mode") || "trace";} +</script> +<script src="/app.js" defer></script> +</head> +<body class="projects"> +<nav aria-label="projects" id="nav"> + <header><a class="brand" href="/"><img class="mark" src="/bemtevi.svg" alt="" width="20" height="20">{{.Title}}</a></header> + <p class="meta">one process · every local notevi sidecar</p> + <input id="projectfind" class="find" type="search" placeholder="find project…" aria-label="find project" autocomplete="off" spellcheck="false"> + <form method="post" action="/projects/refresh" class="project-refresh"> + <button type="submit">refresh repositories</button> + </form> + <p class="meta">scans <code>{{.ScanRoot}}</code></p> + <p class="meta">last refresh {{.Scanned}}</p> + <footer> + <span>selection and scan results stay in memory</span> + <div class="themepick" id="themepick" + data-js="/themes.js" data-css="/themes.css" + data-dark="{{.Theme.Name}}" data-light="{{.Light.Name}}" + data-darkid="{{.Theme.ID}}" data-lightid="{{.Light.ID}}"> + <button type="button" id="themebtn" aria-haspopup="listbox" aria-expanded="false" + title="theme — every one installed here (T)">theme</button> + <div class="thememenu" id="thememenu" hidden> + <input id="themefind" type="search" placeholder="filter themes…" aria-label="filter themes" + role="combobox" aria-controls="themelist" aria-autocomplete="list" + autocomplete="off" spellcheck="false"> + <ul id="themelist" role="listbox" aria-label="themes"></ul> + <p class="meta hint">↑↓ preview · enter keeps · esc puts it back</p> + </div> + </div> + </footer> +</nav> + +<main class="projects-main"> + <header class="filehead"> + <h1>Projects</h1> + <p class="meta">Choose a notevi log, start one in a jj repository, or colocate jj with an existing Git repository.</p> + </header> + {{if .Message}}<p class="ok project-message">{{.Message}}</p>{{end}} + {{if .Err}}<p class="err project-message">{{.Err}}</p>{{end}} + + <section> + <h2>Logs <span class="meta">— {{len .Active}} repositories with a private sidecar</span></h2> + <div class="project-grid"> + {{range .Active}} + <article class="project-card" data-project="{{.Name}} {{.Relative}}"> + <header><h3><a href="/p/{{.ID}}/">{{.Name}}</a></h3><span class="project-state active">log</span></header> + <p class="project-path" title="{{.Path}}"><code>{{.Relative}}</code></p> + <p class="meta">{{.Entries}} entries · {{.Notes}} notes · {{.Agents}} agents · {{.Updated}}</p> + {{if gt (len .Workspaces) 1}}<p class="meta">{{len .Workspaces}} jj workspaces share this log</p>{{end}} + {{if .Err}}<p class="err">{{.Err}}</p>{{end}} + <a class="project-action" href="/p/{{.ID}}/">open project</a> + </article> + {{end}} + {{if not .Active}}<p class="meta hint">no notevi sidecars found yet</p>{{end}} + </div> + </section> + + <section> + <h2>Ready to start <span class="meta">— {{len .Ready}} jj repositories without a notevi sidecar</span></h2> + <div class="project-grid"> + {{range .Ready}} + <article class="project-card" data-project="{{.Name}} {{.Relative}}"> + <header><h3>{{.Name}}</h3><span class="project-state">jj</span></header> + <p class="project-path" title="{{.Path}}"><code>{{.Relative}}</code></p> + {{if gt (len .Workspaces) 1}}<p class="meta">{{len .Workspaces}} workspaces</p>{{end}} + {{if .Err}}<p class="err">{{.Err}}</p>{{end}} + <form method="post" action="/projects/{{.ID}}/start"> + <button type="submit">start private sidecar</button> + </form> + </article> + {{end}} + {{if not .Ready}}<p class="meta hint">every detected jj repository already has a sidecar</p>{{end}} + </div> + </section> + + <section> + <h2>Git repositories <span class="meta">— {{len .Git}} without jj metadata</span></h2> + <p class="meta">Initializing adds colocated <code>.jj</code> metadata; it does not create a sidecar until you choose “start private sidecar”.</p> + <div class="project-grid"> + {{range .Git}} + <article class="project-card" data-project="{{.Name}} {{.Relative}}"> + <header><h3>{{.Name}}</h3><span class="project-state git">git</span></header> + <p class="project-path" title="{{.Path}}"><code>{{.Relative}}</code></p> + <form method="post" action="/git/{{.ID}}/init"> + <button type="submit">initialize colocated jj</button> + </form> + </article> + {{end}} + {{if not .Git}}<p class="meta hint">no Git-only repositories detected</p>{{end}} + </div> + </section> +</main> +</body> +</html>{{end}} |
