Files
MeshTender/internal/core/templates/analytics.html
T
2026-07-30 08:46:19 -04:00

139 lines
5.5 KiB
HTML

{{define "title"}}Traffic analytics · MeshTender{{end}}
{{define "header"}}
<div class="row g-2 align-items-center">
<div class="col">
<div class="page-pretitle">Administration</div>
<h1 class="page-title fs-1">Traffic Analytics</h1>
</div>
<div class="col-auto ms-auto d-print-none">
<div class="btn-group" role="group" aria-label="Time range">
<a class="btn{{if eq .Days 7}} btn-primary{{end}}" href="/admin/analytics?days=7">7d</a>
<a class="btn{{if eq .Days 30}} btn-primary{{end}}" href="/admin/analytics?days=30">30d</a>
<a class="btn{{if eq .Days 90}} btn-primary{{end}}" href="/admin/analytics?days=90">90d</a>
</div>
</div>
</div>
{{end}}
{{define "content"}}
{{if not .HasData}}
<div class="card"><div class="card-body text-secondary">
No traffic recorded yet. Stats roll up every few minutes, so check back shortly after the first visits.
</div></div>
{{else}}
<!-- Summary -->
<div class="row row-cards">
<div class="col-sm-4">
<div class="card card-sm"><div class="card-body">
<div class="h1 mb-0">{{.TotalReq}}</div>
<div class="text-secondary">Requests · last {{.Days}}d</div>
</div></div>
</div>
<div class="col-sm-4">
<div class="card card-sm"><div class="card-body">
<div class="h1 mb-0">{{.TodayReq}}</div>
<div class="text-secondary">Requests today</div>
</div></div>
</div>
<div class="col-sm-4">
<div class="card card-sm"><div class="card-body">
<div class="h1 mb-0">{{.TodayVis}}</div>
<div class="text-secondary">Unique visitors today</div>
</div></div>
</div>
</div>
<!-- Charts -->
<div class="row row-cards mt-1">
<div class="col-lg-6">
<div class="card">
<div class="card-header"><h2 class="card-title">Requests per day</h2></div>
<div class="card-body">
<div class="d-flex align-items-end gap-1" style="height:140px">
{{range .Bars}}<div class="flex-fill rounded-top" style="height:{{.ReqH}}%;min-height:1px;background:var(--tblr-primary)" title="{{.Label}}: {{.Requests}} requests"></div>{{end}}
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card">
<div class="card-header"><h2 class="card-title">Unique visitors per day</h2></div>
<div class="card-body">
<div class="d-flex align-items-end gap-1" style="height:140px">
{{range .Bars}}<div class="flex-fill rounded-top" style="height:{{.VisH}}%;min-height:1px;background:var(--tblr-green)" title="{{.Label}}: {{.Visitors}} visitors"></div>{{end}}
</div>
</div>
</div>
</div>
</div>
<!-- Tables -->
<div class="row row-cards mt-1">
<div class="col-lg-5">
<div class="card">
<div class="card-header"><h2 class="card-title">Traffic by surface</h2></div>
<div class="card-body">
{{if .Surfaces}}{{range .Surfaces}}{{template "value-bar" .}}{{end}}
{{else}}<div class="text-secondary">No data.</div>{{end}}
</div>
</div>
</div>
<div class="col-lg-7">
<div class="card">
<div class="card-header"><h2 class="card-title">Busiest pages</h2></div>
<div class="card-body">
{{if .Paths}}{{range .Paths}}{{template "value-bar" .}}{{end}}
{{else}}<div class="text-secondary">No data.</div>{{end}}
</div>
</div>
</div>
</div>
<!-- Hosts + visitors -->
<div class="row row-cards mt-1">
<div class="col-lg-5">
<div class="card">
<div class="card-header"><h2 class="card-title">Traffic by host</h2></div>
<div class="card-body">
<p class="text-secondary small">“custom” is any request whose host isn't your configured app/auth/root host — e.g. the bare IP, a health probe, or a crawler. These are the actual hosts seen.</p>
{{if .Hosts}}{{range .Hosts}}{{template "value-bar" .}}{{end}}
{{else}}<div class="text-secondary">No data.</div>{{end}}
</div>
</div>
</div>
<div class="col-lg-7">
<div class="card">
<div class="card-header"><h2 class="card-title">Top visitors</h2></div>
<div class="card-body">
<p class="text-secondary small">Anonymous, daily-rotating IDs (a salted hash of IP + browser). The same person gets a fresh ID each day, so a heavy daily visitor shows once per day.</p>
{{if .Visitors}}
{{range .Visitors}}
<div class="mb-2">
<div class="d-flex justify-content-between small">
<span class="font-monospace text-truncate" style="max-width:60%" title="{{.Visitor}}">{{.Visitor}}</span>
<span class="text-secondary">{{.Requests}}</span>
</div>
<div class="progress" style="height:6px"><div class="progress-bar" style="width:{{.W}}%"></div></div>
<div class="text-secondary small text-truncate"><span class="badge bg-secondary-lt">{{.Surface}}</span> {{.LastPath}} · {{ts .LastSeen "datetime"}}</div>
</div>
{{end}}
{{else}}<div class="text-secondary">No data.</div>{{end}}
</div>
</div>
</div>
</div>
{{end}}
<a class="back-link mt-3" href="/admin">{{template "icon-arrow-left" "me-1"}}Back to admin</a>
{{end}}
{{/* value-bar renders one labeled horizontal bar (label, bar, value). */}}
{{define "value-bar"}}
<div class="mb-2">
<div class="d-flex justify-content-between small">
<span class="text-truncate" style="max-width:75%" title="{{.Label}}">{{.Label}}</span>
<span class="text-secondary">{{.Value}}</span>
</div>
<div class="progress" style="height:6px"><div class="progress-bar" style="width:{{.W}}%"></div></div>
</div>
{{end}}