repos
/ status-rust master

status-rust

mirror archived upstream

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

15.5 KB · 330 lines · HTML Raw History
  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 extra_js %}
  8<script id="chart-status-response-times-data" type="application/json">{{ status_response_times_graph | tojson }}</script>
  9<script id="chart-status-codes-data" type="application/json">{{ status_codes_graph | tojson }}</script>
 10<script id="chart-uptime-data" type="application/json">{{ uptime_graph | tojson }}</script>
 11<script id="property-data" type="application/json">{{ property | tojson }}</script>
 12<script type="module" src="{{ vite_asset('static_src/properties/index.js') }}"></script>
 13{% endblock %}
 14
 15{% block breadcrumbs %}
 16<nav aria-label="breadcrumb">
 17  <ol class="breadcrumb mb-0">
 18    <li class="breadcrumb-item"><a href="/">Home</a></li>
 19    {% if user.is_authenticated %}<li class="breadcrumb-item"><a href="/properties">Properties</a></li>{% endif %}
 20    <li class="breadcrumb-item active" aria-current="page">{{ property.name }}</li>
 21  </ol>
 22</nav>
 23{% endblock %}
 24
 25{% block main %}
 26<div class="bg-deep border-bottom border-subtle py-4">
 27  <div class="container">
 28    <div class="row align-items-center g-3">
 29      <div class="col-12 col-lg-7">
 30        <div class="section-label mb-2">property · <span class="{% if property.current_status == 200 %}text-green{% else %}text-terracotta{% endif %}">{% if property.current_status == 200 %}online{% else %}offline{% endif %}</span></div>
 31        <h1 class="mb-2 text-white fw-bolder d-flex align-items-center gap-3" style="letter-spacing: -0.01em; min-width: 0;">
 32          <span class="text-truncate">{{ property.name }}</span>
 33          <span class="status-dot status-dot-lg {% if property.current_status == 200 %}is-up{% else %}is-down{% endif %}" aria-hidden="true"></span>
 34        </h1>
 35        <a href="{{ property.url }}" target="_blank" rel="noopener" class="property-url-link">
 36          <span class="property-url-text">{{ property.url }}</span>
 37          <span class="property-url-arrow" aria-hidden="true"></span>
 38        </a>
 39      </div>
 40      <div class="col-12 col-lg-5 d-flex flex-wrap gap-2 justify-content-lg-end align-items-center d-print-none">
 41        {% if user.is_authenticated %}
 42          <span id="is-public-form" class="d-inline-flex align-items-center gap-2" data-property-id="{{ property.id }}">
 43            <span class="toggle-label" title="Public properties are viewable by anyone with the URL; no login required.">Visibility</span>
 44            <label class="toggle-pill">
 45              <input type="checkbox" name="is_public" id="is-public-switch"{% if property.is_public %} checked{% endif %}>
 46              <span class="toggle-pill-seg toggle-pill-private">Private</span>
 47              <span class="toggle-pill-seg toggle-pill-public">Public</span>
 48            </label>
 49          </span>
 50          <a href="/{{ property.id }}?report" target="_blank" class="btn btn-sm btn-outline-light">Report · pdf</a>
 51          <a href="/{{ property.id }}?report=md" target="_blank" class="btn btn-sm btn-outline-light">Report · md</a>
 52          {% if not property.is_protected %}
 53          <form method="POST" action="/properties/{{ property.id }}/delete" class="d-inline" onsubmit="return confirm('Delete {{ property.url }}?');">
 54            <button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
 55          </form>
 56          {% endif %}
 57        {% endif %}
 58      </div>
 59    </div>
 60  </div>
 61</div>
 62
 63<div class="container my-4">
 64  <div class="section-label mb-2">live signals</div>
 65  <div class="row g-3 mb-4">
 66    <div class="col-6 col-md-3">
 67      <div class="metric-tile {% if property.current_status == 200 %}metric-accent-green{% else %}metric-accent-danger{% endif %}">
 68        <div class="metric-label">http status</div>
 69        <div class="metric-value">{{ property.current_status }}</div>
 70        <div class="metric-sub">{% if property.current_status == 200 %}OK · all clear{% else %}failing{% endif %}</div>
 71      </div>
 72    </div>
 73    <div class="col-6 col-md-3">
 74      <div class="metric-tile {% if property.invalid_cert %}metric-accent-danger{% else %}metric-accent-green{% endif %}">
 75        <div class="metric-label">tls cert</div>
 76        <div class="metric-value" style="font-size: 1.4rem;">{% if property.invalid_cert %}invalid{% else %}valid{% endif %}</div>
 77        <div class="metric-sub">chain + trust</div>
 78      </div>
 79    </div>
 80    <div class="col-6 col-md-3">
 81      <div class="metric-tile {% if property.has_security_issue %}metric-accent-amber{% else %}metric-accent-green{% endif %}">
 82        <div class="metric-label">security</div>
 83        <div class="metric-value" style="font-size: 1.4rem;">{% if property.has_security_issue %}issues{% else %}pass{% endif %}</div>
 84        <div class="metric-sub">header scan</div>
 85      </div>
 86    </div>
 87    <div class="col-6 col-md-3">
 88      <div class="metric-tile {% if property.avg_response_time > 500 %}metric-accent-danger{% else %}metric-accent-green{% endif %}">
 89        <div class="metric-label">response</div>
 90        <div class="metric-value">{{ property.avg_response_time }}<span class="text-muted" style="font-size: 0.9rem;"> ms</span></div>
 91        <div class="metric-sub">rolling avg</div>
 92      </div>
 93    </div>
 94  </div>
 95
 96  {% if property.lighthouse_scores %}
 97  <div class="section-label mb-2">lighthouse</div>
 98  <div class="row g-3 mb-4">
 99    {% for category, score in property.lighthouse_scores | items %}
100    <div class="col-6 col-md-3">
101      <div class="metric-tile {% if score >= 90 %}metric-accent-green{% elif score >= 50 %}metric-accent-amber{% else %}metric-accent-danger{% endif %}">
102        <div class="metric-label">{{ category }}</div>
103        <div class="metric-value">{{ score }}<span class="text-muted" style="font-size: 0.9rem;">%</span></div>
104      </div>
105    </div>
106    {% endfor %}
107  </div>
108  {% endif %}
109</div>
110
111<div class="container my-4">
112  <div class="row g-3">
113    <div class="col-12 col-md-8">
114      <div class="chart-panel mb-3">
115        <div class="chart-panel-header">
116          <span class="chart-panel-title">response time · last 31 checks</span>
117        </div>
118        <div style="position: relative; height: 300px;">
119          <canvas id="chart-response-times"></canvas>
120        </div>
121      </div>
122      <div class="row g-3">
123        <div class="col-12 col-md-6">
124          <div class="chart-panel">
125            <div class="chart-panel-header">
126              <span class="chart-panel-title">status codes</span>
127            </div>
128            <canvas id="chart-status-codes"></canvas>
129          </div>
130        </div>
131        <div class="col-12 col-md-6">
132          <div class="chart-panel">
133            <div class="chart-panel-header">
134              <span class="chart-panel-title">uptime split</span>
135            </div>
136            <canvas id="chart-uptime"></canvas>
137          </div>
138        </div>
139      </div>
140    </div>
141    <div class="col-12 col-md-4">
142      <div class="check-list">
143        <div class="check-list-header">security · header scan</div>
144        <div class="check-list-item">
145          <span class="check-name">Use HTTPS</span>
146          <span class="chip {% if property.is_https %}chip-ok{% else %}chip-down{% endif %}">{% if property.is_https %}pass{% else %}fail{% endif %}</span>
147        </div>
148        <div class="check-list-item">
149          <span class="check-name">Set MIME types</span>
150          <span class="chip {% if property.has_mime_type %}chip-ok{% else %}chip-down{% endif %}">{% if property.has_mime_type %}pass{% else %}fail{% endif %}</span>
151        </div>
152        <div class="check-list-item">
153          <span class="check-name">Content sniff protection</span>
154          <span class="chip {% if property.has_content_sniffing_protection %}chip-ok{% else %}chip-down{% endif %}">{% if property.has_content_sniffing_protection %}pass{% else %}fail{% endif %}</span>
155        </div>
156        <div class="check-list-item">
157          <span class="check-name">Clickjack protection</span>
158          <span class="chip {% if property.has_clickjack_protection %}chip-ok{% else %}chip-down{% endif %}">{% if property.has_clickjack_protection %}pass{% else %}fail{% endif %}</span>
159        </div>
160        <div class="check-list-item">
161          <span class="check-name">Hide server version</span>
162          <span class="chip {% if property.hides_server_version %}chip-ok{% else %}chip-down{% endif %}">{% if property.hides_server_version %}pass{% else %}fail{% endif %}</span>
163        </div>
164        <div class="check-list-item">
165          <span class="check-name">HSTS enabled</span>
166          <span class="chip {% if property.has_hsts %}chip-ok{% else %}chip-down{% endif %}">{% if property.has_hsts %}pass{% else %}fail{% endif %}</span>
167        </div>
168        <div class="check-list-item">
169          <span class="check-name">HSTS preload</span>
170          <span class="chip {% if property.has_hsts_preload %}chip-ok{% else %}chip-down{% endif %}">{% if property.has_hsts_preload %}pass{% else %}fail{% endif %}</span>
171        </div>
172      </div>
173    </div>
174  </div>
175</div>
176
177{% if user.is_authenticated %}
178<div class="container my-4 d-print-none" id="monitoring-status" data-property-id="{{ property.id }}">
179  <div class="section-label mb-2">scheduled audits</div>
180  <div class="row g-3">
181    <div class="col-12 col-md-6">
182      <div class="monitor-card h-100">
183        <div class="monitor-header">
184          <span class="monitor-title">Crawler</span>
185          <span class="d-flex align-items-center gap-2">
186            <span class="chip chip-muted" data-field="crawler.state_badge">&nbsp;</span>
187            <button type="button" id="recrawl-btn" class="btn btn-sm btn-outline-light monitor-btn" title="Recrawl now">
188              <span class="recrawl-btn-label">Recrawl</span>
189              <span class="recrawl-btn-spinner spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
190            </button>
191          </span>
192        </div>
193        <div class="monitor-body">
194          <div class="progress mb-3 d-none" data-field="crawler.progress_wrap" style="height: 4px;">
195            <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" data-field="crawler.progress_bar" style="width: 0%"></div>
196          </div>
197          <div class="alert alert-danger py-2 mb-3 d-none small" data-field="crawler.error_box" role="alert">
198            <strong>Last crawl failed:</strong>
199            <code data-field="crawler.error_text"></code>
200          </div>
201          <dl>
202            <dt>Last success</dt>
203            <dd data-field="crawler.last_success"></dd>
204            <dt>Last attempt</dt>
205            <dd data-field="crawler.last_attempt"></dd>
206            <dt>Pages crawled</dt>
207            <dd data-field="crawler.pages"></dd>
208            <dt>Duration</dt>
209            <dd data-field="crawler.duration"></dd>
210            <dt>Issues found</dt>
211            <dd data-field="crawler.insights"></dd>
212            <dt>Next run</dt>
213            <dd data-field="crawler.next_run"></dd>
214          </dl>
215        </div>
216      </div>
217    </div>
218    <div class="col-12 col-md-6">
219      <div class="monitor-card h-100">
220        <div class="monitor-header">
221          <span class="monitor-title">Lighthouse</span>
222          <span class="d-flex align-items-center gap-2">
223            <span class="chip chip-muted" data-field="lighthouse.state_badge">&nbsp;</span>
224            <button type="button" id="rerun-lighthouse-btn" class="btn btn-sm btn-outline-light monitor-btn" title="Rerun Lighthouse now">
225              <span class="rerun-lh-label">Rerun</span>
226              <span class="rerun-lh-spinner spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
227            </button>
228          </span>
229        </div>
230        <div class="monitor-body">
231          <div class="alert alert-warning py-2 mb-3 d-none small" data-field="lighthouse.error_box" role="alert">
232            <strong>Last Lighthouse run failed:</strong>
233            <code data-field="lighthouse.error_text"></code>
234          </div>
235          <dl>
236            <dt>Last success</dt>
237            <dd data-field="lighthouse.last_success"></dd>
238            <dt>Last attempt</dt>
239            <dd data-field="lighthouse.last_attempt"></dd>
240            <dt>Duration</dt>
241            <dd data-field="lighthouse.duration"></dd>
242            <dt>Next run</dt>
243            <dd data-field="lighthouse.next_run"></dd>
244          </dl>
245        </div>
246      </div>
247    </div>
248  </div>
249</div>
250{% endif %}
251
252{% if property.lighthouse_details %}
253<div class="container my-4">
254  <div class="section-label mb-2">performance breakdown</div>
255  <p class="text-muted small mb-3">
256    Lighthouse combines these weighted metrics to produce the Performance score. Opportunities below are the biggest wins; savings are estimated against the audited URL only.
257  </p>
258
259  {% if property.lighthouse_details.metrics %}
260  <div class="row g-3 mb-4">
261    {% for metric in property.lighthouse_details.metrics %}
262    <div class="col-6 col-md d-flex">
263      <div class="metric-tile w-100 {% if metric.score >= 0.9 %}metric-accent-green{% elif metric.score >= 0.5 %}metric-accent-amber{% else %}metric-accent-danger{% endif %}">
264        <div class="metric-label">{{ metric.acronym }} · {{ metric.weight }}%</div>
265        <div class="metric-value" style="font-size: 1.2rem;">{{ metric.display_value or "—" }}</div>
266        <div class="metric-sub" title="{{ metric.title }}">{{ metric.title }}</div>
267      </div>
268    </div>
269    {% endfor %}
270  </div>
271  {% endif %}
272
273  {% if property.lighthouse_details.opportunities %}
274  <div class="section-label mb-2">top opportunities</div>
275  <div class="insight-header insight--3col">
276    <span>Audit</span>
277    <span>Detail</span>
278    <span class="text-end">Est. savings</span>
279  </div>
280  {% for opp in property.lighthouse_details.opportunities %}
281  <div class="insight-row insight--3col">
282    <span class="insight-col-trunc" title="{{ opp.title }}">{{ opp.title }}</span>
283    <span class="insight-col-trunc text-muted small" title="{{ opp.display_value or '' }}">
284      {{ opp.display_value or "—" }}
285    </span>
286    <span class="text-end small">
287      {% set saved = opp.savings_ms | format_ms_savings %}
288      {% if saved %}<strong class="text-amber">{{ saved }}</strong>{% else %}<span class="text-muted"></span>{% endif %}
289    </span>
290  </div>
291  {% endfor %}
292  {% else %}
293  <div class="alert alert-info py-2 small">No actionable opportunities; everything passing at this URL.</div>
294  {% endif %}
295</div>
296{% endif %}
297
298{% if insights_groups and insights_groups | length > 0 %}
299<div class="container my-4">
300  {% for group in insights_groups %}
301  <div class="section-label mb-2">{{ group.type }} · <span class="text-muted">{{ group.items | length }}</span></div>
302  <div class="insight-header">
303    <span>Severity</span>
304    <span>URL</span>
305    <span>Issue</span>
306    <span>Item</span>
307  </div>
308  {% for insight in group.items %}
309  <div class="insight-row">
310    <span>
311      <span class="chip {% if insight.severity == 'error' %}chip-down{% elif insight.severity == 'warning' %}chip-warn{% else %}chip-info{% endif %}">
312        {{ insight.severity | upper }}
313      </span>
314    </span>
315    <span class="insight-col-trunc">
316      <a href="{{ insight.url }}" target="_blank" rel="noopener" class="text-muted">{{ insight.url | url_path }}</a>
317    </span>
318    <span class="insight-col-trunc" title="{{ insight.issue }}">{{ insight.issue }}</span>
319    <span class="insight-col-trunc text-muted small" title="{{ insight.item or '' }}">{{ insight.item or "" }}</span>
320  </div>
321  {% endfor %}
322  {% endfor %}
323</div>
324{% elif property.last_crawl_error %}
325<div class="container my-4">
326  <div class="alert alert-danger py-2 small mb-0">Last crawl failed: <code>{{ property.last_crawl_error }}</code></div>
327</div>
328{% endif %}
329{% endblock %}