orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1<!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<title>History · search</title>
7<link rel="stylesheet" href="{{ asset "static/app.css" }}">
8</head>
9<body>
10<div class="crt"></div>
11<div class="vignette"></div>
12
13<header class="bar">
14 <div class="barin">
15 <a class="brand" href="/">
16 <span class="key"><span class="cursor"></span></span>
17 <span>search</span>
18 </a>
19 <span class="where">History</span>
20 <span class="spacer"></span>
21 <span class="counts"><b>{{ num .Total }}</b> kept · <b>{{ num .Rated }}</b> rated</span>
22 <a class="ghost" href="/search">ask</a>
23 </div>
24</header>
25
26<main>
27 <p class="introline">Every question that was not asked in incognito, with what it
28 answered and how it was rated. This is the only thing here that holds a question,
29 and it is the only page that can delete one.</p>
30
31 <div class="filters">
32 <a href="/history" class="{{ if eq .Only "" }}on{{ end }}">all</a>
33 <a href="/history?only=down" class="{{ if eq .Only "down" }}on{{ end }}">thumbed down</a>
34 <a href="/history?only=up" class="{{ if eq .Only "up" }}on{{ end }}">thumbed up</a>
35 <a href="/history?only=unrated" class="{{ if eq .Only "unrated" }}on{{ end }}">unrated</a>
36 <span class="spacer"></span>
37 <button class="danger" id="forgetall">delete everything</button>
38 </div>
39
40 {{ if not .Entries }}
41 <p class="faint" style="margin-top:1.4rem">Nothing here yet.</p>
42 {{ end }}
43
44 <ul class="hist">
45 {{ range .Entries }}
46 <li class="hrow" data-id="{{ .ID }}">
47 <details>
48 <summary>
49 <span class="hq">{{ .Question }}</span>
50 {{ if .Good }}<span class="verdict up">good</span>{{ end }}
51 {{ if .Bad }}<span class="verdict down">{{ if .Reason }}{{ reason .Reason }}{{ else }}bad{{ end }}</span>{{ end }}
52 <span class="label">{{ if .Skill }}{{ .Skill }}{{ else }}{{ .Shape }}{{ end }}</span>
53 <span class="hwhen">{{ .Asked.Format "2 Jan, 3:04 PM" }}</span>
54 </summary>
55 <div class="hbody">
56 <div class="answer prose">{{ .Body }}</div>
57
58 {{ if .Sources }}
59 <div class="hmeta">
60 <span class="label">sources</span>
61 {{ range .Sources }}<a class="chip" href="{{ .URL }}" rel="noreferrer">{{ hostname .URL }}</a>{{ end }}
62 </div>
63 {{ end }}
64
65 {{ if .Queries }}
66 <div class="hmeta">
67 <span class="label">searched</span>
68 {{ range .Queries }}<span class="chip">{{ . }}</span>{{ end }}
69 </div>
70 {{ end }}
71
72 <div class="hmeta">
73 <span class="stamp">{{ .Elapsed }} · {{ .Checked }} checked · {{ .Model }} · prompts {{ .Prompts }}</span>
74 <span class="spacer"></span>
75 <button class="danger forget">delete</button>
76 </div>
77 </div>
78 </details>
79 </li>
80 {{ end }}
81 </ul>
82</main>
83
84<script>
85// Deleting is the point of the page, so it asks once and then actually removes
86// the row rather than leaving it there looking deleted.
87async function forget(body) {
88 const res = await fetch("/forget", {
89 method: "POST",
90 headers: { "Content-Type": "application/json" },
91 body: JSON.stringify(body),
92 });
93 return res.ok;
94}
95
96document.querySelectorAll(".forget").forEach((b) => {
97 b.addEventListener("click", async () => {
98 const row = b.closest(".hrow");
99 if (!confirm("Delete this question and its answer?")) return;
100 if (await forget({ id: Number(row.dataset.id) })) row.remove();
101 });
102});
103
104document.getElementById("forgetall").addEventListener("click", async () => {
105 if (!confirm("Delete every question and answer kept here? This cannot be undone.")) return;
106 if (await forget({ all: true })) location.reload();
107});
108</script>
109</body>
110</html>