heartwood every commit a ring
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">&nbsp;</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">&nbsp;</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 %}