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

12.9 KB · 267 lines · HTML Raw History
  1{{ define "extra_js" }}
  2{{/* Chart data as inline JSON blocks, read by properties/index.js. The json
  3     helper marshals with HTML escaping on, so a page URL containing
  4     "</script>" cannot close the block. */}}
  5{{ with .Dash }}
  6<script id="chart-total-events-data" type="application/json">{{ json .Graph }}</script>
  7<script id="chart-total-events-by-browser-data" type="application/json">{{ json .ByBrowser }}</script>
  8<script id="chart-total-events-by-screen-size-data" type="application/json">{{ json .ByScreenSize }}</script>
  9<script id="chart-total-events-by-device-data" type="application/json">{{ json .ByDevice }}</script>
 10<script id="chart-total-events-by-platform-data" type="application/json">{{ json .ByPlatform }}</script>
 11<script id="map-session-starts-by-country" type="application/json">{{ json .SessionsByCountry }}</script>
 12<script id="map-session-starts-by-country-region" type="application/json">{{ json .SessionsByCountryRegion }}</script>
 13{{ end }}
 14<script type="module" src="{{ .PageScript }}"></script>
 15{{ end }}
 16
 17{{ define "breadcrumbs" }}
 18<nav aria-label="breadcrumb">
 19  <ol class="breadcrumb mb-0">
 20    <li class="breadcrumb-item"><a href="/">Home</a></li>
 21    <li class="breadcrumb-item"><a href="/properties">Properties</a></li>
 22    <li class="breadcrumb-item active" aria-current="page">{{ .Title }}</li>
 23  </ol>
 24</nav>
 25{{ end }}
 26
 27{{ define "main" }}
 28{{ $authed := .Authenticated }}
 29{{ $baseURL := .BaseURL }}
 30{{ $p := .Property }}
 31{{ with .Dash }}
 32<div class="bg-deep border-bottom border-subtle py-4">
 33  <div class="container">
 34    <div class="row align-items-center g-3">
 35      <div class="col-12 col-lg-6">
 36        <div class="section-label mb-2">property · <span class="text-green">{{ .LiveUsers }} live</span></div>
 37        <h1 class="mb-1 text-white fw-bolder d-flex align-items-center gap-3" style="letter-spacing: -0.01em; min-width: 0;">
 38          <span class="text-truncate">{{ $p.Name }}</span>
 39          <span class="status-dot status-dot-lg {{ if .LiveUsers }}is-up{{ else }}is-idle{{ end }}" aria-hidden="true"></span>
 40        </h1>
 41        <div class="text-muted small" style="font-family: var(--bs-font-monospace);">{{ $p.ID }}</div>
 42      </div>
 43      <div class="col-12 col-lg-6 d-flex flex-wrap gap-2 justify-content-lg-end align-items-center d-print-none">
 44        {{ if $authed }}
 45        <form id="is-public-form" method="POST" action="/properties/{{ $p.ID }}/public" class="d-inline-flex align-items-center gap-2">
 46          <span class="toggle-label" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Anyone with the URL can view a public property, and no login is needed.">Visibility</span>
 47          <label class="toggle-pill">
 48            <input type="checkbox" name="is_public" id="is-public-switch" {{ if $p.IsPublic }}checked{{ end }}>
 49            <span class="toggle-pill-seg toggle-pill-private">Private</span>
 50            <span class="toggle-pill-seg toggle-pill-public">Public</span>
 51          </label>
 52        </form>
 53        <button type="button" class="btn btn-sm btn-outline-light" data-bs-toggle="modal" data-bs-target="#siteTagModal">
 54          Site tag
 55        </button>
 56        <a href="/{{ $p.ID }}?report" target="_blank" class="btn btn-sm btn-outline-light">Report · pdf</a>
 57        <a href="/{{ $p.ID }}?report=md" target="_blank" class="btn btn-sm btn-outline-light">Report · md</a>
 58        {{ if not $p.IsProtected }}
 59        <button type="button" class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#delete-modal-{{ $p.ID }}">
 60          Delete
 61        </button>
 62        <div class="modal fade" id="delete-modal-{{ $p.ID }}" tabindex="-1" aria-hidden="true">
 63          <div class="modal-dialog">
 64            <div class="modal-content">
 65              <div class="modal-header">
 66                <h5 class="modal-title text-white">Confirm delete</h5>
 67                <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
 68              </div>
 69              <div class="modal-body">
 70                Delete <strong class="text-white">{{ $p.Name }}</strong>? All collected events will be removed.
 71              </div>
 72              <div class="modal-footer">
 73                <button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">Cancel</button>
 74                <form method="post" action="/properties/{{ $p.ID }}/delete" class="d-inline">
 75                  <button type="submit" class="btn btn-outline-danger">Delete</button>
 76                </form>
 77              </div>
 78            </div>
 79          </div>
 80        </div>
 81        {{ end }}
 82        {{ end }}
 83      </div>
 84    </div>
 85
 86    <div class="row mt-3 g-3 align-items-end d-print-none">
 87      {{ if .FilterURL }}
 88      <div class="col-12">
 89        <span class="filter-chip">
 90          <span class="section-label" style="letter-spacing: 0.1em;">filter · url</span>
 91          <span class="filter-chip-label">{{ .FilterURL }}</span>
 92          <button type="button" class="filter-chip-clear btn-filter-clear" data-filter-key="filter_url" data-filter-value="{{ .FilterURL }}" aria-label="Clear filter"></button>
 93        </span>
 94      </div>
 95      {{ end }}
 96      <div class="col-12 col-lg-8 offset-lg-4">
 97        <form method="GET">
 98          <div class="date-range-form">
 99            <div class="form-floating">
100              <input type="date" name="date_start" id="date-start" class="form-control" value="{{ .DateStart }}" />
101              <label for="date-start">Date start</label>
102            </div>
103            <div class="form-floating">
104              <input type="date" name="date_end" id="date-end" class="form-control" value="{{ .DateEnd }}" />
105              <label for="date-end">Date end</label>
106            </div>
107            <div class="form-floating">
108              <select name="date_range" id="date-range" class="form-select">
109                <option value="custom">Custom</option>
110                {{ $r := .DateRange }}
111                <option value="7" {{ if eq $r 7 }}selected{{ end }}>7 days</option>
112                <option value="14" {{ if eq $r 14 }}selected{{ end }}>14 days</option>
113                <option value="28" {{ if eq $r 28 }}selected{{ end }}>28 days</option>
114                <option value="90" {{ if eq $r 90 }}selected{{ end }}>3 months</option>
115                <option value="180" {{ if eq $r 180 }}selected{{ end }}>6 months</option>
116                <option value="365" {{ if eq $r 365 }}selected{{ end }}>1 year</option>
117              </select>
118              <label for="date-range">Date range</label>
119            </div>
120          </div>
121        </form>
122      </div>
123    </div>
124  </div>
125</div>
126
127<div class="container my-4 position-relative">
128  <div class="d-flex align-items-center justify-content-between mb-2">
129    <div class="section-label">metrics · period vs previous</div>
130    {{ if and $authed .CustomEvents }}
131    <div class="dropdown d-print-none">
132      <button class="btn btn-sm btn-outline-light dropdown-toggle" type="button" id="customCards" data-bs-toggle="dropdown" aria-expanded="false">
133        Custom cards
134      </button>
135      <div class="dropdown-menu dropdown-menu-end p-3" style="width: 300px;" aria-labelledby="customCards">
136        <form id="custom-card-form" method="POST" action="/properties/{{ $p.ID }}/cards">
137          {{ range .CustomEvents }}
138          <div class="form-check form-switch">
139            <input class="form-check-input" type="checkbox" role="switch" name="{{ .Event }}" id="{{ .Event }}-switch" {{ if .Active }}checked{{ end }}>
140            <label class="form-check-label small" for="{{ .Event }}-switch">{{ .Event }}</label>
141          </div>
142          {{ end }}
143        </form>
144      </div>
145    </div>
146    {{ end }}
147  </div>
148
149  <div class="row g-3">
150    {{ range .EventCards }}
151    <div class="col-6 col-md-4 col-lg-3">
152      <div class="metric-tile">
153        <div class="metric-header">
154          <div class="metric-label text-truncate" {{ if .HelpText }}data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ .HelpText }}"{{ end }}>
155            {{ .Name }}
156          </div>
157          {{ if eq .PercentChange 0 }}
158          <span class="metric-delta is-flat" title="No change vs previous period">·</span>
159          {{ else if lt .PercentChange 0 }}
160          <span class="metric-delta is-down">{{ .PercentChange }}%</span>
161          {{ else }}
162          <span class="metric-delta is-up">+{{ .PercentChange }}%</span>
163          {{ end }}
164        </div>
165        <div class="metric-value">{{ .Value }}</div>
166      </div>
167    </div>
168    {{ end }}
169  </div>
170</div>
171
172<div class="container my-4">
173  <div class="row g-3">
174    <div class="col-12 col-md-8">
175      <div class="chart-panel mb-3">
176        <div class="chart-panel-header">
177          <span class="chart-panel-title">events over time</span>
178        </div>
179        <div class="chart-panel-body" style="height: 320px;">
180          <canvas id="chart-total-events"></canvas>
181        </div>
182      </div>
183      <div class="chart-panel">
184        <div class="chart-panel-header chart-panel-header-with-action">
185          <span class="chart-panel-title" id="map-title">sessions · world</span>
186          <button type="button" id="map-back" class="chart-panel-action" hidden>← back to world</button>
187        </div>
188        <div class="chart-panel-body">
189          <div id="map" style="min-height: 320px; position: relative;"></div>
190        </div>
191      </div>
192    </div>
193    <div id="doughnut-graphs" class="col-12 col-md-4">
194      <div class="chart-panel mb-3">
195        <div class="chart-panel-header"><span class="chart-panel-title">device</span></div>
196        <div class="chart-panel-body"><canvas id="chart-total-events-by-device"></canvas></div>
197      </div>
198      <div class="chart-panel mb-3">
199        <div class="chart-panel-header"><span class="chart-panel-title">browser</span></div>
200        <div class="chart-panel-body"><canvas id="chart-total-events-by-browser"></canvas></div>
201      </div>
202      <div class="chart-panel mb-3">
203        <div class="chart-panel-header"><span class="chart-panel-title">platform</span></div>
204        <div class="chart-panel-body"><canvas id="chart-total-events-by-platform"></canvas></div>
205      </div>
206      <div class="chart-panel mb-3">
207        <div class="chart-panel-header"><span class="chart-panel-title">screen size</span></div>
208        <div class="chart-panel-body"><canvas id="chart-total-events-by-screen-size"></canvas></div>
209      </div>
210    </div>
211  </div>
212</div>
213
214<div class="container my-4">
215  <div class="section-label mb-2">top lists · ranked</div>
216  <div id="top-lists" class="row g-3">
217    {{ template "rankList" dict "title" "top pages · page views" "col" "count" "items" .PageViewsByPageURL "filterFor" $p.ID }}
218    {{ template "rankList" dict "title" "top pages · all events" "col" "count" "items" .ByPageURL "filterFor" $p.ID }}
219    {{ template "rankList" dict "title" "top referrers" "col" "sessions" "items" .SessionsByReferrer "filterFor" "" }}
220    {{ template "rankList" dict "title" "top custom events" "col" "count" "items" .ByCustomEvent "filterFor" "" }}
221    {{ template "rankList" dict "title" "utm medium" "col" "views" "items" .PageViewsByUTMMedium "filterFor" "" }}
222    {{ template "rankList" dict "title" "utm source" "col" "views" "items" .PageViewsByUTMSource "filterFor" "" }}
223    {{ template "rankList" dict "title" "utm campaign" "col" "views" "items" .PageViewsByUTMCampaig "filterFor" "" }}
224  </div>
225</div>
226
227<div class="container my-4">
228  <div class="section-label mb-2">bot traffic · excluded from metrics above</div>
229  {{ if .Bots.Total }}
230  <div class="row g-3">
231    <div class="col-12 col-md-4">
232      <div class="metric-tile">
233        <div class="metric-label">Total bot events</div>
234        <div class="metric-value">{{ .Bots.Total }}</div>
235      </div>
236    </div>
237    {{ template "rankList" dict "title" "top bots" "col" "events" "items" .Bots.TopBots "filterFor" "" }}
238    {{ template "rankList" dict "title" "top pages · bot hits" "col" "events" "items" .Bots.TopPages "filterFor" "" }}
239  </div>
240  {{ else }}
241  <div class="text-muted small">No bot events in this range.</div>
242  {{ end }}
243</div>
244{{ end }}
245
246<div class="modal fade" id="siteTagModal" tabindex="-1" aria-hidden="true">
247  <div class="modal-dialog modal-lg">
248    <div class="modal-content">
249      <div class="modal-header">
250        <h5 class="modal-title text-white">Collector site tag</h5>
251        <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
252      </div>
253      <div class="modal-body">
254        <p class="text-muted small mb-3">Drop this into the <code class="text-amber">&lt;head&gt;</code> of your site. The collector ID below is scoped to <strong class="text-white">{{ $p.Name }}</strong>.</p>
255        <textarea class="codebox" rows="7" readonly>&lt;script&gt;
256(function(m,e,t,r,i,c,s){m.collectorQueue = m.collectorQueue || r;
257m.collectorServer = c; m.collectorId = s; var script = e.createElement(t);
258script.src = c + i; e.head.appendChild(script);
259})(window,document,'script',[],'/static/collector.js',
260'{{ $baseURL }}','{{ $p.ID }}');
261&lt;/script&gt;</textarea>
262      </div>
263    </div>
264  </div>
265</div>
266{{ end }}