mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-09-02 01:38:17 +00:00
209 lines
8.1 KiB
HTML
209 lines
8.1 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>
|
|
|
|
<div class="alert alert-info mt-3" role="alert">
|
|
<div>
|
|
Everything above counts <strong>real visits only</strong>. Vulnerability scanners, crawlers, and
|
|
404s are recorded too, but kept out of these figures — they're broken out below.
|
|
</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>
|
|
|
|
<!-- Traffic held apart from the figures above -->
|
|
<div class="row row-cards mt-1">
|
|
<div class="col-lg-6">
|
|
<div class="card" data-testid="probe-card">
|
|
<div class="card-header">
|
|
<div>
|
|
<h2 class="card-title">Probes & scanners</h2>
|
|
<div class="card-subtitle">Requests for things we don't serve — other stacks' admin panels,
|
|
credential files, known exploit paths.</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row mb-3">
|
|
<div class="col-6">
|
|
<div class="h1 mb-0" data-testid="probe-requests">{{.ProbeReq}}</div>
|
|
<div class="text-secondary">Requests · last {{.Days}}d</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="h1 mb-0">{{.ProbeSources}}</div>
|
|
<div class="text-secondary">Distinct sources</div>
|
|
</div>
|
|
</div>
|
|
{{if .ProbePaths}}{{range .ProbePaths}}{{template "value-bar" .}}{{end}}
|
|
{{else}}<div class="text-secondary">Nothing probed in this window.</div>{{end}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-6">
|
|
<div class="card" data-testid="bot-card">
|
|
<div class="card-header">
|
|
<div>
|
|
<h2 class="card-title">Bots & crawlers</h2>
|
|
<div class="card-subtitle">Clients that identify themselves as automated: search engines,
|
|
uptime monitors, link previewers.</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row mb-3">
|
|
<div class="col-6">
|
|
<div class="h1 mb-0" data-testid="bot-requests">{{.BotReq}}</div>
|
|
<div class="text-secondary">Requests · last {{.Days}}d</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="h1 mb-0">{{.BotSources}}</div>
|
|
<div class="text-secondary">Distinct sources</div>
|
|
</div>
|
|
</div>
|
|
{{if .BotPaths}}{{range .BotPaths}}{{template "value-bar" .}}{{end}}
|
|
{{else}}<div class="text-secondary">No crawler traffic in this window. Bots are only recorded
|
|
from the point this was deployed.</div>{{end}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card card-sm mt-1">
|
|
<div class="card-body">
|
|
<div class="h1 mb-0" data-testid="notfound-requests">{{.NotFoundReq}}</div>
|
|
<div class="text-secondary">404s with no attack signature · last {{.Days}}d — broken links worth fixing,
|
|
counted nowhere above.</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}}
|