tenseleyflow/shithub / 31f7200

Browse files

S34: admin page templates (dashboard/users/repos/jobs/audit/system/email)

Authored by espadonne
SHA
31f7200f032ffecd92f63169fbbfdb1e9c0e967a
Parents
4fa8d24
Tree
35f1479

10 changed files

StatusFile+-
A internal/web/templates/admin/audit_list.html 42 0
A internal/web/templates/admin/dashboard.html 36 0
A internal/web/templates/admin/email.html 29 0
A internal/web/templates/admin/job_view.html 34 0
A internal/web/templates/admin/jobs_list.html 50 0
A internal/web/templates/admin/repo_view.html 38 0
A internal/web/templates/admin/repos_list.html 47 0
A internal/web/templates/admin/system.html 37 0
A internal/web/templates/admin/user_view.html 55 0
A internal/web/templates/admin/users_list.html 46 0
internal/web/templates/admin/audit_list.htmladded
@@ -0,0 +1,42 @@
1
+{{ define "page" -}}
2
+<div class="shithub-settings-page">
3
+  {{ template "admin-nav" . }}
4
+  <div class="shithub-settings-content">
5
+    <h1>Audit log</h1>
6
+
7
+    <form method="GET" action="/admin/audit" class="shithub-finder-form">
8
+      <input type="text" name="actor" value="{{ .Q.Get "actor" }}" placeholder="actor id">
9
+      <input type="text" name="action" value="{{ .Q.Get "action" }}" placeholder="action prefix">
10
+      <input type="text" name="target_type" value="{{ .Q.Get "target_type" }}" placeholder="target type">
11
+      <input type="text" name="target_id" value="{{ .Q.Get "target_id" }}" placeholder="target id">
12
+      <input type="date" name="since" value="{{ .Q.Get "since" }}">
13
+      <input type="date" name="until" value="{{ .Q.Get "until" }}">
14
+      <button type="submit" class="shithub-button">Filter</button>
15
+    </form>
16
+
17
+    {{ if .Rows }}
18
+    <table class="shithub-branches-table">
19
+      <thead><tr><th>When</th><th>Actor</th><th>Action</th><th>Target</th><th>Meta</th></tr></thead>
20
+      <tbody>
21
+        {{ range .Rows }}
22
+        <tr>
23
+          <td>{{ relativeTime .CreatedAt.Time }}</td>
24
+          <td>{{ if .ActorID.Valid }}#{{ .ActorID.Int64 }}{{ else }}<span class="shithub-fg-muted">system</span>{{ end }}</td>
25
+          <td><code>{{ .Action }}</code></td>
26
+          <td>{{ .TargetType }}{{ if .TargetID.Valid }} #{{ .TargetID.Int64 }}{{ end }}</td>
27
+          <td><code>{{ printf "%s" .Meta }}</code></td>
28
+        </tr>
29
+        {{ end }}
30
+      </tbody>
31
+    </table>
32
+    {{ else }}
33
+    <p class="shithub-empty-note">No audit rows match.</p>
34
+    {{ end }}
35
+
36
+    <nav class="shithub-pagination">
37
+      {{ if gt .Page 1 }}<a href="?page={{ .PrevPage }}&{{ .Filters }}">← Prev</a>{{ end }}
38
+      {{ if .HasMore }}<a href="?page={{ .NextPage }}&{{ .Filters }}">Next →</a>{{ end }}
39
+    </nav>
40
+  </div>
41
+</div>
42
+{{- end }}
internal/web/templates/admin/dashboard.htmladded
@@ -0,0 +1,36 @@
1
+{{ define "page" -}}
2
+<div class="shithub-settings-page">
3
+  {{ template "admin-nav" . }}
4
+  <div class="shithub-settings-content">
5
+    <h1>Dashboard</h1>
6
+
7
+    <section class="shithub-settings-section">
8
+      <h2>Counts</h2>
9
+      <ul>
10
+        <li>Active users: <strong>{{ index .Counts "users" }}</strong></li>
11
+        <li>Suspended users: <strong>{{ index .Counts "suspended" }}</strong></li>
12
+        <li>Site admins: <strong>{{ index .Counts "admins" }}</strong></li>
13
+        <li>Active repositories: <strong>{{ index .Counts "repos" }}</strong></li>
14
+        <li>Active organizations: <strong>{{ index .Counts "orgs" }}</strong></li>
15
+      </ul>
16
+    </section>
17
+
18
+    <section class="shithub-settings-section">
19
+      <h2>Job queue</h2>
20
+      {{ if .JobsByStatus }}
21
+      <table class="shithub-branches-table">
22
+        <thead><tr><th>Status</th><th>Count</th></tr></thead>
23
+        <tbody>
24
+          {{ range .JobsByStatus }}
25
+          <tr><td><code>{{ .Status }}</code></td><td>{{ .N }}</td></tr>
26
+          {{ end }}
27
+        </tbody>
28
+      </table>
29
+      {{ else }}
30
+      <p class="shithub-empty-note">No jobs in the queue.</p>
31
+      {{ end }}
32
+      <p><a href="/admin/jobs">Open job queue →</a></p>
33
+    </section>
34
+  </div>
35
+</div>
36
+{{- end }}
internal/web/templates/admin/email.htmladded
@@ -0,0 +1,29 @@
1
+{{ define "page" -}}
2
+<div class="shithub-settings-page">
3
+  {{ template "admin-nav" . }}
4
+  <div class="shithub-settings-content">
5
+    <h1>Email queue</h1>
6
+    <p class="shithub-hint">Recent transactional email (auth, settings, admin actions). Notification-fanout email lives separately in <code>notification_email_log</code> and is reachable via the audit view.</p>
7
+
8
+    {{ if .Rows }}
9
+    <table class="shithub-branches-table">
10
+      <thead><tr><th>When</th><th>Kind</th><th>To</th><th>Subject</th><th>Status</th><th>Error</th></tr></thead>
11
+      <tbody>
12
+        {{ range .Rows }}
13
+        <tr>
14
+          <td>{{ relativeTime .SentAt.Time }}</td>
15
+          <td><code>{{ .Kind }}</code></td>
16
+          <td>{{ .RecipientEmail }}</td>
17
+          <td>{{ .Subject }}</td>
18
+          <td><code>{{ .Status }}</code></td>
19
+          <td>{{ if .ErrorSummary.Valid }}{{ .ErrorSummary.String }}{{ end }}</td>
20
+        </tr>
21
+        {{ end }}
22
+      </tbody>
23
+    </table>
24
+    {{ else }}
25
+    <p class="shithub-empty-note">No transactional email logged yet.</p>
26
+    {{ end }}
27
+  </div>
28
+</div>
29
+{{- end }}
internal/web/templates/admin/job_view.htmladded
@@ -0,0 +1,34 @@
1
+{{ define "page" -}}
2
+<div class="shithub-settings-page">
3
+  {{ template "admin-nav" . }}
4
+  <div class="shithub-settings-content">
5
+    <h1>Job #{{ .Job.ID }} <span class="shithub-fg-muted">{{ .Job.Kind }}</span></h1>
6
+    <p><a href="/admin/jobs">← All jobs</a></p>
7
+
8
+    <section class="shithub-settings-section">
9
+      <h2>Summary</h2>
10
+      <ul>
11
+        <li>Attempts: {{ .Job.Attempts }} of {{ .Job.MaxAttempts }}</li>
12
+        <li>Run at: {{ .Job.RunAt.Time }}</li>
13
+        {{ if .Job.LockedBy.Valid }}<li>Locked by: <code>{{ .Job.LockedBy.String }}</code> at {{ relativeTime .Job.LockedAt.Time }}</li>{{ end }}
14
+        {{ if .Job.CompletedAt.Valid }}<li>Completed: {{ relativeTime .Job.CompletedAt.Time }}</li>{{ end }}
15
+        {{ if .Job.FailedAt.Valid }}<li>Failed: {{ relativeTime .Job.FailedAt.Time }}</li>{{ end }}
16
+        {{ if .Job.LastError.Valid }}<li>Last error: <code>{{ .Job.LastError.String }}</code></li>{{ end }}
17
+      </ul>
18
+      <form method="POST" action="/admin/jobs/{{ .Job.ID }}/retry" style="display:inline">
19
+        <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
20
+        <button type="submit" class="shithub-button shithub-button-primary">Retry now</button>
21
+      </form>
22
+      <form method="POST" action="/admin/jobs/{{ .Job.ID }}/discard" style="display:inline">
23
+        <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
24
+        <button type="submit" class="shithub-button shithub-button-danger">Discard</button>
25
+      </form>
26
+    </section>
27
+
28
+    <section class="shithub-settings-section">
29
+      <h2>Payload</h2>
30
+      <pre class="shithub-code-pre">{{ .PayloadStr }}</pre>
31
+    </section>
32
+  </div>
33
+</div>
34
+{{- end }}
internal/web/templates/admin/jobs_list.htmladded
@@ -0,0 +1,50 @@
1
+{{ define "page" -}}
2
+<div class="shithub-settings-page">
3
+  {{ template "admin-nav" . }}
4
+  <div class="shithub-settings-content">
5
+    <h1>Jobs</h1>
6
+    {{ with .Notice }}<p class="shithub-flash shithub-flash-notice">{{ . }}</p>{{ end }}
7
+
8
+    <p>
9
+      {{ range .Counts }}<code>{{ .Status }}</code>: {{ .N }} &nbsp; {{ end }}
10
+    </p>
11
+
12
+    <form method="GET" action="/admin/jobs" class="shithub-finder-form">
13
+      <input type="text" name="kind" value="{{ .Kind }}" placeholder="kind (e.g. push:process)">
14
+      <select name="status">
15
+        <option value="">any</option>
16
+        <option value="queued" {{ if eq .Status "queued" }}selected{{ end }}>queued</option>
17
+        <option value="running" {{ if eq .Status "running" }}selected{{ end }}>running</option>
18
+        <option value="failed" {{ if eq .Status "failed" }}selected{{ end }}>failed</option>
19
+        <option value="completed" {{ if eq .Status "completed" }}selected{{ end }}>completed</option>
20
+      </select>
21
+      <button type="submit" class="shithub-button">Filter</button>
22
+    </form>
23
+
24
+    {{ if .Jobs }}
25
+    <table class="shithub-branches-table">
26
+      <thead><tr><th>ID</th><th>Kind</th><th>Status</th><th>Attempts</th><th>Created</th><th></th></tr></thead>
27
+      <tbody>
28
+        {{ range .Jobs }}
29
+        <tr>
30
+          <td>#{{ .ID }}</td>
31
+          <td><code>{{ .Kind }}</code></td>
32
+          <td><code>{{ .Status }}</code></td>
33
+          <td>{{ .Attempts }}/{{ .MaxAttempts }}</td>
34
+          <td>{{ relativeTime .CreatedAt.Time }}</td>
35
+          <td><a href="/admin/jobs/{{ .ID }}">View →</a></td>
36
+        </tr>
37
+        {{ end }}
38
+      </tbody>
39
+    </table>
40
+    {{ else }}
41
+    <p class="shithub-empty-note">No jobs match.</p>
42
+    {{ end }}
43
+
44
+    <nav class="shithub-pagination">
45
+      {{ if gt .Page 1 }}<a href="?page={{ .PrevPage }}">← Prev</a>{{ end }}
46
+      {{ if .HasMore }}<a href="?page={{ .NextPage }}">Next →</a>{{ end }}
47
+    </nav>
48
+  </div>
49
+</div>
50
+{{- end }}
internal/web/templates/admin/repo_view.htmladded
@@ -0,0 +1,38 @@
1
+{{ define "page" -}}
2
+<div class="shithub-settings-page">
3
+  {{ template "admin-nav" . }}
4
+  <div class="shithub-settings-content">
5
+    <h1>{{ .Repo.Name }} <span class="shithub-fg-muted">#{{ .Repo.ID }}</span></h1>
6
+    {{ with .Notice }}<p class="shithub-flash shithub-flash-notice">{{ . }}</p>{{ end }}
7
+
8
+    <section class="shithub-settings-section">
9
+      <h2>Details</h2>
10
+      <ul>
11
+        <li>Visibility: <code>{{ .Repo.Visibility }}</code></li>
12
+        <li>Default branch: <code>{{ .Repo.DefaultBranch }}</code></li>
13
+        <li>Disk used: {{ .Repo.DiskUsedBytes }} bytes</li>
14
+        <li>Stars: {{ .Repo.StarCount }} · Watchers: {{ .Repo.WatcherCount }} · Forks: {{ .Repo.ForkCount }}</li>
15
+        <li>Created: {{ relativeTime .Repo.CreatedAt.Time }}</li>
16
+        {{ if .Repo.IsArchived }}<li>Archived {{ relativeTime .Repo.ArchivedAt.Time }}</li>{{ end }}
17
+        {{ if .Repo.DeletedAt.Valid }}<li>Soft-deleted {{ relativeTime .Repo.DeletedAt.Time }}</li>{{ end }}
18
+      </ul>
19
+    </section>
20
+
21
+    <section class="shithub-settings-section">
22
+      <h2>Force actions</h2>
23
+      <p class="shithub-hint">These bypass owner consent. Use only for emergency takedown / abuse response.</p>
24
+      <form method="POST" action="/admin/repos/{{ .Repo.ID }}/archive" style="display:inline">
25
+        <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
26
+        <button type="submit" class="shithub-button">{{ if .Repo.IsArchived }}Force-unarchive{{ else }}Force-archive{{ end }}</button>
27
+      </form>
28
+
29
+      <form method="POST" action="/admin/repos/{{ .Repo.ID }}/delete">
30
+        <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
31
+        <label>Type <code>{{ .Repo.Name }}</code> to confirm
32
+          <input type="text" name="confirm" required></label>
33
+        <button type="submit" class="shithub-button shithub-button-danger">Force-delete (skip grace)</button>
34
+      </form>
35
+    </section>
36
+  </div>
37
+</div>
38
+{{- end }}
internal/web/templates/admin/repos_list.htmladded
@@ -0,0 +1,47 @@
1
+{{ define "page" -}}
2
+<div class="shithub-settings-page">
3
+  {{ template "admin-nav" . }}
4
+  <div class="shithub-settings-content">
5
+    <h1>Repositories</h1>
6
+    {{ with .Notice }}<p class="shithub-flash shithub-flash-notice">{{ . }}</p>{{ end }}
7
+
8
+    <form method="GET" action="/admin/repos" class="shithub-finder-form">
9
+      <input type="text" name="q" value="{{ .Q }}" placeholder="name prefix">
10
+      <select name="visibility">
11
+        <option value="">any visibility</option>
12
+        <option value="public" {{ if eq .Visibility "public" }}selected{{ end }}>public</option>
13
+        <option value="private" {{ if eq .Visibility "private" }}selected{{ end }}>private</option>
14
+      </select>
15
+      <label><input type="checkbox" name="archived" value="1" {{ if .ArchivedOnly }}checked{{ end }}> Archived</label>
16
+      <label><input type="checkbox" name="deleted" value="1" {{ if .DeletedOnly }}checked{{ end }}> Deleted</label>
17
+      <button type="submit" class="shithub-button">Filter</button>
18
+    </form>
19
+
20
+    {{ if .Repos }}
21
+    <table class="shithub-branches-table">
22
+      <thead><tr><th>ID</th><th>Owner</th><th>Name</th><th>Visibility</th><th>State</th><th>Disk</th><th></th></tr></thead>
23
+      <tbody>
24
+        {{ range .Repos }}
25
+        <tr>
26
+          <td>#{{ .ID }}</td>
27
+          <td>{{ if .OwnerUserUsername }}{{ .OwnerUserUsername }}{{ else if .OwnerOrgSlug }}{{ .OwnerOrgSlug }} (org){{ else }}<span class="shithub-fg-muted">—</span>{{ end }}</td>
28
+          <td><a href="/admin/repos/{{ .ID }}">{{ .Name }}</a></td>
29
+          <td><code>{{ .Visibility }}</code></td>
30
+          <td>{{ if .DeletedAt.Valid }}deleted{{ else if .IsArchived }}archived{{ else }}active{{ end }}</td>
31
+          <td>{{ .DiskUsedBytes }}</td>
32
+          <td><a href="/admin/repos/{{ .ID }}">View →</a></td>
33
+        </tr>
34
+        {{ end }}
35
+      </tbody>
36
+    </table>
37
+    {{ else }}
38
+    <p class="shithub-empty-note">No repositories match.</p>
39
+    {{ end }}
40
+
41
+    <nav class="shithub-pagination">
42
+      {{ if gt .Page 1 }}<a href="?page={{ .PrevPage }}">← Prev</a>{{ end }}
43
+      {{ if .HasMore }}<a href="?page={{ .NextPage }}">Next →</a>{{ end }}
44
+    </nav>
45
+  </div>
46
+</div>
47
+{{- end }}
internal/web/templates/admin/system.htmladded
@@ -0,0 +1,37 @@
1
+{{ define "page" -}}
2
+<div class="shithub-settings-page">
3
+  {{ template "admin-nav" . }}
4
+  <div class="shithub-settings-content">
5
+    <h1>System</h1>
6
+
7
+    <section class="shithub-settings-section">
8
+      <h2>Process</h2>
9
+      <ul>
10
+        <li>shithubd version: <code>{{ .Version }}</code></li>
11
+        <li>Go runtime: <code>{{ .GoVersion }}</code> ({{ .GoOS }}/{{ .GoArch }})</li>
12
+      </ul>
13
+    </section>
14
+
15
+    <section class="shithub-settings-section">
16
+      <h2>Database</h2>
17
+      <ul>
18
+        <li>Database: <code>{{ index .DB "name" }}</code> as <code>{{ index .DB "user" }}</code></li>
19
+        <li>Active connections: {{ index .DB "conns" }}</li>
20
+      </ul>
21
+      <h3>pgx pool</h3>
22
+      <ul>
23
+        <li>Acquired / idle / total / max: {{ index .Pool "acquired" }} / {{ index .Pool "idle" }} / {{ index .Pool "total" }} / {{ index .Pool "max" }}</li>
24
+        <li>Lifetime acquires: {{ index .Pool "acquireCount" }}</li>
25
+      </ul>
26
+    </section>
27
+
28
+    <section class="shithub-settings-section">
29
+      <h2>Repositories</h2>
30
+      <ul>
31
+        <li>Repos: {{ index .Repos "count" }}</li>
32
+        <li>Disk used (sum): {{ index .Repos "diskBytes" }} bytes</li>
33
+      </ul>
34
+    </section>
35
+  </div>
36
+</div>
37
+{{- end }}
internal/web/templates/admin/user_view.htmladded
@@ -0,0 +1,55 @@
1
+{{ define "page" -}}
2
+<div class="shithub-settings-page">
3
+  {{ template "admin-nav" . }}
4
+  <div class="shithub-settings-content">
5
+    <h1>{{ .User.Username }} <span class="shithub-fg-muted">#{{ .User.ID }}</span></h1>
6
+    {{ with .Notice }}<p class="shithub-flash shithub-flash-notice">{{ . }}</p>{{ end }}
7
+
8
+    <section class="shithub-settings-section">
9
+      <h2>Profile</h2>
10
+      <ul>
11
+        <li>Display name: {{ if .User.DisplayName }}{{ .User.DisplayName }}{{ else }}<span class="shithub-fg-muted">unset</span>{{ end }}</li>
12
+        <li>Email verified: {{ if .User.EmailVerified }}yes{{ else }}<span class="shithub-fg-muted">no</span>{{ end }}</li>
13
+        <li>Last login: {{ if .User.LastLoginAt.Valid }}{{ relativeTime .User.LastLoginAt.Time }}{{ else }}<span class="shithub-fg-muted">never</span>{{ end }}</li>
14
+        <li>Created: {{ relativeTime .User.CreatedAt.Time }}</li>
15
+        {{ if .User.SuspendedAt.Valid }}<li>Suspended {{ relativeTime .User.SuspendedAt.Time }} — <em>{{ .User.SuspendedReason.String }}</em></li>{{ end }}
16
+        {{ if .User.DeletedAt.Valid }}<li>Soft-deleted {{ relativeTime .User.DeletedAt.Time }}</li>{{ end }}
17
+        <li>Site admin: {{ if .User.IsSiteAdmin }}<strong>yes</strong>{{ else }}no{{ end }}</li>
18
+      </ul>
19
+    </section>
20
+
21
+    <section class="shithub-settings-section">
22
+      <h2>Actions</h2>
23
+      {{ if .User.SuspendedAt.Valid }}
24
+      <form method="POST" action="/admin/users/{{ .User.ID }}/unsuspend" style="display:inline">
25
+        <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
26
+        <button type="submit" class="shithub-button">Unsuspend</button>
27
+      </form>
28
+      {{ else }}
29
+      <form method="POST" action="/admin/users/{{ .User.ID }}/suspend">
30
+        <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
31
+        <label>Reason (visible to user) <input type="text" name="reason" maxlength="200"></label>
32
+        <button type="submit" class="shithub-button shithub-button-danger">Suspend</button>
33
+      </form>
34
+      {{ end }}
35
+
36
+      <form method="POST" action="/admin/users/{{ .User.ID }}/site-admin" style="display:inline">
37
+        <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
38
+        <button type="submit" class="shithub-button">{{ if .User.IsSiteAdmin }}Revoke admin{{ else }}Grant admin{{ end }}</button>
39
+      </form>
40
+
41
+      <form method="POST" action="/admin/users/{{ .User.ID }}/reset-password" style="display:inline">
42
+        <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
43
+        <button type="submit" class="shithub-button">Send password reset</button>
44
+      </form>
45
+
46
+      {{ if not .User.DeletedAt.Valid }}
47
+      <form method="POST" action="/admin/impersonate/{{ .User.ID }}" style="display:inline">
48
+        <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
49
+        <button type="submit" class="shithub-button">Impersonate (read-only)</button>
50
+      </form>
51
+      {{ end }}
52
+    </section>
53
+  </div>
54
+</div>
55
+{{- end }}
internal/web/templates/admin/users_list.htmladded
@@ -0,0 +1,46 @@
1
+{{ define "page" -}}
2
+<div class="shithub-settings-page">
3
+  {{ template "admin-nav" . }}
4
+  <div class="shithub-settings-content">
5
+    <h1>Users</h1>
6
+    {{ with .Notice }}<p class="shithub-flash shithub-flash-notice">{{ . }}</p>{{ end }}
7
+
8
+    <form method="GET" action="/admin/users" class="shithub-finder-form">
9
+      <input type="text" name="q" value="{{ .Q }}" placeholder="username prefix">
10
+      <label><input type="checkbox" name="suspended" value="1" {{ if .SuspendedOnly }}checked{{ end }}> Suspended</label>
11
+      <label><input type="checkbox" name="deleted" value="1" {{ if .DeletedOnly }}checked{{ end }}> Deleted</label>
12
+      <button type="submit" class="shithub-button">Filter</button>
13
+    </form>
14
+
15
+    {{ if .Users }}
16
+    <table class="shithub-branches-table">
17
+      <thead><tr><th>ID</th><th>Username</th><th>Display name</th><th>State</th><th>Last login</th><th></th></tr></thead>
18
+      <tbody>
19
+        {{ range .Users }}
20
+        <tr>
21
+          <td>#{{ .ID }}</td>
22
+          <td><a href="/admin/users/{{ .ID }}">{{ .Username }}</a></td>
23
+          <td>{{ .DisplayName }}</td>
24
+          <td>
25
+            {{ if .DeletedAt.Valid }}<span class="shithub-fg-muted">deleted</span>
26
+            {{ else if .SuspendedAt.Valid }}<span class="shithub-fg-muted">suspended</span>
27
+            {{ else }}active{{ end }}
28
+            {{ if .IsSiteAdmin }}<span class="shithub-pill">admin</span>{{ end }}
29
+          </td>
30
+          <td>{{ if .LastLoginAt.Valid }}{{ relativeTime .LastLoginAt.Time }}{{ else }}<span class="shithub-fg-muted">never</span>{{ end }}</td>
31
+          <td><a href="/admin/users/{{ .ID }}">View →</a></td>
32
+        </tr>
33
+        {{ end }}
34
+      </tbody>
35
+    </table>
36
+    {{ else }}
37
+    <p class="shithub-empty-note">No users match.</p>
38
+    {{ end }}
39
+
40
+    <nav class="shithub-pagination">
41
+      {{ if gt .Page 1 }}<a href="?page={{ .PrevPage }}">← Prev</a>{{ end }}
42
+      {{ if .HasMore }}<a href="?page={{ .NextPage }}">Next →</a>{{ end }}
43
+    </nav>
44
+  </div>
45
+</div>
46
+{{- end }}