15.5 KB
raw
{% extends "base.html" %}
{% block extra_css %}
<link rel="stylesheet" href="{{ vite_asset('static_src/properties/index.js', 'css') }}">
{% endblock %}
{% block extra_js %}
<script id="chart-status-response-times-data" type="application/json">{{ status_response_times_graph | tojson }}</script>
<script id="chart-status-codes-data" type="application/json">{{ status_codes_graph | tojson }}</script>
<script id="chart-uptime-data" type="application/json">{{ uptime_graph | tojson }}</script>
<script id="property-data" type="application/json">{{ property | tojson }}</script>
<script type="module" src="{{ vite_asset('static_src/properties/index.js') }}"></script>
{% endblock %}
{% block breadcrumbs %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="/">Home</a></li>
{% if user.is_authenticated %}<li class="breadcrumb-item"><a href="/properties">Properties</a></li>{% endif %}
<li class="breadcrumb-item active" aria-current="page">{{ property.name }}</li>
</ol>
</nav>
{% endblock %}
{% block main %}
<div class="bg-deep border-bottom border-subtle py-4">
<div class="container">
<div class="row align-items-center g-3">
<div class="col-12 col-lg-7">
<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>
<h1 class="mb-2 text-white fw-bolder d-flex align-items-center gap-3" style="letter-spacing: -0.01em; min-width: 0;">
<span class="text-truncate">{{ property.name }}</span>
<span class="status-dot status-dot-lg {% if property.current_status == 200 %}is-up{% else %}is-down{% endif %}" aria-hidden="true"></span>
</h1>
<a href="{{ property.url }}" target="_blank" rel="noopener" class="property-url-link">
<span class="property-url-text">{{ property.url }}</span>
<span class="property-url-arrow" aria-hidden="true">↗</span>
</a>
</div>
<div class="col-12 col-lg-5 d-flex flex-wrap gap-2 justify-content-lg-end align-items-center d-print-none">
{% if user.is_authenticated %}
<span id="is-public-form" class="d-inline-flex align-items-center gap-2" data-property-id="{{ property.id }}">
<span class="toggle-label" title="Public properties are viewable by anyone with the URL; no login required.">Visibility</span>
<label class="toggle-pill">
<input type="checkbox" name="is_public" id="is-public-switch"{% if property.is_public %} checked{% endif %}>
<span class="toggle-pill-seg toggle-pill-private">Private</span>
<span class="toggle-pill-seg toggle-pill-public">Public</span>
</label>
</span>
<a href="/{{ property.id }}?report" target="_blank" class="btn btn-sm btn-outline-light">Report · pdf</a>
<a href="/{{ property.id }}?report=md" target="_blank" class="btn btn-sm btn-outline-light">Report · md</a>
{% if not property.is_protected %}
<form method="POST" action="/properties/{{ property.id }}/delete" class="d-inline" onsubmit="return confirm('Delete {{ property.url }}?');">
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
</form>
{% endif %}
{% endif %}
</div>
</div>
</div>
</div>
<div class="container my-4">
<div class="section-label mb-2">live signals</div>
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="metric-tile {% if property.current_status == 200 %}metric-accent-green{% else %}metric-accent-danger{% endif %}">
<div class="metric-label">http status</div>
<div class="metric-value">{{ property.current_status }}</div>
<div class="metric-sub">{% if property.current_status == 200 %}OK · all clear{% else %}failing{% endif %}</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="metric-tile {% if property.invalid_cert %}metric-accent-danger{% else %}metric-accent-green{% endif %}">
<div class="metric-label">tls cert</div>
<div class="metric-value" style="font-size: 1.4rem;">{% if property.invalid_cert %}invalid{% else %}valid{% endif %}</div>
<div class="metric-sub">chain + trust</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="metric-tile {% if property.has_security_issue %}metric-accent-amber{% else %}metric-accent-green{% endif %}">
<div class="metric-label">security</div>
<div class="metric-value" style="font-size: 1.4rem;">{% if property.has_security_issue %}issues{% else %}pass{% endif %}</div>
<div class="metric-sub">header scan</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="metric-tile {% if property.avg_response_time > 500 %}metric-accent-danger{% else %}metric-accent-green{% endif %}">
<div class="metric-label">response</div>
<div class="metric-value">{{ property.avg_response_time }}<span class="text-muted" style="font-size: 0.9rem;"> ms</span></div>
<div class="metric-sub">rolling avg</div>
</div>
</div>
</div>
{% if property.lighthouse_scores %}
<div class="section-label mb-2">lighthouse</div>
<div class="row g-3 mb-4">
{% for category, score in property.lighthouse_scores | items %}
<div class="col-6 col-md-3">
<div class="metric-tile {% if score >= 90 %}metric-accent-green{% elif score >= 50 %}metric-accent-amber{% else %}metric-accent-danger{% endif %}">
<div class="metric-label">{{ category }}</div>
<div class="metric-value">{{ score }}<span class="text-muted" style="font-size: 0.9rem;">%</span></div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
<div class="container my-4">
<div class="row g-3">
<div class="col-12 col-md-8">
<div class="chart-panel mb-3">
<div class="chart-panel-header">
<span class="chart-panel-title">response time · last 31 checks</span>
</div>
<div style="position: relative; height: 300px;">
<canvas id="chart-response-times"></canvas>
</div>
</div>
<div class="row g-3">
<div class="col-12 col-md-6">
<div class="chart-panel">
<div class="chart-panel-header">
<span class="chart-panel-title">status codes</span>
</div>
<canvas id="chart-status-codes"></canvas>
</div>
</div>
<div class="col-12 col-md-6">
<div class="chart-panel">
<div class="chart-panel-header">
<span class="chart-panel-title">uptime split</span>
</div>
<canvas id="chart-uptime"></canvas>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="check-list">
<div class="check-list-header">security · header scan</div>
<div class="check-list-item">
<span class="check-name">Use HTTPS</span>
<span class="chip {% if property.is_https %}chip-ok{% else %}chip-down{% endif %}">{% if property.is_https %}pass{% else %}fail{% endif %}</span>
</div>
<div class="check-list-item">
<span class="check-name">Set MIME types</span>
<span class="chip {% if property.has_mime_type %}chip-ok{% else %}chip-down{% endif %}">{% if property.has_mime_type %}pass{% else %}fail{% endif %}</span>
</div>
<div class="check-list-item">
<span class="check-name">Content sniff protection</span>
<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>
</div>
<div class="check-list-item">
<span class="check-name">Clickjack protection</span>
<span class="chip {% if property.has_clickjack_protection %}chip-ok{% else %}chip-down{% endif %}">{% if property.has_clickjack_protection %}pass{% else %}fail{% endif %}</span>
</div>
<div class="check-list-item">
<span class="check-name">Hide server version</span>
<span class="chip {% if property.hides_server_version %}chip-ok{% else %}chip-down{% endif %}">{% if property.hides_server_version %}pass{% else %}fail{% endif %}</span>
</div>
<div class="check-list-item">
<span class="check-name">HSTS enabled</span>
<span class="chip {% if property.has_hsts %}chip-ok{% else %}chip-down{% endif %}">{% if property.has_hsts %}pass{% else %}fail{% endif %}</span>
</div>
<div class="check-list-item">
<span class="check-name">HSTS preload</span>
<span class="chip {% if property.has_hsts_preload %}chip-ok{% else %}chip-down{% endif %}">{% if property.has_hsts_preload %}pass{% else %}fail{% endif %}</span>
</div>
</div>
</div>
</div>
</div>
{% if user.is_authenticated %}
<div class="container my-4 d-print-none" id="monitoring-status" data-property-id="{{ property.id }}">
<div class="section-label mb-2">scheduled audits</div>
<div class="row g-3">
<div class="col-12 col-md-6">
<div class="monitor-card h-100">
<div class="monitor-header">
<span class="monitor-title">Crawler</span>
<span class="d-flex align-items-center gap-2">
<span class="chip chip-muted" data-field="crawler.state_badge"> </span>
<button type="button" id="recrawl-btn" class="btn btn-sm btn-outline-light monitor-btn" title="Recrawl now">
<span class="recrawl-btn-label">Recrawl</span>
<span class="recrawl-btn-spinner spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
</button>
</span>
</div>
<div class="monitor-body">
<div class="progress mb-3 d-none" data-field="crawler.progress_wrap" style="height: 4px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" data-field="crawler.progress_bar" style="width: 0%"></div>
</div>
<div class="alert alert-danger py-2 mb-3 d-none small" data-field="crawler.error_box" role="alert">
<strong>Last crawl failed:</strong>
<code data-field="crawler.error_text"></code>
</div>
<dl>
<dt>Last success</dt>
<dd data-field="crawler.last_success">—</dd>
<dt>Last attempt</dt>
<dd data-field="crawler.last_attempt">—</dd>
<dt>Pages crawled</dt>
<dd data-field="crawler.pages">—</dd>
<dt>Duration</dt>
<dd data-field="crawler.duration">—</dd>
<dt>Issues found</dt>
<dd data-field="crawler.insights">—</dd>
<dt>Next run</dt>
<dd data-field="crawler.next_run">—</dd>
</dl>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="monitor-card h-100">
<div class="monitor-header">
<span class="monitor-title">Lighthouse</span>
<span class="d-flex align-items-center gap-2">
<span class="chip chip-muted" data-field="lighthouse.state_badge"> </span>
<button type="button" id="rerun-lighthouse-btn" class="btn btn-sm btn-outline-light monitor-btn" title="Rerun Lighthouse now">
<span class="rerun-lh-label">Rerun</span>
<span class="rerun-lh-spinner spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
</button>
</span>
</div>
<div class="monitor-body">
<div class="alert alert-warning py-2 mb-3 d-none small" data-field="lighthouse.error_box" role="alert">
<strong>Last Lighthouse run failed:</strong>
<code data-field="lighthouse.error_text"></code>
</div>
<dl>
<dt>Last success</dt>
<dd data-field="lighthouse.last_success">—</dd>
<dt>Last attempt</dt>
<dd data-field="lighthouse.last_attempt">—</dd>
<dt>Duration</dt>
<dd data-field="lighthouse.duration">—</dd>
<dt>Next run</dt>
<dd data-field="lighthouse.next_run">—</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% if property.lighthouse_details %}
<div class="container my-4">
<div class="section-label mb-2">performance breakdown</div>
<p class="text-muted small mb-3">
Lighthouse combines these weighted metrics to produce the Performance score. Opportunities below are the biggest wins; savings are estimated against the audited URL only.
</p>
{% if property.lighthouse_details.metrics %}
<div class="row g-3 mb-4">
{% for metric in property.lighthouse_details.metrics %}
<div class="col-6 col-md d-flex">
<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 %}">
<div class="metric-label">{{ metric.acronym }} · {{ metric.weight }}%</div>
<div class="metric-value" style="font-size: 1.2rem;">{{ metric.display_value or "—" }}</div>
<div class="metric-sub" title="{{ metric.title }}">{{ metric.title }}</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% if property.lighthouse_details.opportunities %}
<div class="section-label mb-2">top opportunities</div>
<div class="insight-header insight--3col">
<span>Audit</span>
<span>Detail</span>
<span class="text-end">Est. savings</span>
</div>
{% for opp in property.lighthouse_details.opportunities %}
<div class="insight-row insight--3col">
<span class="insight-col-trunc" title="{{ opp.title }}">{{ opp.title }}</span>
<span class="insight-col-trunc text-muted small" title="{{ opp.display_value or '' }}">
{{ opp.display_value or "—" }}
</span>
<span class="text-end small">
{% set saved = opp.savings_ms | format_ms_savings %}
{% if saved %}<strong class="text-amber">{{ saved }}</strong>{% else %}<span class="text-muted">—</span>{% endif %}
</span>
</div>
{% endfor %}
{% else %}
<div class="alert alert-info py-2 small">No actionable opportunities; everything passing at this URL.</div>
{% endif %}
</div>
{% endif %}
{% if insights_groups and insights_groups | length > 0 %}
<div class="container my-4">
{% for group in insights_groups %}
<div class="section-label mb-2">{{ group.type }} · <span class="text-muted">{{ group.items | length }}</span></div>
<div class="insight-header">
<span>Severity</span>
<span>URL</span>
<span>Issue</span>
<span>Item</span>
</div>
{% for insight in group.items %}
<div class="insight-row">
<span>
<span class="chip {% if insight.severity == 'error' %}chip-down{% elif insight.severity == 'warning' %}chip-warn{% else %}chip-info{% endif %}">
{{ insight.severity | upper }}
</span>
</span>
<span class="insight-col-trunc">
<a href="{{ insight.url }}" target="_blank" rel="noopener" class="text-muted">{{ insight.url | url_path }}</a>
</span>
<span class="insight-col-trunc" title="{{ insight.issue }}">{{ insight.issue }}</span>
<span class="insight-col-trunc text-muted small" title="{{ insight.item or '' }}">{{ insight.item or "" }}</span>
</div>
{% endfor %}
{% endfor %}
</div>
{% elif property.last_crawl_error %}
<div class="container my-4">
<div class="alert alert-danger py-2 small mb-0">Last crawl failed: <code>{{ property.last_crawl_error }}</code></div>
</div>
{% endif %}
{% endblock %}