Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Build out the forge web UI

f09ef39efe44de693748053a7b596ef328fcf06b · Keenan Nemetz · 15d ago · parent 70b70264

Build out the forge web UI

Turn the web surface into a compact, server-rendered forge with HTMX as the
progressive dynamic layer.

- Split templates and assets into embedded files; add a tokenised dark theme,
  Lucide icons, and vendored HTMX.
- Add the repository Overview page and a separate chronological Activity feed
  with type filters and HTMX fragment updates.
- Finish the issue and change workflows: create pages, comments, close/reopen,
  labels and assignees, approval, discussion, line-anchored review comments,
  and HTMX list filters.
- Add syntax-highlighted blobs, contributor/statistics derivation, branch-aware
  code browsing, and a repository Runs list.
- Add a loopback-only development login for local write testing, with explicit
  configuration and listener checks.
- Extend tests for the new pages, fragments, labels, assignees, anchors, and
  development login.

server/web/templates/layout.html +130

@@ -0,0 +1,130 @@
1 {{define "header"}}<!doctype html>
2 <html lang="en">
3 <head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width,initial-scale=1">
6 <meta name="csrf" content="{{.CSRF}}">
7 <meta name="color-scheme" content="dark">
8 <title>{{.Title}} · Vole</title>
9 <link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
10 <link rel="stylesheet" href="/static/vole.css">
11 <script src="/static/htmx.min.js" defer></script>
12 <script src="/auth.js" defer></script>
13 <script src="/static/app.js" defer></script>
14 </head>
15 <body>
16 <header class="topbar">
17 <a class="brand" href="/" aria-label="Vole home"><span class="mark" aria-hidden="true">v</span>vole</a>
18 <nav class="topnav" aria-label="Global">
19 <a href="/">{{icon "book-open"}}Repositories</a>
20 <a href="/runners">{{icon "server"}}Runners</a>
21 </nav>
22 <div class="topright">
23 {{if .LoggedIn}}
24 <a class="account" href="/account">{{icon "user"}}{{.User}}</a>
25 {{if .DevLogin}}<span class="label" title="Development login">dev</span>{{end}}
26 <form class="inline" method="post" action="/logout"><input type="hidden" name="csrf" value="{{.CSRF}}"><button class="link" type="submit">Log out</button></form>
27 {{else}}{{if .DevLogin}}<a href="/dev/login">Dev login</a> · {{end}}<a href="/login">Log in</a>{{end}}
28 </div>
29 </header>
30 <main class="page">
31 {{if .Error}}<div class="flash bad">{{.Error}}</div>{{end}}
32 {{end}}
33
34 {{define "footer"}}
35 </main>
36 </body>
37 </html>
38 {{end}}
39
40 {{define "repohead"}}
41 <div class="repohead">
42 <a class="reponame" href="/r/{{.Name}}">{{.Name}}</a>
43 <nav class="tabs" aria-label="Repository">
44 <a href="/r/{{.Name}}"{{if eq .Tab "overview"}} class="active" aria-current="page"{{end}}>{{icon "layout-dashboard"}}Overview</a>
45 <a href="/r/{{.Name}}/activity"{{if eq .Tab "activity"}} class="active" aria-current="page"{{end}}>{{icon "activity"}}Activity</a>
46 <a href="/r/{{.Name}}/code"{{if eq .Tab "code"}} class="active" aria-current="page"{{end}}>{{icon "code"}}Code</a>
47 <a href="/r/{{.Name}}/issues"{{if eq .Tab "issues"}} class="active" aria-current="page"{{end}}>{{icon "circle-dot"}}Issues</a>
48 <a href="/r/{{.Name}}/requests"{{if eq .Tab "requests"}} class="active" aria-current="page"{{end}}>{{icon "git-pull-request"}}Changes</a>
49 <a href="/r/{{.Name}}/releases"{{if eq .Tab "releases"}} class="active" aria-current="page"{{end}}>{{icon "tag"}}Releases</a>
50 <a href="/r/{{.Name}}/runs"{{if eq .Tab "runs"}} class="active" aria-current="page"{{end}}>{{icon "play"}}Runs</a>
51 <a href="/r/{{.Name}}/locks"{{if eq .Tab "locks"}} class="active" aria-current="page"{{end}}>{{icon "lock"}}Locks</a>
52 </nav>
53 </div>
54 {{end}}
55
56 {{define "notfound"}}
57 {{template "header" .}}
58 <div class="empty-page">
59 <p class="eyebrow">Error</p>
60 <h1>{{.Title}}</h1>
61 <p class="muted">{{.Message}}</p>
62 <p><a class="button" href="/">Back to repositories</a></p>
63 </div>
64 {{template "footer" .}}
65 {{end}}
66
67 {{define "token_input"}}
68 <div class="label-input" data-label-input data-label-item="{{.Item}}">
69 <div class="label-tokens" data-label-tokens>
70 <input type="text" class="label-entry" name="{{.Field}}" value="{{join .Selected ", "}}" data-label-entry autocomplete="off" placeholder="{{.Placeholder}}" aria-label="{{.Placeholder}}">
71 </div>
72 <div class="label-suggest" data-label-suggest hidden>
73 {{range .All}}<button type="button" class="label-option" data-label-option="{{.}}">{{.}}</button>{{end}}
74 </div>
75 </div>
76 {{end}}
77
78 {{define "commitlist"}}
79 {{if .Commits}}
80 <table class="commits">
81 <tbody>
82 {{range .Commits}}
83 <tr>
84 <td class="sha"><a href="/r/{{$.Name}}/commit/{{.SHA}}">{{short .SHA}}</a></td>
85 <td><a href="/r/{{$.Name}}/commit/{{.SHA}}">{{.Subject}}</a></td>
86 <td class="muted">{{.Author}}</td>
87 <td class="muted nowrap">{{since .When}}</td>
88 </tr>
89 {{end}}
90 </tbody>
91 </table>
92 {{else}}
93 <p class="muted">No commits yet.</p>
94 {{end}}
95 {{end}}
96
97 {{define "diff"}}
98 <div class="diffsum muted">{{len .Files}} file(s) changed{{if .Added}} · <span class="adds">+{{.Added}}</span>{{end}}{{if .Removed}} · <span class="dels">−{{.Removed}}</span>{{end}}{{if .Truncated}} · showing the first 512 KiB{{end}}</div>
99 {{range .Files}}
100 {{$file := .}}
101 <section class="diff">
102 <h3><span class="path">{{if and .OldPath (ne .OldPath .NewPath)}}{{.OldPath}} → {{end}}{{.Label}}</span>{{if .Binary}} <span class="label">binary</span>{{end}}</h3>
103 {{if .Binary}}
104 <p class="binary">Binary file: no line diff to show. Use <code>git show</code> to inspect it.</p>
105 {{end}}
106 {{if .Hunks}}
107 <table>
108 <tbody>
109 {{range .Hunks}}
110 <tr class="hunk"><td colspan="4">{{.Header}}</td></tr>
111 {{range .Lines}}
112 {{$side := "new"}}{{$path := $file.NewPath}}
113 {{if eq .Kind "del"}}{{$side = "old"}}{{$path = $file.OldPath}}{{end}}
114 <tr class="{{.Kind}}">
115 <td class="comment">
116 {{if and $.LineCommentURL $path}}<a href="{{$.LineCommentURL}}?path={{urlquery $path}}&amp;line={{if eq $side "old"}}{{.OldNo}}{{else}}{{.NewNo}}{{end}}&amp;side={{$side}}#request-comment-form" title="Comment on this line" aria-label="Comment on this line">{{icon "plus"}}</a>{{end}}
117 </td>
118 <td class="ln">{{if .OldNo}}{{.OldNo}}{{end}}</td>
119 <td class="ln">{{if .NewNo}}{{.NewNo}}{{end}}</td>
120 <td class="code">{{.Text}}</td>
121 </tr>
122 {{if .Comments}}<tr class="note"><td colspan="4">{{range .Comments}}<a class="label" href="#c-{{.ID}}">{{who .Author}} commented</a>{{end}}</td></tr>{{end}}
123 {{end}}
124 {{end}}
125 </tbody>
126 </table>
127 {{end}}
128 </section>
129 {{end}}
130 {{end}}