Single-binary self-hosted website analytics on Rust axum: collector API, dashboards, world map, and PDF reports.
analyticsaxumdockerrustself-hostedsqliteviteweb-analytics
1{% extends 'base.html' %}
2
3{% block breadcrumbs %}
4<nav aria-label="breadcrumb">
5 <ol class="breadcrumb mb-0">
6 <li class="breadcrumb-item"><a href="/">Home</a></li>
7 <li class="breadcrumb-item active" aria-current="page">{{ page.title }}</li>
8 </ol>
9</nav>
10{% endblock %}
11
12{% block main %}
13<div class="container my-4">
14 <div class="row align-items-center g-3 mb-4">
15 <div class="col-md-5">
16 <div class="section-label mb-1">operator</div>
17 <h1 class="fw-bolder text-white mb-0" style="letter-spacing: -0.01em;">{{ page.title }}</h1>
18 <p class="text-muted mb-0 small mt-1">Every site you're collecting events from, event totals, and seven-day activity state.</p>
19 </div>
20 <div class="col-md-7">
21 <div class="dashboard-toolbar">
22 <form method="get" class="search-form flex-grow-1">
23 <span class="search-icon">
24 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
25 <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
26 </svg>
27 </span>
28 <input type="text" class="form-control search-input" name="q" id="id_search" placeholder="grep properties..." {% if q %}value="{{ q }}"{% endif %} />
29 </form>
30 <button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapsePropertyAdd" aria-expanded="false" aria-controls="collapsePropertyAdd" onclick="window.collectorQueue && collectorQueue.push({event: 'add_property_toggle'})">
31 + new
32 </button>
33 </div>
34 </div>
35 </div>
36
37 <div class="collapse mb-4" id="collapsePropertyAdd">
38 <div class="bg-surface border-subtle rounded border p-3">
39 <div class="section-label mb-2">register property</div>
40 <p class="text-muted small mb-3">Give it any friendly name. You'll get a collector ID to embed right after you save.</p>
41 <form method="POST" action="/properties" class="dashboard-toolbar">
42 <input type="text" name="name" id="id_name" class="form-control flex-grow-1" placeholder="e.g. marketing-site" required>
43 <button type="submit" class="btn btn-primary">Add →</button>
44 </form>
45 </div>
46 </div>
47
48 <div class="row g-3 mb-4">
49 <div class="col-6 col-md-3">
50 <div class="metric-tile metric-accent-green">
51 <div class="metric-label">total properties</div>
52 <div class="metric-value">{{ totals.properties }}</div>
53 </div>
54 </div>
55 <div class="col-6 col-md-3">
56 <div class="metric-tile metric-accent-green">
57 <div class="metric-label">total events</div>
58 <div class="metric-value">{{ totals.events }}</div>
59 </div>
60 </div>
61 <div class="col-6 col-md-3">
62 <div class="metric-tile metric-accent-green">
63 <div class="metric-label">page views</div>
64 <div class="metric-value">{{ totals.page_views }}</div>
65 </div>
66 </div>
67 <div class="col-6 col-md-3">
68 <div class="metric-tile metric-accent-amber">
69 <div class="metric-label">sessions</div>
70 <div class="metric-value">{{ totals.sessions }}</div>
71 </div>
72 </div>
73 </div>
74
75 <div class="section-label mb-2">registered properties</div>
76 {% for property in properties %}
77 <div class="property-row {% if not property.is_active %}is-quiet{% endif %}">
78 <div class="pr-main">
79 <div class="pr-title">
80 <span class="status-dot {% if property.is_active %}is-up{% else %}is-idle{% endif %}" aria-hidden="true"></span>
81 <a href="/{{ property.id }}" class="text-truncate" onclick="window.collectorQueue && collectorQueue.push({event: 'view_property'})">{{ property.name }}</a>
82 </div>
83 <div class="pr-id"><span class="pr-id-k">id</span>{{ property.id }}</div>
84 </div>
85 <div class="pr-side">
86 <div class="pr-meta">
87 <div class="pr-meta-cell">
88 <span class="pr-meta-k">state</span>
89 <span class="pr-meta-v {% if property.is_active %}text-green{% else %}text-muted{% endif %}">{% if property.is_active %}live{% else %}quiet{% endif %}</span>
90 </div>
91 <div class="pr-meta-cell">
92 <span class="pr-meta-k">events</span>
93 <span class="pr-meta-v">{{ property.total_events }}</span>
94 </div>
95 <div class="pr-meta-cell">
96 <span class="pr-meta-k">views</span>
97 <span class="pr-meta-v">{{ property.total_page_views }}</span>
98 </div>
99 <div class="pr-meta-cell">
100 <span class="pr-meta-k">sessions</span>
101 <span class="pr-meta-v">{{ property.total_session_starts }}</span>
102 </div>
103 </div>
104 <div class="pr-actions">
105 {% if not property.is_protected %}
106 <button type="button" class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#delete-modal-{{ property.id }}">
107 Delete
108 </button>
109 <div class="modal fade" id="delete-modal-{{ property.id }}" tabindex="-1" aria-hidden="true">
110 <div class="modal-dialog">
111 <div class="modal-content">
112 <div class="modal-header">
113 <h5 class="modal-title text-white">Confirm delete</h5>
114 <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
115 </div>
116 <div class="modal-body">
117 Are you sure you want to delete <strong class="text-white">{{ property.name }}</strong>? All collected events will be removed.
118 </div>
119 <div class="modal-footer">
120 <button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">Cancel</button>
121 <form method="post" action="/properties/{{ property.id }}/delete" class="d-inline">
122 <button type="submit" class="btn btn-outline-danger">Delete</button>
123 </form>
124 </div>
125 </div>
126 </div>
127 </div>
128 {% endif %}
129 <a href="/{{ property.id }}" class="btn btn-sm btn-outline-light">View →</a>
130 </div>
131 </div>
132 </div>
133 {% else %}
134 <div class="text-center py-5 text-muted">
135 <div class="section-label mb-3" style="justify-content:center;">no properties yet</div>
136 <p class="mb-0 small">Click <strong>+ new</strong> above to register your first site.</p>
137 </div>
138 {% endfor %}
139</div>
140{% endblock %}