repos
/ orchard main

orchard

mirror

Every 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

14.6 KB · 328 lines · HTML Raw History
  1{{ define "extra_js" }}
  2{{/* Chart data as inline JSON blocks, which is what the CSP's 'unsafe-inline'
  3     in script-src is for. property_crawl_status.js takes the property id from
  4     #monitoring-status[data-property-id]. */}}
  5<script id="chart-status-response-times-data" type="application/json">{{ json .ResponseTimes }}</script>
  6<script id="chart-status-codes-data" type="application/json">{{ json .StatusCodes }}</script>
  7<script id="chart-uptime-data" type="application/json">{{ json .UptimeSlices }}</script>
  8{{ end }}
  9
 10{{ define "breadcrumbs" }}
 11<nav aria-label="breadcrumb">
 12  <ol class="breadcrumb mb-0">
 13    <li class="breadcrumb-item"><a href="/">Home</a></li>
 14    {{ if .Authenticated }}<li class="breadcrumb-item"><a href="/properties">Properties</a></li>{{ end }}
 15    <li class="breadcrumb-item active" aria-current="page">{{ .Property.Name }}</li>
 16  </ol>
 17</nav>
 18{{ end }}
 19
 20{{ define "main" }}
 21{{ $authed := .Authenticated }}
 22{{ with .Property }}
 23<div class="bg-deep border-bottom border-subtle py-4">
 24  <div class="container">
 25    <div class="row align-items-center g-3">
 26      <div class="col-12 col-lg-7">
 27        <div class="section-label mb-2">property · <span class="{{ if eq .CurrentStatus 200 }}text-green{{ else }}text-terracotta{{ end }}">{{ if eq .CurrentStatus 200 }}online{{ else }}offline{{ end }}</span></div>
 28        <h1 class="mb-2 text-white fw-bolder d-flex align-items-center gap-3" style="letter-spacing: -0.01em; min-width: 0;">
 29          <span class="text-truncate">{{ .Name }}</span>
 30          <span class="status-dot status-dot-lg {{ if eq .CurrentStatus 200 }}is-up{{ else }}is-down{{ end }}" aria-hidden="true"></span>
 31        </h1>
 32        <a href="{{ .URL }}" target="_blank" rel="noopener" class="property-url-link">
 33          <span class="property-url-text">{{ .URL }}</span>
 34          <span class="property-url-arrow" aria-hidden="true"></span>
 35        </a>
 36      </div>
 37      <div class="col-12 col-lg-5 d-flex flex-wrap gap-2 justify-content-lg-end align-items-center d-print-none">
 38        {{ if $authed }}
 39          <span id="is-public-form" class="d-inline-flex align-items-center gap-2" data-property-id="{{ .ID }}">
 40            <span class="toggle-label" title="Public properties are viewable by anyone with the URL; no login required.">Visibility</span>
 41            <label class="toggle-pill">
 42              <input type="checkbox" name="is_public" id="is-public-switch"{{ if .IsPublic }} checked{{ end }}>
 43              <span class="toggle-pill-seg toggle-pill-private">Private</span>
 44              <span class="toggle-pill-seg toggle-pill-public">Public</span>
 45            </label>
 46          </span>
 47          <a href="/{{ .ID }}?report=pdf" target="_blank" class="btn btn-sm btn-outline-light">Report · pdf</a>
 48          <a href="/{{ .ID }}?report=md" target="_blank" class="btn btn-sm btn-outline-light">Report · md</a>
 49          {{ if not .IsProtected }}
 50          <form method="POST" action="/properties/{{ .ID }}/delete" class="d-inline" onsubmit="return confirm('Delete {{ .URL }}?');">
 51            <button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
 52          </form>
 53          {{ end }}
 54        {{ end }}
 55      </div>
 56    </div>
 57  </div>
 58</div>
 59
 60<div class="container my-4">
 61  <div class="section-label mb-2">live signals</div>
 62  <div class="row g-3 mb-4">
 63    <div class="col-6 col-md-3">
 64      <div class="metric-tile {{ if eq .CurrentStatus 200 }}metric-accent-green{{ else }}metric-accent-danger{{ end }}">
 65        <div class="metric-label">http status</div>
 66        <div class="metric-value">{{ .CurrentStatus }}</div>
 67        <div class="metric-sub">{{ if eq .CurrentStatus 200 }}OK · all clear{{ else }}failing{{ end }}</div>
 68      </div>
 69    </div>
 70    <div class="col-6 col-md-3">
 71      <div class="metric-tile {{ if .InvalidCert }}metric-accent-danger{{ else }}metric-accent-green{{ end }}">
 72        <div class="metric-label">tls cert</div>
 73        <div class="metric-value" style="font-size: 1.4rem;">{{ if .InvalidCert }}invalid{{ else }}valid{{ end }}</div>
 74        <div class="metric-sub">chain + trust</div>
 75      </div>
 76    </div>
 77    <div class="col-6 col-md-3">
 78      <div class="metric-tile {{ if .HasSecurityIssue }}metric-accent-amber{{ else }}metric-accent-green{{ end }}">
 79        <div class="metric-label">security</div>
 80        <div class="metric-value" style="font-size: 1.4rem;">{{ if .HasSecurityIssue }}issues{{ else }}pass{{ end }}</div>
 81        <div class="metric-sub">header scan</div>
 82      </div>
 83    </div>
 84    <div class="col-6 col-md-3">
 85      <div class="metric-tile {{ if gt .AvgResponseTime 500 }}metric-accent-danger{{ else }}metric-accent-green{{ end }}">
 86        <div class="metric-label">response</div>
 87        <div class="metric-value">{{ .AvgResponseTime }}<span class="text-muted" style="font-size: 0.9rem;"> ms</span></div>
 88        <div class="metric-sub">rolling avg</div>
 89      </div>
 90    </div>
 91  </div>
 92
 93  {{ with .LighthouseScores }}
 94  <div class="section-label mb-2">lighthouse</div>
 95  <div class="row g-3 mb-4">
 96    {{ range .Pairs }}
 97    <div class="col-6 col-md-3">
 98      <div class="metric-tile metric-accent-{{ if ge .Score 90 }}green{{ else if ge .Score 50 }}amber{{ else }}danger{{ end }}">
 99        <div class="metric-label">{{ .Label }}</div>
100        <div class="metric-value">{{ .Score }}<span class="text-muted" style="font-size: 0.9rem;">%</span></div>
101      </div>
102    </div>
103    {{ end }}
104  </div>
105  {{ end }}
106</div>
107
108<div class="container my-4">
109  <div class="row g-3">
110    <div class="col-12 col-md-8">
111      <div class="chart-panel mb-3">
112        <div class="chart-panel-header">
113          <span class="chart-panel-title">response time · last 31 checks</span>
114        </div>
115        <div style="position: relative; height: 300px;">
116          <canvas id="chart-response-times"></canvas>
117        </div>
118      </div>
119      <div class="row g-3">
120        <div class="col-12 col-md-6">
121          <div class="chart-panel">
122            <div class="chart-panel-header">
123              <span class="chart-panel-title">status codes</span>
124            </div>
125            <canvas id="chart-status-codes"></canvas>
126          </div>
127        </div>
128        <div class="col-12 col-md-6">
129          <div class="chart-panel">
130            <div class="chart-panel-header">
131              <span class="chart-panel-title">uptime split</span>
132            </div>
133            <canvas id="chart-uptime"></canvas>
134          </div>
135        </div>
136      </div>
137    </div>
138    <div class="col-12 col-md-4">
139      <div class="check-list">
140        <div class="check-list-header">security · header scan</div>
141        <div class="check-list-item">
142          <span class="check-name">Use HTTPS</span>
143          <span class="chip {{ if .IsHTTPS }}chip-ok{{ else }}chip-down{{ end }}">{{ if .IsHTTPS }}pass{{ else }}fail{{ end }}</span>
144        </div>
145        <div class="check-list-item">
146          <span class="check-name">Set MIME types</span>
147          <span class="chip {{ if .HasMIMEType }}chip-ok{{ else }}chip-down{{ end }}">{{ if .HasMIMEType }}pass{{ else }}fail{{ end }}</span>
148        </div>
149        <div class="check-list-item">
150          <span class="check-name">Content sniff protection</span>
151          <span class="chip {{ if .HasContentSniffProtection }}chip-ok{{ else }}chip-down{{ end }}">{{ if .HasContentSniffProtection }}pass{{ else }}fail{{ end }}</span>
152        </div>
153        <div class="check-list-item">
154          <span class="check-name">Clickjack protection</span>
155          <span class="chip {{ if .HasClickjackProtection }}chip-ok{{ else }}chip-down{{ end }}">{{ if .HasClickjackProtection }}pass{{ else }}fail{{ end }}</span>
156        </div>
157        <div class="check-list-item">
158          <span class="check-name">Hide server version</span>
159          <span class="chip {{ if .HidesServerVersion }}chip-ok{{ else }}chip-down{{ end }}">{{ if .HidesServerVersion }}pass{{ else }}fail{{ end }}</span>
160        </div>
161        <div class="check-list-item">
162          <span class="check-name">HSTS enabled</span>
163          <span class="chip {{ if .HasHSTS }}chip-ok{{ else }}chip-down{{ end }}">{{ if .HasHSTS }}pass{{ else }}fail{{ end }}</span>
164        </div>
165        <div class="check-list-item">
166          <span class="check-name">HSTS preload</span>
167          <span class="chip {{ if .HasHSTSPreload }}chip-ok{{ else }}chip-down{{ end }}">{{ if .HasHSTSPreload }}pass{{ else }}fail{{ end }}</span>
168        </div>
169      </div>
170    </div>
171  </div>
172</div>
173
174{{ if $authed }}
175<div class="container my-4 d-print-none" id="monitoring-status" data-property-id="{{ .ID }}">
176  <div class="section-label mb-2">scheduled audits</div>
177  <div class="row g-3">
178    <div class="col-12 col-md-6">
179      <div class="monitor-card h-100">
180        <div class="monitor-header">
181          <span class="monitor-title">Crawler</span>
182          <span class="d-flex align-items-center gap-2">
183            <span class="chip chip-muted" data-field="crawler.state_badge">&nbsp;</span>
184            <button type="button" id="recrawl-btn" class="btn btn-sm btn-outline-light monitor-btn" title="Recrawl now">
185              <span class="recrawl-btn-label">Recrawl</span>
186              <span class="recrawl-btn-spinner spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
187            </button>
188          </span>
189        </div>
190        <div class="monitor-body">
191          <div class="progress mb-3 d-none" data-field="crawler.progress_wrap" style="height: 4px;">
192            <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" data-field="crawler.progress_bar" style="width: 0%"></div>
193          </div>
194          <div class="alert alert-danger py-2 mb-3 d-none small" data-field="crawler.error_box" role="alert">
195            <strong>Last crawl failed:</strong>
196            <code data-field="crawler.error_text"></code>
197          </div>
198          <dl>
199            <dt>Last success</dt>
200            <dd data-field="crawler.last_success"></dd>
201            <dt>Last attempt</dt>
202            <dd data-field="crawler.last_attempt"></dd>
203            <dt>Pages crawled</dt>
204            <dd data-field="crawler.pages"></dd>
205            <dt>Duration</dt>
206            <dd data-field="crawler.duration"></dd>
207            <dt>Issues found</dt>
208            <dd data-field="crawler.insights"></dd>
209            <dt>Next run</dt>
210            <dd data-field="crawler.next_run"></dd>
211          </dl>
212        </div>
213      </div>
214    </div>
215    <div class="col-12 col-md-6">
216      <div class="monitor-card h-100">
217        <div class="monitor-header">
218          <span class="monitor-title">Lighthouse</span>
219          <span class="d-flex align-items-center gap-2">
220            <span class="chip chip-muted" data-field="lighthouse.state_badge">&nbsp;</span>
221            <button type="button" id="rerun-lighthouse-btn" class="btn btn-sm btn-outline-light monitor-btn" title="Rerun Lighthouse now">
222              <span class="rerun-lh-label">Rerun</span>
223              <span class="rerun-lh-spinner spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
224            </button>
225          </span>
226        </div>
227        <div class="monitor-body">
228          <div class="alert alert-warning py-2 mb-3 d-none small" data-field="lighthouse.error_box" role="alert">
229            <strong>Last Lighthouse run failed:</strong>
230            <code data-field="lighthouse.error_text"></code>
231          </div>
232          <dl>
233            <dt>Last success</dt>
234            <dd data-field="lighthouse.last_success"></dd>
235            <dt>Last attempt</dt>
236            <dd data-field="lighthouse.last_attempt"></dd>
237            <dt>Duration</dt>
238            <dd data-field="lighthouse.duration"></dd>
239            <dt>Next run</dt>
240            <dd data-field="lighthouse.next_run"></dd>
241          </dl>
242        </div>
243      </div>
244    </div>
245  </div>
246</div>
247{{ end }}
248
249{{ with .LighthouseDetails }}
250<div class="container my-4">
251  <div class="section-label mb-2">performance breakdown</div>
252  <p class="text-muted small mb-3">
253    Lighthouse combines these weighted metrics to produce the Performance score. Opportunities below are the biggest wins; savings are estimated against the audited URL only.
254  </p>
255
256  {{ if .Metrics }}
257  <div class="row g-3 mb-4">
258    {{ range .Metrics }}
259    <div class="col-6 col-md d-flex">
260      <div class="metric-tile w-100 metric-accent-{{ metricClass .Score }}">
261        <div class="metric-label">{{ .Acronym }} · {{ .Weight }}%</div>
262        <div class="metric-value" style="font-size: 1.2rem;">{{ if .DisplayValue }}{{ .DisplayValue }}{{ else }}—{{ end }}</div>
263        <div class="metric-sub" title="{{ .Title }}">{{ .Title }}</div>
264      </div>
265    </div>
266    {{ end }}
267  </div>
268  {{ end }}
269
270  {{ if .Opportunities }}
271  <div class="section-label mb-2">top opportunities</div>
272  <div class="insight-header insight--3col">
273    <span>Audit</span>
274    <span>Detail</span>
275    <span class="text-end">Est. savings</span>
276  </div>
277  {{ range .Opportunities }}
278  <div class="insight-row insight--3col">
279    <span class="insight-col-trunc" title="{{ .Title }}">{{ .Title }}</span>
280    <span class="insight-col-trunc text-muted small" title="{{ .DisplayValue }}">
281      {{ if .DisplayValue }}{{ .DisplayValue }}{{ else }}—{{ end }}
282    </span>
283    <span class="text-end small">
284      {{ $saved := msSavings .SavingsMS }}
285      {{ if $saved }}<strong class="text-amber">{{ $saved }}</strong>{{ else }}<span class="text-muted"></span>{{ end }}
286    </span>
287  </div>
288  {{ end }}
289  {{ else }}
290  <div class="alert alert-info py-2 small">No actionable opportunities; everything passing at this URL.</div>
291  {{ end }}
292</div>
293{{ end }}
294{{ end }}
295
296{{ if .InsightGroups }}
297<div class="container my-4">
298  {{ range .InsightGroups }}
299  <div class="section-label mb-2">{{ .Type }} · <span class="text-muted">{{ len .Items }}</span></div>
300  <div class="insight-header">
301    <span>Severity</span>
302    <span>URL</span>
303    <span>Issue</span>
304    <span>Item</span>
305  </div>
306  {{ range .Items }}
307  <div class="insight-row">
308    <span>
309      <span class="chip {{ if eq .Severity "error" }}chip-down{{ else if eq .Severity "warning" }}chip-warn{{ else }}chip-info{{ end }}">
310        {{ upper .Severity }}
311      </span>
312    </span>
313    <span class="insight-col-trunc">
314      <a href="{{ .URL }}" target="_blank" rel="noopener" class="text-muted">{{ urlPath .URL }}</a>
315    </span>
316    <span class="insight-col-trunc" title="{{ .Issue }}">{{ .Issue }}</span>
317    <span class="insight-col-trunc text-muted small" title="{{ .Item }}">{{ .Item }}</span>
318  </div>
319  {{ end }}
320  {{ end }}
321</div>
322{{ else if .Property.LastCrawlError }}
323<div class="container my-4">
324  <div class="alert alert-danger py-2 small mb-0">Last crawl failed: <code>{{ .Property.LastCrawlError }}</code></div>
325</div>
326{{ end }}
327{{ end }}