Single-binary self-hosted uptime monitoring and status pages on Rust axum: HTTP probes, Lighthouse audits, SEO crawler, and PDF reports.
axumdockerrustself-hostedsqlitestatus-pageuptime-monitoringvite
1{% extends "base.html" %}
2
3{% block extra_css %}
4<link rel="stylesheet" href="{{ vite_asset('static_src/properties/index.js', 'css') }}">
5{% endblock %}
6
7{% block breadcrumbs %}
8<nav aria-label="breadcrumb">
9 <ol class="breadcrumb mb-0">
10 <li class="breadcrumb-item"><a href="/">Home</a></li>
11 <li class="breadcrumb-item active" aria-current="page">{{ title }}</li>
12 </ol>
13</nav>
14{% endblock %}
15
16{% block main %}
17<div class="container my-4">
18 <div class="row align-items-center g-3 mb-4">
19 <div class="col-md-5">
20 <div class="section-label mb-1">operator</div>
21 <h1 class="fw-bolder text-white mb-0" style="letter-spacing: -0.01em;">{{ title }}</h1>
22 <p class="text-muted mb-0 small mt-1">Every URL you've registered, current probe state, and audit signals.</p>
23 </div>
24 <div class="col-md-7">
25 <div class="dashboard-toolbar">
26 <form method="get" class="search-form flex-grow-1">
27 <input type="text" class="form-control search-input" name="q" id="id_search" placeholder="grep properties..."{% if q %} value="{{ q }}"{% endif %} />
28 </form>
29 <button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapsePropertyAdd" aria-expanded="false" aria-controls="collapsePropertyAdd">
30 + new
31 </button>
32 </div>
33 </div>
34 </div>
35
36 <div class="collapse mb-4" id="collapsePropertyAdd">
37 <div class="bg-surface border-subtle rounded border p-3">
38 <div class="section-label mb-2">register property</div>
39 <p class="text-muted small mb-3">Any public URL. Probe begins within 3 minutes.</p>
40 <form method="POST" action="{{ url_for('properties') }}" class="dashboard-toolbar">
41 <input type="url" name="url" id="id_url" class="form-control flex-grow-1" placeholder="https://example.com" required>
42 <button type="submit" class="btn btn-primary">Add →</button>
43 </form>
44 </div>
45 </div>
46
47 <div class="section-label mb-2">registered properties</div>
48 {% for property in properties %}
49 <div class="property-row {% if property.current_status != 200 %}is-down{% endif %}">
50 <div class="pr-main">
51 <div class="pr-title">
52 <span class="status-dot {% if property.current_status == 200 %}is-up{% else %}is-down{% endif %}" aria-hidden="true"></span>
53 <a href="/{{ property.id }}" class="text-truncate">{{ property.name }}</a>
54 </div>
55 <div class="pr-url">{{ property.url }}</div>
56 <div class="uptime-strip" aria-label="Recent checks">
57 {% for tick in property.recent_tick_stream %}
58 <span class="uptime-tick is-{{ tick }}"></span>
59 {% else %}
60 {% for i in range(30) %}<span class="uptime-tick is-none"></span>{% endfor %}
61 {% endfor %}
62 </div>
63 </div>
64 <div class="pr-side">
65 <div class="pr-meta">
66 <div class="pr-meta-cell">
67 <span class="pr-meta-k">status</span>
68 {% if property.current_status == 200 %}
69 <span class="chip chip-ok">ok · 200</span>
70 {% else %}
71 <span class="chip chip-down">down · {{ property.current_status }}</span>
72 {% endif %}
73 </div>
74 <div class="pr-meta-cell">
75 <span class="pr-meta-k">uptime</span>
76 {% if property.recent_uptime_pct is not none %}
77 {% if property.recent_uptime_pct >= 99 %}
78 <span class="chip chip-ok">{{ property.recent_uptime_pct }}%</span>
79 {% elif property.recent_uptime_pct < 95 %}
80 <span class="chip chip-down">{{ property.recent_uptime_pct }}%</span>
81 {% else %}
82 <span class="chip chip-warn">{{ property.recent_uptime_pct }}%</span>
83 {% endif %}
84 {% else %}
85 <span class="chip chip-muted">—</span>
86 {% endif %}
87 </div>
88 <div class="pr-meta-cell">
89 <span class="pr-meta-k">sec</span>
90 {% if property.has_security_issue %}
91 <span class="chip chip-warn">issues</span>
92 {% else %}
93 <span class="chip chip-ok">ok</span>
94 {% endif %}
95 </div>
96 <div class="pr-meta-cell">
97 <span class="pr-meta-k">seo</span>
98 {% set insights = property.crawler_insights | length %}
99 {% if insights > 100 %}
100 <span class="chip chip-down">{{ insights }}</span>
101 {% elif insights > 25 %}
102 <span class="chip chip-warn">{{ insights }}</span>
103 {% else %}
104 <span class="chip chip-ok">{{ insights }}</span>
105 {% endif %}
106 </div>
107 <div class="pr-meta-cell">
108 <span class="pr-meta-k">lh</span>
109 {% if property.avg_lighthouse_score %}
110 {% if property.avg_lighthouse_score >= 90 %}
111 <span class="chip chip-ok">{{ property.avg_lighthouse_score }}%</span>
112 {% elif property.avg_lighthouse_score >= 80 %}
113 <span class="chip chip-warn">{{ property.avg_lighthouse_score }}%</span>
114 {% else %}
115 <span class="chip chip-down">{{ property.avg_lighthouse_score }}%</span>
116 {% endif %}
117 {% else %}
118 <span class="chip chip-muted">—</span>
119 {% endif %}
120 </div>
121 </div>
122 <div class="pr-actions">
123 {% if not property.is_protected %}
124 <form method="POST" action="/properties/{{ property.id }}/delete" class="d-inline" onsubmit="return confirm('Delete {{ property.url }} and all its checks?');">
125 <button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
126 </form>
127 {% endif %}
128 <a href="/{{ property.id }}" class="btn btn-sm btn-outline-light">View →</a>
129 </div>
130 </div>
131 </div>
132 {% else %}
133 <div class="text-center py-5 text-muted">
134 <div class="section-label mb-3" style="justify-content:center;">no properties yet</div>
135 <p class="mb-0 small">Click <strong>+ new</strong> above to register your first URL.</p>
136 </div>
137 {% endfor %}
138</div>
139{% endblock %}