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
1package main
2
3import (
4 "html/template"
5 "net/http"
6 "strings"
7 "time"
8)
9
10var templateFuncs = template.FuncMap{
11 "ago": ago,
12 "when": when,
13 "eventLabel": eventLabel,
14 "eventClass": eventClass,
15 "browser": browser,
16 "pips": pips,
17 "dict": dict,
18}
19
20// dict builds a map inline, so a partial can take named arguments rather than
21// one positional value.
22func dict(pairs ...any) map[string]any {
23 out := make(map[string]any, len(pairs)/2)
24 for i := 0; i+1 < len(pairs); i += 2 {
25 key, ok := pairs[i].(string)
26 if !ok {
27 continue
28 }
29 out[key] = pairs[i+1]
30 }
31 return out
32}
33
34// pips renders the recovery code meter as ten lit or unlit marks, because eight
35// of ten is read at a glance and the number 8 is not.
36func pips(remaining int) []bool {
37 out := make([]bool, recoveryCount)
38 for i := range out {
39 out[i] = i < remaining
40 }
41 return out
42}
43
44// when is the one timestamp format on this site. Everything here is UTC,
45// because a container's local time silently differs from the host's.
46func when(t time.Time) string { return t.UTC().Format("2006-01-02 15:04 UTC") }
47
48func ago(t time.Time) string {
49 d := time.Since(t)
50 switch {
51 case d < time.Minute:
52 return "just now"
53 case d < time.Hour:
54 return itoa(int(d.Minutes())) + "m ago"
55 case d < 24*time.Hour:
56 return itoa(int(d.Hours())) + "h ago"
57 default:
58 return itoa(int(d.Hours()/24)) + "d ago"
59 }
60}
61
62func itoa(n int) string {
63 if n == 0 {
64 return "0"
65 }
66 var b [20]byte
67 i := len(b)
68 for n > 0 {
69 i--
70 b[i] = byte('0' + n%10)
71 n /= 10
72 }
73 return string(b[i:])
74}
75
76// eventLabel turns a stored kind into something readable without changing the
77// stored value, which stays machine-facing so a query written today still runs.
78func eventLabel(kind string) string {
79 switch kind {
80 case evCodeRequested:
81 return "code requested"
82 case evCodeSent:
83 return "code sent"
84 case evCodeFailed:
85 return "wrong code"
86 case evCodeExpired:
87 return "code expired"
88 case evLogin:
89 return "signed in"
90 case evLogout:
91 return "signed out"
92 case evSessionRevoked:
93 return "session revoked"
94 case evRecoveryUsed:
95 return "recovery code used"
96 case evRecoveryFailed:
97 return "wrong recovery code"
98 case evRecoveryRotated:
99 return "recovery codes replaced"
100 case evRateLimited:
101 return "rate limited"
102 case evCeilingHit:
103 return "send ceiling hit"
104 case evUsernameChanged:
105 return "username changed"
106 }
107 return strings.ReplaceAll(kind, "_", " ")
108}
109
110// eventClass colours the ones worth noticing on the activity page.
111func eventClass(kind string) string {
112 switch kind {
113 case evCodeFailed, evRecoveryFailed, evRateLimited, evCeilingHit:
114 return "is-warn"
115 case evLogin, evRecoveryUsed, evRecoveryRotated, evUsernameChanged:
116 return "is-note"
117 }
118 return ""
119}
120
121// browser reduces a user agent to something that fits in a table cell. It is
122// display only and guesses, so it never claims more than the family.
123func browser(ua string) string {
124 switch {
125 case ua == "":
126 return "unknown"
127 case strings.Contains(ua, "Firefox/"):
128 return "Firefox"
129 case strings.Contains(ua, "Edg/"):
130 return "Edge"
131 case strings.Contains(ua, "OPR/"):
132 return "Opera"
133 case strings.Contains(ua, "Chrome/"):
134 return "Chrome"
135 case strings.Contains(ua, "Safari/"):
136 return "Safari"
137 case strings.Contains(ua, "curl/"):
138 return "curl"
139 }
140 return "other"
141}
142
143// Inline rather than a file so it cannot drift from the navbar logo, which is
144// the same markup. A north star with three companions, which is the same
145// language as the field behind the sign in form and still reads at 16px, where
146// a full constellation turns to mush.
147const faviconSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
148 <g stroke="#6b9e78" stroke-width="1.6" opacity="0.55" stroke-linecap="round">
149 <path d="M46 15 L34 28"/>
150 <path d="M18 47 L29 36"/>
151 <path d="M46 15 L52 40"/>
152 </g>
153 <path d="M32 4 L36.4 26.2 L58 32 L36.4 37.8 L32 60 L27.6 37.8 L6 32 L27.6 26.2 Z"
154 fill="#6b9e78"/>
155 <circle cx="46" cy="15" r="3.4" fill="#c9d9cb"/>
156 <circle cx="18" cy="47" r="2.6" fill="#7eaab8"/>
157 <circle cx="52" cy="40" r="2.2" fill="#c9a84c"/>
158</svg>`
159
160func favicon(w http.ResponseWriter, r *http.Request) {
161 w.Header().Set("Content-Type", "image/svg+xml")
162 w.Header().Set("Cache-Control", "public, max-age=86400")
163 _, _ = w.Write([]byte(faviconSVG))
164}
165
166// robots refuses the whole site. There is nothing here worth indexing and the
167// login form is the only page a stranger can reach at all.
168func robots(w http.ResponseWriter, r *http.Request) {
169 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
170 _, _ = w.Write([]byte("User-agent: *\nDisallow: /\n"))
171}