modified
accounts/templates/accounts/profile.html
@@ -2,7 +2,7 @@{% block breadcrumbs %}<nav style="--bs-breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='%236c757d'/%3E%3C/svg%3E");" aria-label="breadcrumb"><nav aria-label="breadcrumb"> <ol class="breadcrumb mb-0"> <li class="breadcrumb-item"><a href="/">Home</a></li> <li class="breadcrumb-item active" aria-current="page">{{ title }}</li>
@@ -14,36 +14,28 @@{% block main %}<div class="container my-5"> <div class="row"> <div class="col-lg-6 offset-lg-3 col-sm-8 offset-sm-2 col-xs-12 offset-xs-0"> <h1>{{ title }}</h1> </div> </div> <div class="col-lg-6 offset-lg-3 col-md-8 offset-md-2"> <div class="section-label mb-2">operator ยท {{ request.user.username }}</div> <h1 class="fw-bolder text-white" style="letter-spacing: -0.01em;">{{ title }}</h1> <p class="text-muted small mb-4">Account identifiers and alert channels. Changes save on submit.</p> <div class="row"> <div class="col-lg-6 offset-lg-3 col-sm-8 offset-sm-2 col-xs-12 offset-xs-0"> <form method="POST"> {% csrf_token %} <div class="form-floating mb-3"> <input type="text" class="form-control {% if form.username.errors %}is-invalid{% endif %}" name="username" id="id_username" placeholder="Username" value="{{ form.username.value }}" /> <label for="id_username" class="form-label">Username</label> <label for="id_username">Username</label> </div> <div class="form-floating mb-3"> <input type="text" class="form-control {% if form.email.errors %}is-invalid{% endif %}" name="email" id="id_email" placeholder="Email" value="{{ form.email.value }}" /> <label for="id_email" class="form-label">Email</label> <input type="email" class="form-control {% if form.email.errors %}is-invalid{% endif %}" name="email" id="id_email" placeholder="Email" value="{{ form.email.value }}" /> <label for="id_email">Email</label> </div> <div class="form-floating mb-3"> <input type="text" class="form-control {% if form.discord_webhook_url.errors %}is-invalid{% endif %}" name="discord_webhook_url" id="id_discord_webhook_url" placeholder="Discord webhook url" value="{{ form.discord_webhook_url.value|default:"" }}" /> <label for="id_discord_webhook_url" class="form-label">Discord webhook url</label> <input type="text" class="form-control {% if form.discord_webhook_url.errors %}is-invalid{% endif %}" name="discord_webhook_url" id="id_discord_webhook_url" placeholder="Discord webhook URL" value="{{ form.discord_webhook_url.value|default:'' }}" /> <label for="id_discord_webhook_url">Discord webhook URL</label> </div> <div class="row"> <div class="col"> <button type="submit" class="btn btn-primary"> Update profile </button> <a href="{% url 'password_change' %}" class="btn btn-secondary"> Password change </a> </div> <div class="d-flex gap-2"> <button type="submit" class="btn btn-primary">Save โ</button> <a href="{% url 'password_change' %}" class="btn btn-outline-light">Change password</a> </div> </form> </div>
@@ -4,6 +4,7 @@ "workspaces": { "": { "dependencies": { "@fontsource/monaspace-argon": "^5.2.5", "@popperjs/core": "^2.11.5", "bootstrap": "^5.1.3", "chart.js": "^3.7.1",
@@ -72,6 +73,8 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], "@fontsource/monaspace-argon": ["@fontsource/monaspace-argon@5.2.5", "", {}, "sha512-EJ+jq1Smm3BB+8RK/gwB1uzjrSKdycZkKm8OZGCYvqJiTChKcGe7b2lmj0PmQbb/+lAEdCBIAcFLlPaWhtRANA=="], "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="], "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="],
@@ -6,6 +6,7 @@ "build": "vite build" }, "dependencies": { "@fontsource/monaspace-argon": "^5.2.5", "@popperjs/core": "^2.11.5", "bootstrap": "^5.1.3", "chart.js": "^3.7.1",
deleted
pages/static_src/images/home-hero.webp
binary file
modified
pages/static_src/index.js
@@ -1,3 +1 @@import "./styles/pages.css";import "./images/home-hero.webp";import "./styles/pages.scss";
deleted
pages/static_src/styles/pages.css
@@ -1,10 +0,0 @@.home-hero { background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), var(--background-image); background-size: cover; background-position: center; background-repeat: no-repeat; overflow: hidden; height: calc(100vh - 50px); max-height: 1080px;}
added
pages/static_src/styles/pages.scss
@@ -0,0 +1,154 @@@use "../../../status/static_src/styles/variables" as *;// โโ Home hero โโ.hero { position: relative; background: #090806; border-bottom: 1px solid rgba(107, 158, 120, 0.08); padding: 4rem 0 4rem; .hero-eyebrow { display: inline-flex; align-items: center; gap: 0.55rem; font-family: $font-family-monospace; font-size: 0.72rem; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: rgba(201, 168, 76, 0.85); background: rgba(201, 168, 76, 0.06); border: 1px solid $amber-border; padding: 0.35rem 0.75rem; border-radius: 999px; .eyebrow-dot { width: 6px; height: 6px; border-radius: 50%; background: $green; } } .hero-title { font-size: clamp(2rem, 5vw, 3.2rem); font-weight: 700; line-height: 1.1; color: $white; letter-spacing: -0.01em; margin-top: 1.5rem; .accent { color: $green-bright; } } .hero-sub { color: #a09890; font-size: 1rem; line-height: 1.7; margin-top: 1.25rem; max-width: 620px; } .hero-ctas { display: flex; gap: 0.75rem; flex-wrap: wrap; margin-top: 1.75rem; .btn { font-size: 0.82rem; letter-spacing: 0.04em; padding: 0.6rem 1.1rem; } }}// โโ Feature tiles โโ.feature { background: #13120e; border: 1px solid rgba(107, 158, 120, 0.08); border-radius: $border-radius; padding: 1.5rem; height: 100%; transition: border-color 250ms ease; &:hover { border-color: rgba(107, 158, 120, 0.22); } .feature-label { font-family: $font-family-monospace; font-size: 0.68rem; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: rgba(201, 168, 76, 0.75); } .feature-title { color: $white; font-size: 1.05rem; font-weight: 600; margin-top: 0.5rem; } .feature-desc { color: #a09890; font-size: 0.88rem; line-height: 1.65; margin-top: 0.6rem; margin-bottom: 0; }}// โโ Stat strip โโ.stat-strip { background: #0e0d0a; border-top: 1px solid rgba(107, 158, 120, 0.06); border-bottom: 1px solid rgba(107, 158, 120, 0.06); padding: 1.5rem 0; .stat { text-align: center; padding: 0 1rem; } .stat-label { font-family: $font-family-monospace; font-size: 0.66rem; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: #665f56; } .stat-value { font-size: 1.75rem; font-weight: 700; color: $green-bright; letter-spacing: -0.01em; line-height: 1.2; margin-top: 0.2rem; }}// โโ Changelog โโ.changelog-entry { display: grid; grid-template-columns: 140px 1fr; gap: 1.5rem; padding: 1rem 0; border-bottom: 1px solid rgba(107, 158, 120, 0.06); @media (max-width: 575px) { grid-template-columns: 1fr; gap: 0.4rem; } .changelog-date { font-family: $font-family-monospace; font-size: 0.78rem; color: rgba(201, 168, 76, 0.75); letter-spacing: 0.04em; } ul { margin: 0; padding-left: 1.1rem; color: $gray-200; font-size: 0.9rem; } li { margin-bottom: 0.3rem; }}
modified
pages/templates/pages/changelog.html
@@ -1,4 +1,5 @@{% extends 'base.html' %}{% load static %}{% block extra_head %}
@@ -6,8 +7,13 @@{% endblock %}{% block extra_css %}<link href="{% static 'pages.css' %}" rel="stylesheet">{% endblock %}{% block breadcrumbs %}<nav style="--bs-breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='%236c757d'/%3E%3C/svg%3E");" aria-label="breadcrumb"><nav aria-label="breadcrumb"> <ol class="breadcrumb mb-0"> <li class="breadcrumb-item"><a href="/">Home</a></li> <li class="breadcrumb-item active" aria-current="page">{{ title }}</li>
@@ -19,104 +25,128 @@{% block main %}<div class="container my-5"> <div class="row"> <div class="col-lg-6 offset-lg-3 col-sm-8 offset-sm-2 col-xs-12 offset-xs-0"> <h1>{{ title }}</h1> <p>New features and changes to status, sometimes I'll tease something coming soon.</p> <div class="col-lg-8 offset-lg-2"> <div class="section-label mb-2">release log</div> <h1 class="fw-bolder text-white" style="letter-spacing: -0.01em;">{{ title }}</h1> <p class="text-muted">Shipped changes, incidents, and the occasional tease for something coming.</p> </div> </div> <div class="row"> <div class="col-lg-6 offset-lg-3 col-sm-8 offset-sm-2 col-xs-12 offset-xs-0"> <div class="row border-bottom py-2"> <div class="col-3"> <span class="badge bg-success">2025-05-31</span> </div> <div class="col-9"> <ul class="mb-0"> <li>Improve alert system with state tracking and recovery notifications</li> </ul> </div> <div class="row mt-4"> <div class="col-lg-8 offset-lg-2"> <div class="changelog-entry"> <div class="changelog-date">2026-04-17</div> <ul> <li>Redesign the whole dashboard with a warm-earth palette โ mossy forest green and amber on deep charcoal, Monaspace Argon throughout</li> <li>Rework the property page with live-signals tiles, Lighthouse score cards, styled Chart.js panels, and compact insight tables</li> <li>Add recent-uptime bar strips and rolling uptime percentage to every property row on the dashboard</li> <li>Surface the full Lighthouse performance breakdown โ weighted metric tiles and top savings opportunities, ranked by impact</li> <li>Swap the ๐ favicon for a themed SVG tile matching the in-app logo</li> <li>Update the chromium wrapper to also pick up <code>google-chrome</code> so the webdev container runs locally without extra setup</li> <li>Fix logout 405 by switching to a POST form + CSRF (Django 5 <code>LogoutView</code> is POST-only)</li> </ul> </div> <div class="changelog-entry"> <div class="changelog-date">2026-04-16</div> <ul> <li>Add live crawl/lighthouse status panel with real-time recrawl and rerun buttons</li> <li>Rewrite the SEO crawler in-process using <code>requests</code> + <code>BeautifulSoup</code> โ no more Scrapy subprocess</li> <li>Harden the scheduler, alert state machine, and crawl pipeline against races and partial failures</li> <li>Harden the Lighthouse runner and surface failure reasons in the UI</li> <li>Migrate the frontend build from Webpack/Yarn to Vite/Bun</li> </ul> </div> <div class="changelog-entry"> <div class="changelog-date">2026-04-09</div> <ul> <li>Migrate Python package management from Pipenv/pyenv to <code>uv</code></li> </ul> </div> <div class="row border-bottom py-2"> <div class="col-3"> <span class="badge bg-success">2022-07-16</span> </div> <div class="col-9"> <ul class="mb-0"> <li>Add docker init to handle lighthouse chrome zombies</li> </ul> </div> <div class="changelog-entry"> <div class="changelog-date">2026-04-03</div> <ul> <li>Bind the Docker port to localhost only โ host should proxy in</li> </ul> </div> <div class="row border-bottom py-2"> <div class="col-3"> <span class="badge bg-success">2022-07-15</span> </div> <div class="col-9"> <ul class="mb-0"> <li>Add check queue to be more resource friendly</li> <li>Add lighthouse scores</li> </ul> </div> <div class="changelog-entry"> <div class="changelog-date">2025-06-01</div> <ul> <li>Refactor property email templates to share a base</li> </ul> </div> <div class="row border-bottom py-2"> <div class="col-3"> <span class="badge bg-success">2022-07-05</span> </div> <div class="col-9"> <ul class="mb-0"> <li>Improve property UI</li> <li>Improve properties listing UI</li> </ul> </div> <div class="changelog-entry"> <div class="changelog-date">2025-05-31</div> <ul> <li>Improve alert system with state tracking and recovery notifications โ no more alert spam on flapping sites</li> </ul> </div> <div class="row border-bottom py-2"> <div class="col-3"> <span class="badge bg-success">2022-07-04</span> </div> <div class="col-9"> <ul class="mb-0"> <li>Add properties listing pagination</li> <li>Add properties listing search</li> <li>Improve properties listing UI</li> </ul> </div> <div class="changelog-entry"> <div class="changelog-date">2022-07-25</div> <ul> <li>Add recrawl button</li> <li>Run crawler weekly on a schedule</li> </ul> </div> <div class="row border-bottom py-2"> <div class="col-3"> <span class="badge bg-success">2022-06-26</span> </div> <div class="col-9"> <ul class="mb-0"> <li>Add HSTS monitoring</li> </ul> </div> <div class="changelog-entry"> <div class="changelog-date">2022-07-23</div> <ul> <li>Add crawler insights โ title, description, canonical, OG tags, H1 extraction per page</li> </ul> </div> <div class="row border-bottom py-2"> <div class="col-3"> <span class="badge bg-success">2022-06-25</span> </div> <div class="col-9"> <ul class="mb-0"> <li>Add uptime chart</li> </ul> </div> <div class="changelog-entry"> <div class="changelog-date">2022-07-16</div> <ul> <li>Add docker init to handle Lighthouse Chrome zombies</li> </ul> </div> <div class="row border-bottom py-2"> <div class="col-3"> <span class="badge bg-success">2022-06-19</span> </div> <div class="col-9"> <ul class="mb-0"> <li>Create status project</li> </ul> </div> <div class="changelog-entry"> <div class="changelog-date">2022-07-15</div> <ul> <li>Add check queue to be more resource friendly</li> <li>Add Lighthouse scores</li> </ul> </div> <div class="changelog-entry"> <div class="changelog-date">2022-07-04</div> <ul> <li>Add properties listing pagination</li> <li>Add properties listing search</li> <li>Improve properties listing UI</li> </ul> </div> <div class="changelog-entry"> <div class="changelog-date">2022-06-26</div> <ul> <li>Add HSTS monitoring</li> </ul> </div> <div class="changelog-entry"> <div class="changelog-date">2022-06-25</div> <ul> <li>Add uptime chart</li> </ul> </div> <div class="changelog-entry"> <div class="changelog-date">2022-06-19</div> <ul> <li>Create status project</li> </ul> </div> </div> </div></div>
modified
pages/templates/pages/home.html
@@ -16,87 +16,145 @@{% block main %}<div class="bg-dark mb-5 home-hero d-flex align-items-center" style="--background-image: url({% static 'images/home-hero.webp' %});"><section class="hero"> <div class="container"> <div class="row"> <div class="col-12 col-lg-10 col-xl-8"> <div class="fs-4 text-light"> <span class="badge bg-warning text-black">Alpha</span> Things are rapidly changing! <div class="col-12 col-lg-7"> <span class="hero-eyebrow"> <span class="eyebrow-dot" aria-hidden="true"></span> v0.9 ยท alpha ยท self-hosted </span> <h1 class="hero-title"> Website status monitoring for operators who host their <span class="accent">own stack</span>. </h1> <p class="hero-sub"> HTTP checks every three minutes. Lighthouse audits. Security header analysis. Full-site SEO crawl. Discord + email alerts on state transitions. Your data never leaves your infrastructure. </p> <div class="hero-ctas"> <a href="/accounts/login/" class="btn btn-primary">Access dashboard โ</a> <a href="https://github.com/overshard/status" target="_blank" class="btn btn-outline-light">View source</a> </div> <h1 class="display-1 mt-4 text-light fw-bolder">Self-hosted website status monitoring</h1> <p class="mb-0 text-white mt-4 fs-4">Made by <a href="https://isaacbythewood.com/" class="text-white">Isaac Bythewood</a>, a simple opinionated self-hosted website status monitoring solution.</p> <div class="mt-4"> <a href="/accounts/login/" class="btn btn-light btn-lg me-3"> Login </a> <a href="https://github.com/overshard/status" target="_blank" class="btn btn-outline-light btn-lg"> Source code </a> </div> <div class="col-12 col-lg-5 mt-5 mt-lg-0 d-flex align-items-center"> <div class="terminal-block w-100"> <span class="t-line"><span class="t-comment"># probe cycle</span></span> <span class="t-line"><span class="t-prompt">probe$</span><span class="t-out">GET https://bythewood.me</span></span> <span class="t-line"><span class="t-key">status</span>=<span class="t-val">200 OK</span> <span class="t-key">rtt</span>=<span class="t-val">142ms</span></span> <span class="t-line"><span class="t-key">cert</span>=<span class="t-val">valid ยท 87d</span></span> <span class="t-line"><span class="t-prompt">probe$</span> <span class="t-cursor"></span></span> </div> </div> </div> </div></div><div class="container mb-2"> <div class="row justify-content-center"> <div class="col-12 col-md-6 col-lg-4"> <div class="card bg-primary text-white text-center mb-3"> <div class="card-body"> <div class="display-5">{{ total_properties }}</div> <div class="h5">Total properties</div></section><section class="stat-strip"> <div class="container"> <div class="row g-4"> <div class="col-6 col-md-3"> <div class="stat"> <div class="stat-label">properties</div> <div class="stat-value">{{ total_properties }}</div> </div> </div> <div class="col-6 col-md-3"> <div class="stat"> <div class="stat-label">checks logged</div> <div class="stat-value">{{ total_statuses }}</div> </div> </div> <div class="col-6 col-md-3"> <div class="stat"> <div class="stat-label">operators</div> <div class="stat-value">{{ total_users }}</div> </div> </div> <div class="col-6 col-md-3"> <div class="stat"> <div class="stat-label">online since</div> <div class="stat-value" style="font-size: 1.05rem;"> {% if first_status_created_at %}{{ first_status_created_at|date:"M j, Y" }}{% else %}โ{% endif %} </div> </div> </div> </div> </div></section><section class="container my-5 py-4"> <div class="row mb-4"> <div class="col-12"> <div class="section-label mb-2">capabilities</div> <h2 class="h3 text-white fw-bolder" style="letter-spacing: -0.01em;"> Everything your infrastructure team needs in one dashboard. </h2> </div> </div> <div class="row g-4"> <div class="col-12 col-md-6 col-lg-4"> <div class="feature"> <div class="feature-label">uptime</div> <div class="feature-title">Live HTTP probes</div> <p class="feature-desc"> Three-minute intervals, two-strike alert debouncing, SSL and timeout mapping. Discord + SMTP notifications only fire on real state transitions. </p> </div> </div> <div class="col-12 col-md-6 col-lg-4"> <div class="card bg-primary text-white text-center mb-3"> <div class="card-body"> <div class="display-5">{{ total_statuses }}</div> <div class="h5">Total events</div> </div> <div class="feature"> <div class="feature-label">performance</div> <div class="feature-title">Lighthouse audits</div> <p class="feature-desc"> Daily headless runs surface performance, accessibility, best practices, and SEO scores with weighted metric breakdowns and opportunity savings. </p> </div> </div> <div class="col-12 col-md-6 col-lg-4"> <div class="card bg-primary text-white text-center mb-3"> <div class="card-body"> <div class="display-5">{{ first_status_created_at|date:"M j, Y" }}</div> <div class="h5">First event logged</div> </div> <div class="feature"> <div class="feature-label">security</div> <div class="feature-title">Header analysis</div> <p class="feature-desc"> HTTPS, HSTS, HSTS preload, XSS and content-sniffing protection, clickjack defence, and server-version leak checks โ graded on every probe. </p> </div> </div> </div></div><div class="container"> <div class="row justify-content-center"> <div class="col-12 col-md-6 col-lg-4 mb-4"> <div class="card bg-light h-100"> <div class="card-body"> <h2 class="card-title h5">Open source</h2> <p class="card-text">The source code is available on GitHub and is published under the simplified BSD license, do what you want with it.</p> </div> <div class="col-12 col-md-6 col-lg-4"> <div class="feature"> <div class="feature-label">crawler</div> <div class="feature-title">Weekly SEO spider</div> <p class="feature-desc"> Extracts title, description, canonical, OG tags, and H1 per page โ every mismatch becomes a severity-ranked insight you can drill into. </p> </div> </div> <div class="col-12 col-md-6 col-lg-4 mb-4"> <div class="card bg-light h-100"> <div class="card-body"> <h2 class="card-title h5">Privacy friendly</h2> <p class="card-text">The status data is stored on whatever hosting service you put it on. You can store as much, or as little, data as you want.</p> </div> <div class="col-12 col-md-6 col-lg-4"> <div class="feature"> <div class="feature-label">sharing</div> <div class="feature-title">Public status pages</div> <p class="feature-desc"> Toggle a property public and share the URL โ no account required, customers see live status, response graphs, and uptime at a glance. </p> </div> </div> <div class="col-12 col-md-6 col-lg-4 mb-4"> <div class="card bg-light h-100"> <div class="card-body"> <h2 class="card-title h5">First class mobile support</h2> <p class="card-text">A responsive design that should work across all platforms.</p> </div> <div class="col-12 col-md-6 col-lg-4"> <div class="feature"> <div class="feature-label">ownership</div> <div class="feature-title">BSD, single container</div> <p class="feature-desc"> Django + SQLite + a scheduler. Ships as a Docker Compose stack. Your data, your ingress, your retention policy. No SaaS vendor lock-in. </p> </div> </div> </div></div></section>{% endblock %}
@@ -39,8 +39,16 @@ def changelog(request):def favicon(request): icon = "๐" svg = f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y="80" font-size="80">{icon}</text></svg>' # Heartbeat/pulse mark โ a single EKG-style waveform sweep, reads as # "vital signs / liveness" and is distinct from bars, rings, and shells. svg = ( '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">' '<polyline points="2,34 18,34 24,28 30,14 36,52 42,20 48,34 62,34" ' 'fill="none" stroke="#6b9e78" stroke-width="6" ' 'stroke-linejoin="round" stroke-linecap="round"/>' '<circle cx="30" cy="14" r="3.5" fill="#c9a84c"/>' "</svg>" ) return HttpResponse(svg, content_type="image/svg+xml")
modified
properties/forms.py
@@ -20,13 +20,3 @@ class PropertyForm(forms.ModelForm): except requests.exceptions.RequestException: raise forms.ValidationError('Invalid URL') return r.url # NOTE: requests' "Final URL location of Response."class PropertyImportForm(forms.Form): csv_file = forms.FileField() def clean_csv_file(self): csv_file = self.cleaned_data['csv_file'] if not csv_file.name.endswith('.csv'): raise forms.ValidationError('Invalid file type') return csv_file
modified
properties/models.py
@@ -450,6 +450,24 @@ class Property(CrawlerMixin, AlertsMixin, SecurityMixin, models.Model): scores = [score for score in self.lighthouse_scores.values()] return round(sum(scores) / len(scores)) def recent_tick_stream(self, limit=30): """Most-recent-first list of 'up' / 'down' strings for the uptime strip.""" codes = list( self.statuses.order_by("-created_at").values_list("status_code", flat=True)[:limit] ) return ["up" if c == 200 else "down" for c in reversed(codes)] @cached_property def recent_uptime_pct(self): """Uptime percentage over the most recent 100 checks, rounded to one decimal.""" codes = list( self.statuses.order_by("-created_at").values_list("status_code", flat=True)[:100] ) if not codes: return None up = sum(1 for c in codes if c == 200) return round((up / len(codes)) * 100, 1)class Check(models.Model): property = models.ForeignKey(
modified
properties/static_src/scripts/property_graphs.js
@@ -1,19 +1,54 @@import Chart from "chart.js/auto";const accent = { green: "#6b9e78", greenBright: "#7db88c", greenFill: "rgba(107, 158, 120, 0.35)", amber: "#c9a84c", amberFill: "rgba(201, 168, 76, 0.35)", terracotta: "#c47055", terracottaFill: "rgba(196, 112, 85, 0.35)", slate: "#7eaab8", slateFill: "rgba(126, 170, 184, 0.3)", grid: "rgba(107, 158, 120, 0.08)", ticks: "#847c72",};const backgroundColors = [ "rgba(13, 110, 253, 0.4)", "rgba(102, 16, 242, 0.4)", "rgba(111, 66, 193, 0.4)", "rgba(214, 51, 132, 0.4)", "rgba(220, 53, 69, 0.4)", "rgba(253, 126, 20, 0.4)", "rgba(255, 193, 7, 0.4)", "rgba(25, 135, 84, 0.4)", "rgba(32, 201, 151, 0.4)", "rgba(13, 202, 240, 0.4)", accent.greenFill, accent.terracottaFill, accent.amberFill, accent.slateFill, "rgba(221, 215, 205, 0.18)", "rgba(160, 152, 144, 0.3)", "rgba(107, 158, 120, 0.55)", "rgba(196, 112, 85, 0.55)", "rgba(201, 168, 76, 0.55)", "rgba(126, 170, 184, 0.55)",];const borderColors = [ "rgba(107, 158, 120, 0.9)", "rgba(196, 112, 85, 0.9)", "rgba(201, 168, 76, 0.9)", "rgba(126, 170, 184, 0.9)", "rgba(221, 215, 205, 0.5)", "rgba(160, 152, 144, 0.7)", "rgba(107, 158, 120, 1)", "rgba(196, 112, 85, 1)", "rgba(201, 168, 76, 1)", "rgba(126, 170, 184, 1)",];const fontStack = 'Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace';const fontStack = '"Monaspace Argon", Consolas, "Liberation Mono", Monaco, "Courier New", monospace';Chart.defaults.color = accent.ticks;Chart.defaults.borderColor = accent.grid;Chart.defaults.font.family = fontStack;Chart.defaults.font.size = 11;const tickFont = { size: 11, family: fontStack };const legendLabel = { boxWidth: 10, boxHeight: 10, font: tickFont, color: "#d1d8d4" };document.addEventListener("DOMContentLoaded", function () { const canvas = document.getElementById("chart-response-times");
@@ -22,6 +57,9 @@ document.addEventListener("DOMContentLoaded", function () { document.getElementById("chart-status-response-times-data").innerHTML ); const ctx = canvas.getContext("2d"); const gradient = ctx.createLinearGradient(0, 0, 0, 300); gradient.addColorStop(0, "rgba(107, 158, 120, 0.35)"); gradient.addColorStop(1, "rgba(107, 158, 120, 0)"); const chart = new Chart(ctx, { type: "line", data: {
@@ -31,11 +69,15 @@ document.addEventListener("DOMContentLoaded", function () { }), datasets: [ { label: "Response times (ms)", label: "Response time (ms)", data: data.map((d) => d.count), backgroundColor: "rgba(13, 110, 253, 0.4)", borderColor: "rgba(13, 110, 253, 0.8)", borderWidth: 3, backgroundColor: gradient, borderColor: accent.green, borderWidth: 2, pointRadius: 0, pointHoverRadius: 4, pointHoverBackgroundColor: accent.greenBright, tension: 0.25, fill: true, }, ],
@@ -43,47 +85,34 @@ document.addEventListener("DOMContentLoaded", function () { options: { responsive: true, maintainAspectRatio: false, animation: { duration: 0, }, animation: { duration: 0 }, plugins: { tooltip: { mode: "index", intersect: false, backgroundColor: "rgba(9, 8, 6, 0.95)", titleColor: "#ede8e0", bodyColor: "#ddd7cd", borderColor: "rgba(107, 158, 120, 0.2)", borderWidth: 1, padding: 10, titleFont: tickFont, bodyFont: tickFont, }, legend: { position: "top", labels: { boxWidth: 10, boxHeight: 10, font: { size: 12, family: fontStack, }, }, }, legend: { position: "top", labels: legendLabel }, }, scales: { xAxes: { ticks: { autoSkip: true, font: { size: 12, family: fontStack, }, maxRotation: 25, }, x: { grid: { color: accent.grid, drawBorder: false }, ticks: { autoSkip: true, maxRotation: 25, font: tickFont, color: accent.ticks }, }, yAxes: { y: { grid: { color: accent.grid, drawBorder: false }, ticks: { beginAtZero: true, font: { size: 12, family: fontStack, }, callback: function(value, index, ticks) { return `${value} ms`; } font: tickFont, color: accent.ticks, callback: (value) => `${value} ms`, }, }, },
@@ -93,56 +122,30 @@ document.addEventListener("DOMContentLoaded", function () { chart.canvas.parentNode.style.height = "300px";});document.addEventListener("DOMContentLoaded", function () { const canvas = document.getElementById("chart-status-codes");function buildDoughnut(canvasId, dataId) { const canvas = document.getElementById(canvasId); if (!canvas) return; const data = JSON.parse( document.getElementById("chart-status-codes-data").innerHTML ); const data = JSON.parse(document.getElementById(dataId).innerHTML); const ctx = canvas.getContext("2d"); new Chart(ctx, { type: "doughnut", data: { labels: data.map((d) => d.label), datasets: [ { data: data.map((d) => d.count), backgroundColor: backgroundColors, borderColor: backgroundColors, borderWidth: 1, }, ], }, options: { responsive: true, aspectRatio: 2, animation: { animateRotate: false, }, plugins: { legend: { position: "right", labels: { boxWidth: 10, boxHeight: 10, font: { size: 12, family: fontStack, }, }, }, }, }, // Color rule: status 200 / Uptime = moss green, non-200 / Downtime = // terracotta, everything else pulls from the earthy palette in order. const paint = (label) => { const name = String(label || "").toLowerCase(); if (name === "uptime" || name === "200") return [accent.greenFill, "rgba(107, 158, 120, 0.95)"]; if (name === "downtime") return [accent.terracottaFill, "rgba(196, 112, 85, 0.95)"]; return null; }; const bg = data.map((d, i) => { const p = paint(d.label); return p ? p[0] : backgroundColors[i % backgroundColors.length]; }); const bd = data.map((d, i) => { const p = paint(d.label); return p ? p[1] : borderColors[i % borderColors.length]; });});document.addEventListener("DOMContentLoaded", function () { const canvas = document.getElementById("chart-uptime"); if (!canvas) return; const data = JSON.parse( document.getElementById("chart-uptime-data").innerHTML ); const ctx = canvas.getContext("2d"); new Chart(ctx, { type: "doughnut", data: {
@@ -150,31 +153,33 @@ document.addEventListener("DOMContentLoaded", function () { datasets: [ { data: data.map((d) => d.count), backgroundColor: backgroundColors, borderColor: backgroundColors, borderWidth: 1, backgroundColor: bg, borderColor: bd, borderWidth: 1.5, }, ], }, options: { responsive: true, aspectRatio: 2, animation: { animateRotate: false, }, animation: { animateRotate: false }, cutout: "62%", plugins: { legend: { position: "right", labels: { boxWidth: 10, boxHeight: 10, font: { size: 12, family: fontStack, }, }, legend: { position: "right", labels: legendLabel }, tooltip: { backgroundColor: "rgba(9, 8, 6, 0.95)", titleColor: "#ede8e0", bodyColor: "#ddd7cd", borderColor: "rgba(107, 158, 120, 0.2)", borderWidth: 1, padding: 10, titleFont: tickFont, bodyFont: tickFont, }, }, }, });});}document.addEventListener("DOMContentLoaded", () => buildDoughnut("chart-status-codes", "chart-status-codes-data"));document.addEventListener("DOMContentLoaded", () => buildDoughnut("chart-uptime", "chart-uptime-data"));
modified
properties/templates/properties/properties.html
@@ -8,7 +8,7 @@{% block breadcrumbs %}<nav style="--bs-breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='%236c757d'/%3E%3C/svg%3E");" aria-label="breadcrumb"><nav aria-label="breadcrumb"> <ol class="breadcrumb mb-0"> <li class="breadcrumb-item"><a href="/">Home</a></li> <li class="breadcrumb-item active" aria-current="page">{{ title }}</li>
@@ -18,177 +18,196 @@{% block main %}<div class="container"> <div class="row my-3 d-flex align-items-center"> <div class="col-sm-6"> <h1>{{ title }}</h1> <p>All properties you've created with their current active status and issues.</p><div class="container my-4"> <div class="row align-items-center g-3 mb-4"> <div class="col-md-5"> <div class="section-label mb-1">operator ยท {{ request.user.username }}</div> <h1 class="fw-bolder text-white mb-0" style="letter-spacing: -0.01em;">{{ title }}</h1> <p class="text-muted mb-0 small mt-1">Every URL you've registered, current probe state, and audit signals.</p> </div> <div class="col-sm-6"> <form method="get" class="d-flex"> <div class="form-floating flex-grow-1 rounded-0 rounded-start"> <input type="text" class="form-control" name="q" id="id_search" placeholder="Search" {% if q %}value="{{ q }}"{% endif %} /> <label for="id_search" class="form-label">Search</label> </div> <button type="submit" class="btn btn-secondary px-3 rounded-0 rounded-end"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-search" viewBox="0 0 16 16"> <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"/> </svg> </button> <button class="btn btn-primary px-3 ms-4" type="button" data-bs-toggle="collapse" data-bs-target="#collapsePropertyAdd" aria-expanded="false" aria-controls="collapsePropertyAdd"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-node-plus" viewBox="0 0 16 16"> <path fill-rule="evenodd" d="M11 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8zM6.025 7.5a5 5 0 1 1 0 1H4A1.5 1.5 0 0 1 2.5 10h-1A1.5 1.5 0 0 1 0 8.5v-1A1.5 1.5 0 0 1 1.5 6h1A1.5 1.5 0 0 1 4 7.5h2.025zM11 5a.5.5 0 0 1 .5.5v2h2a.5.5 0 0 1 0 1h-2v2a.5.5 0 0 1-1 0v-2h-2a.5.5 0 0 1 0-1h2v-2A.5.5 0 0 1 11 5zM1.5 7a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1z"/> </svg> <div class="col-md-7"> <div class="dashboard-toolbar"> <form method="get" class="search-form flex-grow-1"> <span class="search-icon"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"> <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"/> </svg> </span> <input type="text" class="form-control search-input" name="q" id="id_search" placeholder="grep properties..." {% if q %}value="{{ q }}"{% endif %} /> </form> <button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapsePropertyAdd" aria-expanded="false" aria-controls="collapsePropertyAdd"> + new </button> </form> </div> </div> </div> <div class="row mx-0 mb-3 bg-dark py-3 rounded border border-warning collapse" id="collapsePropertyAdd"> <div class="col-sm-6"> <h2 class="text-white">Add a new property</h2> <p class="text-white">All public URLs are supported.</p> {{ form.errors }} <form method="POST" class="d-flex"> <div class="collapse mb-4" id="collapsePropertyAdd"> <div class="bg-surface border-subtle rounded border p-3"> <div class="section-label mb-2">register property</div> <p class="text-muted small mb-3">Any public URL. Probe begins within 3 minutes.</p> <form method="POST" class="dashboard-toolbar"> {% csrf_token %} <div class="flex-grow-1"> <div class="form-floating"> <input type="text" name="url" id="id_url" class="form-control {% if form.url.errors %}is-invalid{% endif %} rounded-0 rounded-start" placeholder="URL" required> <label for="id_url">URL</label> </div> </div> <button type="submit" class="btn btn-primary px-3 rounded-0 rounded-end"> <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="currentColor" class="bi bi-plus" viewBox="0 0 16 16"> <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/> </svg> </button> <input type="url" name="url" id="id_url" class="form-control flex-grow-1 {% if form.url.errors %}is-invalid{% endif %}" placeholder="https://example.com" required> <button type="submit" class="btn btn-primary">Add โ</button> </form> {% if form.errors %}<div class="text-danger small mt-2">{{ form.errors }}</div>{% endif %} </div> <div class="col-sm-6"> <h2 class="text-white">Bulk import properties</h2> <p class="text-white">Add a CSV file with the first column being the URL.</p> <form method="POST" class="d-flex" enctype="multipart/form-data" action="{% url 'import_properties' %}"> {% csrf_token %} <input type="file" name="csv_file" id="id_csv_file" class="form-control py-3 px-4 {% if form.file.errors %}is-invalid{% endif %} rounded-0 rounded-start" required> <button type="submit" class="btn btn-primary px-4 rounded-0 rounded-end"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-download" viewBox="0 0 16 16"> <path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/> <path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/> </svg> </button> </form> </div> <div class="row g-3 mb-4"> <div class="col-6 col-md-3"> <div class="metric-tile metric-accent-green"> <div class="metric-label">total properties</div> <div class="metric-value">{{ user.total_properties }}</div> </div> </div> <div class="col-6 col-md-3"> <div class="metric-tile {% if user.total_properties_down %}metric-accent-danger{% else %}metric-accent-green{% endif %}"> <div class="metric-label">currently down</div> <div class="metric-value">{{ user.total_properties_down }}</div> </div> </div> <div class="col-6 col-md-3"> <div class="metric-tile metric-accent-green"> <div class="metric-label">checks logged</div> <div class="metric-value">{{ user.total_checks }}</div> </div> </div> <div class="col-6 col-md-3"> <div class="metric-tile metric-accent-amber"> <div class="metric-label">probe cycle</div> <div class="metric-value" style="font-size: 1.2rem;">3m ยท <span class="text-muted">live</span></div> <div class="metric-sub">last sync: <span class="text-green">online</span></div> </div> </div> </div> <div class="row"> <div class="col" id="properties"> <div class="card rounded-0 border-0 rounded-top py-1 bg-secondary text-white"> <div class="row g-0"> <div class="col-6 col-md-2 d-flex align-items-center"> <div class="card-body py-1"> <div class="card-title h3 mb-0">{{ user.total_properties }}</div> <p class="card-text">Properties</p> </div> <div class="section-label mb-2">registered properties</div> {% for property in properties.object_list %} <div class="property-row {% if property.current_status != 200 %}is-down{% endif %}"> <div class="pr-main"> <div class="pr-title"> <span class="status-dot {% if property.current_status == 200 %}is-up{% else %}is-down{% endif %}" aria-hidden="true"></span> <a href="{% url 'property' property.id %}" class="text-truncate">{{ property.name }}</a> </div> <div class="pr-url">{{ property.url }}</div> <div class="uptime-strip" aria-label="Recent checks"> {% for tick in property.recent_tick_stream %} <span class="uptime-tick is-{{ tick }}"></span> {% empty %} {% for _ in "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" %}<span class="uptime-tick is-none"></span>{% endfor %} {% endfor %} </div> </div> <div class="pr-side"> <div class="pr-meta"> <div class="pr-meta-cell"> <span class="pr-meta-k">status</span> {% if property.current_status == 200 %} <span class="chip chip-ok">ok ยท 200</span> {% else %} <span class="chip chip-down">down ยท {{ property.current_status }}</span> {% endif %} </div> <div class="col-6 offset-md-2 col-md-2 d-flex align-items-center"> <div class="card-body py-1"> <div class="card-title h3 mb-0">{{ user.total_properties_down }}</div> <p class="card-text">Properties down</p> </div> <div class="pr-meta-cell"> <span class="pr-meta-k">uptime</span> {% if property.recent_uptime_pct is not None %} {% if property.recent_uptime_pct >= 99 %} <span class="chip chip-ok">{{ property.recent_uptime_pct }}%</span> {% elif property.recent_uptime_pct < 95 %} <span class="chip chip-down">{{ property.recent_uptime_pct }}%</span> {% else %} <span class="chip chip-warn">{{ property.recent_uptime_pct }}%</span> {% endif %} {% else %} <span class="chip chip-muted">โ</span> {% endif %} </div> <div class="col-6 col-md-2 d-flex align-items-center"> <div class="card-body py-1"> <div class="card-title h3 mb-0">{{ user.total_checks }}</div> <p class="card-text">Total checks</p> </div> <div class="pr-meta-cell"> <span class="pr-meta-k">sec</span> {% if property.has_security_issue %} <span class="chip chip-warn">issues</span> {% else %} <span class="chip chip-ok">ok</span> {% endif %} </div> {% with insights=property.crawler_insights|length %} <div class="pr-meta-cell"> <span class="pr-meta-k">seo</span> {% if insights > 100 %} <span class="chip chip-down">{{ insights }}</span> {% elif insights > 25 %} <span class="chip chip-warn">{{ insights }}</span> {% else %} <span class="chip chip-ok">{{ insights }}</span> {% endif %} </div> {% endwith %} <div class="pr-meta-cell"> <span class="pr-meta-k">lh</span> {% if property.avg_lighthouse_score %} {% if property.avg_lighthouse_score >= 90 %} <span class="chip chip-ok">{{ property.avg_lighthouse_score }}%</span> {% elif property.avg_lighthouse_score >= 80 %} <span class="chip chip-warn">{{ property.avg_lighthouse_score }}%</span> {% else %} <span class="chip chip-down">{{ property.avg_lighthouse_score }}%</span> {% endif %} {% else %} <span class="chip chip-muted">โ</span> {% endif %} </div> </div> </div> {% for property in properties.object_list %} <div class="card rounded-0 border-0"> <div class="row g-0"> <div class="col-12 col-md-4 bg-dark text-white"> <div class="card-body"> <h2 class="card-title h4 text-truncate">{{ property.name }}</h2> <div class="d-flex"> <a href="{% url 'property' property.id %}" class="btn btn-sm btn-success me-2 w-25">View</a> {% if not property.is_protected %} <button type="button" class="btn btn-sm btn-outline-danger w-25" data-bs-toggle="modal" data-bs-target="#delete-modal-{{ property.id }}"> Delete </button> <div class="pr-actions"> {% if not property.is_protected %} <button type="button" class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#delete-modal-{{ property.id }}"> Delete </button> <div class="modal fade" id="delete-modal-{{ property.id }}" tabindex="-1" aria-labelledby="delete-modal-{{ property.id }}-label" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title text-white" id="delete-modal-{{ property.id }}-label">Confirm delete</h5> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal fade text-dark" id="delete-modal-{{ property.id }}" tabindex="-1" aria-labelledby="delete-modal-{{ property.id }}-label" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="delete-modal-{{ property.id }}-label">Confirm property delete</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> Are you sure you want to delete <strong>{{ property.url }}</strong>? </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">I've changed my mind</button> <a href="{% url 'property_delete' property.id %}" class="btn btn-danger">Confirm</a> </div> </div> </div> <div class="modal-body"> Are you sure you want to delete <strong class="text-white">{{ property.url }}</strong>? All checks and audit data will be removed. </div> {% endif %} </div> </div> <div class="col-6 col-md-2 d-flex align-items-center text-white {% if property.current_status == 200 %}bg-success{% else %}bg-danger{% endif %}"> <div class="card-body"> <div class="card-title h4">{% if property.current_status == 200 %}Ok{% else %}Failed{% endif %}</div> <div class="card-text text-truncate small">Current status</div> </div> </div> <div class="col-6 col-md-2 d-none d-md-flex d-flex align-items-center text-white {% if not property.has_security_issue %}bg-success{% else %}bg-danger{% endif %}"> <div class="card-body"> <div class="card-title h4">{% if not property.has_security_issue %}Ok{% else %}Failed{% endif %}</div> <div class="card-text text-truncate small">Security</div> </div> </div> {% with property.crawler_insights|length as insights %} <div class="col-6 col-md-2 d-flex align-items-center text-white {% if insights > 100 %}bg-danger{% elif insights > 25 %}bg-warning{% else %}bg-success{% endif %}"> <div class="card-body"> <div class="card-title h4">{{ insights }}</div> <div class="card-text text-truncate small">Crawler issues</div> </div> </div> {% endwith %} <div class="col-6 col-md-2 d-none d-md-flex d-flex align-items-center text-white {% if property.avg_lighthouse_score < 80 %}bg-warning{% else %}bg-success{% endif %}"> <div class="card-body"> <div class="card-title h4"> {% if property.avg_lighthouse_score %} {{ property.avg_lighthouse_score }}% {% else %} Checking... {% endif %} <div class="modal-footer"> <button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">Cancel</button> <a href="{% url 'property_delete' property.id %}" class="btn btn-outline-danger">Delete</a> </div> <div class="card-text text-truncate small">Avg. LH score</div> </div> </div> </div> {% endif %} <a href="{% url 'property' property.id %}" class="btn btn-sm btn-outline-light">View โ</a> </div> {% endfor %} </div> </div> </div> {% empty %} <div class="text-center py-5 text-muted"> <div class="section-label mb-3" style="justify-content:center;">no properties yet</div> <p class="mb-0 small">Click <strong>+ new</strong> above to register your first URL.</p> </div> {% endfor %} {% if properties.paginator.num_pages > 1 %} <div class="row mt-5"> <div class="row mt-4"> <div class="col d-flex justify-content-center"> <nav aria-label="Pagination"> <ul class="pagination"> <li class="page-item {% if not properties.has_previous %}disabled{% endif %}"> <a class="page-link" {% if properties.has_previous %}href="?page={{ properties.previous_page_number }}"{% endif %}>Previous</a> <a class="page-link" {% if properties.has_previous %}href="?page={{ properties.previous_page_number }}"{% endif %}>โ Prev</a> </li> {% for page_num in properties.paginator.page_range %} <li class="page-item {% if properties.number == page_num %}active{% endif %}"> <a class="page-link" href="/properties/?page={{ page_num }}">{{ page_num }}</a> </li> {% endfor %} <li class="page-item {% if not properties.has_next %}disabled{% endif %}"> <a class="page-link" {% if properties.has_next %}href="?page={{ properties.next_page_number }}"{% endif %}>Next</a> <a class="page-link" {% if properties.has_next %}href="?page={{ properties.next_page_number }}"{% endif %}>Next โ</a> </li> </ul> </nav>
modified
properties/templates/properties/property.html
@@ -16,7 +16,7 @@{% block breadcrumbs %}<nav style="--bs-breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='%236c757d'/%3E%3C/svg%3E");" aria-label="breadcrumb"><nav aria-label="breadcrumb"> <ol class="breadcrumb mb-0"> <li class="breadcrumb-item"><a href="/">Home</a></li> <li class="breadcrumb-item"><a href="/properties/">Properties</a></li>
@@ -27,113 +27,115 @@{% block main %}<div class="bg-dark text-white py-2"><div class="bg-deep border-bottom border-subtle py-4"> <div class="container"> <div class="row"> <div class="col-12 col-lg-5"> <div class="d-flex align-items-center"> <h1 class="me-3 display-5 text-truncate">{{ title }}</h1> </div> <div class="row align-items-center g-3"> <div class="col-12 col-lg-6"> <div class="section-label mb-2">property ยท <span class="text-green">{% if property.current_status == 200 %}online{% else %}offline{% endif %}</span></div> <h1 class="mb-1 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" class="text-muted small text-decoration-none" style="font-family: var(--bs-font-monospace);">{{ property.url }} โ</a> </div> <div class="col-12 col-lg-7 d-flex align-items-center justify-content-lg-end"> <div class="d-lg-flex my-lg-0 d-print-none"> {% if user.is_authenticated %} <form id="is-public-form" method="POST" class="d-flex align-items-center"> {% csrf_token %} <div class="form-check form-switch my-1"> <input class="form-check-input" type="checkbox" role="switch" name="is_public" id="is-public-switch" {% if property.is_public %}checked{% endif %}> <label class="form-check-label" for="{{ custom_event.event|slugify }}-switch"> <span class="badge bg-warning text-dark rounded-pill" data-bs-placement="bottom" data-bs-toggle="tooltip" title="Anyone with the URL will have access to this property, you can disable at anytime.">?</span> Public Property </label> </div> </form> {% endif %} {% if user.is_authenticated %} <a href="{% url 'property' property.id %}?report" target="_blank" class="btn btn-sm btn-primary ms-0 ms-lg-3 my-1"> Report </a> {% endif %} {% if not property.is_protected and user.is_authenticated %} <button type="button" class="btn btn-sm btn-outline-danger ms-1 ms-lg-3 my-1" data-bs-toggle="modal" data-bs-target="#delete-modal-{{ property.id }}"> Delete </button> <div class="modal fade text-dark" id="delete-modal-{{ property.id }}" tabindex="-1" aria-labelledby="delete-modal-{{ property.id }}-label" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="delete-modal-{{ property.id }}-label">Confirm property delete</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> Are you sure you want to delete <strong>{{ property.name }}</strong>? </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">I've changed my mind</button> <a href="{% url 'property_delete' property.id %}" class="btn btn-danger">Confirm</a> </div> <div class="col-12 col-lg-6 d-flex flex-wrap gap-2 justify-content-lg-end align-items-center d-print-none"> {% if user.is_authenticated %} <form id="is-public-form" method="POST" class="d-inline-flex align-items-center gap-2"> {% csrf_token %} <span class="toggle-label" data-bs-toggle="tooltip" data-bs-placement="bottom" 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> </form> <a href="{% url 'property' property.id %}?report" target="_blank" class="btn btn-sm btn-outline-light">Report ยท pdf</a> <a href="{% url 'property' property.id %}?report=md" target="_blank" class="btn btn-sm btn-outline-light">Report ยท md</a> {% if not property.is_protected %} <button type="button" class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#delete-modal-{{ property.id }}"> Delete </button> <div class="modal fade" id="delete-modal-{{ property.id }}" tabindex="-1" aria-labelledby="delete-modal-{{ property.id }}-label" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title text-white" id="delete-modal-{{ property.id }}-label">Confirm delete</h5> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> Delete <strong class="text-white">{{ property.name }}</strong>? All probe and audit data will be removed. </div> <div class="modal-footer"> <button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">Cancel</button> <a href="{% url 'property_delete' property.id %}" class="btn btn-outline-danger">Delete</a> </div> </div> </div> {% endif %} </div> {% endif %} {% endif %} </div> </div> </div></div><div class="container-fluid"> <div class="row {% if not property.lighthouse_scores %}mb-4{% endif %}"> <div class="col-6 col-md-3 d-flex align-items-center text-white {% if property.current_status == 200 %}bg-success{% else %}bg-danger{% endif %}"> <div class="card-body text-center py-2"> <div class="card-title h4">{% if property.current_status == 200 %}Ok{% else %}Failed{% endif %}</div> <div class="card-text text-truncate small">Current status</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">{% if property.current_status == 200 %}200{% else %}{{ property.current_status }}{% endif %}</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 d-flex align-items-center text-white {% if not property.invalid_cert %}bg-success{% else %}bg-danger{% endif %}"> <div class="card-body text-center py-2"> <div class="card-title h4">{% if not property.invalid_cert %}Ok{% else %}Unhealthy{% endif %}</div> <div class="card-text text-truncate small">Certificate</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 d-flex align-items-center text-white {% if not property.has_security_issue %}bg-success{% else %}bg-danger{% endif %}"> <div class="card-body text-center py-2"> <div class="card-title h4">{% if not property.has_security_issue %}Ok{% else %}Failed{% endif %}</div> <div class="card-text text-truncate small">Security</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 d-flex align-items-center text-white {% if property.avg_response_time > 500 %}bg-danger{% else %}bg-success{% endif %}"> <div class="card-body text-center py-2"> <div class="card-title h4">{% if property.avg_response_time > 500 %}Unhealthy{% else %}Ok{% endif %}</div> <div class="card-text text-truncate small">Response time</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 style="font-size: 0.9rem; color: #89918c;">ms</span></div> <div class="metric-sub">rolling avg</div> </div> </div> </div></div>{% if property.lighthouse_scores %}<div class="container-fluid"> <div class="row mb-4"> {% 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 d-flex align-items-center text-white {% if score >= 80 %}bg-success{% else %}bg-warning{% endif %}"> <div class="card-body text-center py-2"> <div class="card-title h4">{{ score }}%</div> <div class="card-text text-truncate small">{{ category }}</div> <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 style="font-size: 0.9rem; color: #89918c;">%</span></div> </div> </div> {% endfor %} </div> {% endif %}</div>{% endif %}{% if user.is_authenticated %}<form style="display:none">{% csrf_token %}</form><div class="container mt-4 d-print-none" id="monitoring-status" data-property-id="{{ property.id }}" data-status-url="{% url 'property_status' property.id %}" data-recrawl-url="{% url 'property_recrawl' property.id %}" data-rerun-lighthouse-url="{% url 'property_rerun_lighthouse' property.id %}"><div class="container my-4 d-print-none" id="monitoring-status" data-property-id="{{ property.id }}" data-status-url="{% url 'property_status' property.id %}" data-recrawl-url="{% url 'property_recrawl' property.id %}" data-rerun-lighthouse-url="{% url 'property_rerun_lighthouse' 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="card h-100"> <div class="card-header bg-dark text-white d-flex justify-content-between align-items-center"> <strong>Crawler</strong> <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="badge" data-field="crawler.state_badge"> </span> <button type="button" id="recrawl-btn" class="btn btn-sm btn-outline-light py-0" title="Recrawl now">
@@ -142,40 +144,35 @@ </button> </span> </div> <div class="card-body"> <div class="progress mb-3 d-none" data-field="crawler.progress_wrap" style="height: 6px;"> <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 class="row small mb-0"> <dt class="col-5 col-sm-5 text-muted">Last success</dt> <dd class="col-7 col-sm-7 mb-1" data-field="crawler.last_success">โ</dd> <dt class="col-5 col-sm-5 text-muted">Last attempt</dt> <dd class="col-7 col-sm-7 mb-1" data-field="crawler.last_attempt">โ</dd> <dt class="col-5 col-sm-5 text-muted">Pages crawled</dt> <dd class="col-7 col-sm-7 mb-1" data-field="crawler.pages">โ</dd> <dt class="col-5 col-sm-5 text-muted">Duration</dt> <dd class="col-7 col-sm-7 mb-1" data-field="crawler.duration">โ</dd> <dt class="col-5 col-sm-5 text-muted">Issues found</dt> <dd class="col-7 col-sm-7 mb-1" data-field="crawler.insights">โ</dd> <dt class="col-5 col-sm-5 text-muted">Next run</dt> <dd class="col-7 col-sm-7 mb-0" data-field="crawler.next_run">โ</dd> <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="card h-100"> <div class="card-header bg-dark text-white d-flex justify-content-between align-items-center"> <strong>Lighthouse</strong> <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="badge" data-field="lighthouse.state_badge"> </span> <button type="button" id="rerun-lighthouse-btn" class="btn btn-sm btn-outline-light py-0" title="Rerun Lighthouse now">
@@ -184,23 +181,20 @@ </button> </span> </div> <div class="card-body"> <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 class="row small mb-0"> <dt class="col-5 col-sm-5 text-muted">Last success</dt> <dd class="col-7 col-sm-7 mb-1" data-field="lighthouse.last_success">โ</dd> <dt class="col-5 col-sm-5 text-muted">Last attempt</dt> <dd class="col-7 col-sm-7 mb-1" data-field="lighthouse.last_attempt">โ</dd> <dt class="col-5 col-sm-5 text-muted">Duration</dt> <dd class="col-7 col-sm-7 mb-1" data-field="lighthouse.duration">โ</dd> <dt class="col-5 col-sm-5 text-muted">Next run</dt> <dd class="col-7 col-sm-7 mb-0" data-field="lighthouse.next_run">โ</dd> <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>
@@ -209,89 +203,91 @@</div>{% endif %}<div class="container mt-4"> <div class="row"><div class="container my-4"> <div class="row g-3"> <div class="col-12 col-md-8"> <div class="row"> <div class="col-12"> <div class="bg-light mb-4 p-2 rounded"> <canvas id="chart-response-times" height="300" width="100%"></canvas> </div> <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" id="doughnut-graphs"> <div class="row g-3"> <div class="col-12 col-md-6"> <div class="bg-light mb-4 p-2 rounded"> <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="bg-light mb-4 p-2 rounded"> <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"> <ul class="list-group mb-4"> <li class="list-group-item bg-dark text-white"> <strong>Security</strong> </li> <li class="list-group-item d-flex justify-content-between bg-light border-white border-start-0 border-end-0"> <span>Use HTTPS</span> <span class="badge {% if property.is_https %}bg-success{% else %}bg-danger{% endif %}">{{ property.is_https }}</span> </li> <li class="list-group-item d-flex justify-content-between bg-light border-white border-start-0 border-end-0"> <span>Set MIME types</span> <span class="badge {% if property.has_mime_type %}bg-success{% else %}bg-danger{% endif %}">{{ property.has_mime_type }}</span> </li> <li class="list-group-item d-flex justify-content-between bg-light border-white border-start-0 border-end-0"> <span>Use content sniffing protection</span> <span class="badge {% if property.has_content_sniffing_protection %}bg-success{% else %}bg-danger{% endif %}">{{ property.has_content_sniffing_protection }}</span> </li> <li class="list-group-item d-flex justify-content-between bg-light border-white border-start-0 border-end-0"> <span>Use clickjack protection</span> <span class="badge {% if property.has_clickjack_protection %}bg-success{% else %}bg-danger{% endif %}">{{ property.has_clickjack_protection }}</span> </li> <li class="list-group-item d-flex justify-content-between bg-light border-white border-start-0 border-end-0"> <span>Use XSS protection</span> <span class="badge {% if property.has_xss_protection %}bg-success{% else %}bg-danger{% endif %}">{{ property.has_xss_protection }}</span> </li> <li class="list-group-item d-flex justify-content-between bg-light border-white border-start-0 border-end-0"> <span>Hide server version data</span> <span class="badge {% if property.hides_server_version %}bg-success{% else %}bg-danger{% endif %}">{{ property.hides_server_version }}</span> </li> <li class="list-group-item d-flex justify-content-between bg-light border-white border-start-0 border-end-0"> <span>Use HSTS</span> <span class="badge {% if property.has_hsts %}bg-success{% else %}bg-danger{% endif %}">{{ property.has_hsts }}</span> </li> <li class="list-group-item d-flex justify-content-between bg-light border-white border-start-0 border-end-0"> <span>Use HSTS preload</span> <span class="badge {% if property.has_hsts_preload %}bg-success{% else %}bg-danger{% endif %}">{{ property.has_hsts_preload }}</span> </li> </ul> <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">XSS protection</span> <span class="chip {% if property.has_xss_protection %}chip-ok{% else %}chip-down{% endif %}">{% if property.has_xss_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 property.lighthouse_details %}<div class="container mt-4"> <h3 class="mt-4">Performance breakdown</h3><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. The opportunities below are the biggest wins โ savings are estimated against the audited URL only. 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 mb-4"> <div class="row g-3 mb-4"> {% for metric in property.lighthouse_details.metrics %} <div class="col-6 col-md d-flex"> <div class="card w-100 mb-2"> <div class="card-body text-center py-2 border-top border-4 border-{{ metric.score|lh_score_class }}"> <div class="card-title h5 mb-0">{{ metric.display_value|default:"โ" }}</div> <div class="card-text text-truncate small" title="{{ metric.title }}"> {{ metric.acronym }} <span class="text-muted">ยท {{ metric.weight }}%</span> </div> </div> <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|default:"โ" }}</div> <div class="metric-sub" title="{{ metric.title }}">{{ metric.title }}</div> </div> </div> {% endfor %}
@@ -299,64 +295,64 @@ {% endif %} {% if property.lighthouse_details.opportunities %} <h4 class="mt-4">Top opportunities</h4> <div class="row bg-dark text-white py-2 mb-2 rounded rounded-sm fw-bolder"> <div class="col-2 col-md-1">Score</div> <div class="col-6 col-md-7">Audit</div> <div class="col-4 col-md-2">Detail</div> <div class="col-12 col-md-2 text-md-end">Est. savings</div> <div class="section-label mb-2">top opportunities</div> <div class="insight-header"> <span>Score</span> <span>Audit</span> <span>Detail</span> <span class="text-end">Est. savings</span> </div> {% for opp in property.lighthouse_details.opportunities %} <div class="row bg-light py-2 mb-2 rounded rounded-sm align-items-center"> <div class="col-2 col-md-1"> <span class="badge bg-{{ opp.score|lh_score_class }}">{{ opp.score|floatformat:"2" }}</span> </div> <div class="col-6 col-md-7">{{ opp.title }}</div> <div class="col-4 col-md-2 text-truncate small text-muted" {% if opp.display_value %}data-bs-toggle="tooltip" data-bs-title="{{ opp.display_value }}"{% endif %}> <div class="insight-row"> <span> <span class="chip {% if opp.score >= 0.9 %}chip-ok{% elif opp.score >= 0.5 %}chip-warn{% else %}chip-down{% endif %}"> {{ opp.score|floatformat:"2" }} </span> </span> <span class="insight-col-trunc" title="{{ opp.title }}">{{ opp.title }}</span> <span class="insight-col-trunc text-muted small" {% if opp.display_value %}data-bs-toggle="tooltip" data-bs-title="{{ opp.display_value }}"{% endif %}> {{ opp.display_value|default:"โ" }} </div> <div class="col-12 col-md-2 text-md-end small"> </span> <span class="text-end small"> {% with saved=opp.savings_ms|format_ms_savings %} {% if saved %}<strong>{{ saved }}</strong>{% else %}<span class="text-muted">โ</span>{% endif %} {% if saved %}<strong class="text-amber">{{ saved }}</strong>{% else %}<span class="text-muted">โ</span>{% endif %} {% endwith %} </div> </span> </div> {% endfor %} {% else %} <p class="text-success small">No actionable opportunities โ everything passing at this URL.</p> <div class="alert alert-info py-2 small">No actionable opportunities โ everything passing at this URL.</div> {% endif %}</div>{% endif %}{% if property.crawler_insights %}<div class="container mt-4"><div class="container my-4"> {% regroup property.crawler_insights|dictsort:"type" by type as insight_type_groups %} {% for group in insight_type_groups %} <h3 class="mt-4 text-capitalize">{{ group.grouper }} <small class="text-muted">({{ group.list|length }})</small></h3> <div class="row bg-dark text-white py-2 mb-2 rounded rounded-sm fw-bolder"> <div class="col-2">Severity</div> <div class="col-3">URL</div> <div class="col-4">Issue</div> <div class="col-3">Item</div> <div class="section-label mb-2">{{ group.grouper }} ยท <span class="text-muted">{{ group.list|length }}</span></div> <div class="insight-header"> <span>Severity</span> <span>URL</span> <span>Issue</span> <span>Item</span> </div> {% for insight in group.list|dictsort:"severity" %} <div class="row bg-light py-2 mb-2 rounded rounded-sm"> <div class="col-md-2 d-flex align-items-center"> <span class="badge {% if insight.severity == 'error' %}bg-danger{% elif insight.severity == 'warning' %}bg-warning text-dark{% else %}bg-info text-dark{% endif %}"> <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> </div> <div class="col-md-3 text-truncate"> <a href="{{ insight.url }}" target="_blank"> {{ insight.url|url_path }} </a> </div> <div class="col-md-4 text-truncate" {% if insight.issue %}data-bs-toggle="tooltip" data-bs-title="{{ insight.issue }}"{% endif %}> </span> <span class="insight-col-trunc"> <a href="{{ insight.url }}" target="_blank" class="text-muted">{{ insight.url|url_path }}</a> </span> <span class="insight-col-trunc" {% if insight.issue %}data-bs-toggle="tooltip" data-bs-title="{{ insight.issue }}"{% endif %}> {{ insight.issue }} </div> <div class="col-md-3 text-truncate" {% if insight.item %}data-bs-toggle="tooltip" data-bs-title="{{ insight.item }}"{% endif %}> </span> <span class="insight-col-trunc text-muted small" {% if insight.item %}data-bs-toggle="tooltip" data-bs-title="{{ insight.item }}"{% endif %}> {{ insight.item }} </div> </span> </div> {% endfor %} {% endfor %}
added
properties/templates/properties/property_report.html
@@ -0,0 +1,341 @@{% load static properties_tags %}<!doctype html><html lang="en"><head> <meta charset="utf-8"> <title>{{ property.name }} ยท Status Report</title> <style> @page { size: letter; margin: 0.6in; } :root { --ink: #1a1d1b; --ink-soft: #4a4a44; --ink-muted: #7a7a74; --paper: #ffffff; --paper-2: #f6f4ef; --rule: #d8d4c8; --accent: #4e7a5a; /* deeper moss on white */ --accent-soft: #e8f0ea; --amber: #9a7f2e; --amber-soft: #f6efd7; --danger: #9f3f2b; --danger-soft: #f5e3dd; } * { box-sizing: border-box; } html, body { background: var(--paper); color: var(--ink); font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10pt; line-height: 1.5; margin: 0; padding: 0; } h1, h2, h3, h4 { margin: 0 0 0.4em 0; font-weight: 700; color: var(--ink); letter-spacing: -0.01em; } h1 { font-size: 22pt; line-height: 1.15; } h2 { font-size: 12pt; margin-top: 1.4em; letter-spacing: 0.06em; text-transform: uppercase; color: var(--accent); border-bottom: 1px solid var(--rule); padding-bottom: 0.25em; } h3 { font-size: 10.5pt; margin-top: 1em; } p { margin: 0 0 0.6em 0; } a { color: var(--accent); text-decoration: none; } code, .mono { font-family: ui-monospace, Menlo, Consolas, 'Courier New', monospace; font-size: 9pt; } .doc-header { display: flex; justify-content: space-between; align-items: flex-end; gap: 1em; padding-bottom: 0.8em; border-bottom: 2px solid var(--ink); margin-bottom: 1em; } .doc-header .brand { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 9pt; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-muted); } .doc-header .brand strong { color: var(--ink); letter-spacing: 0.08em; } .meta-url { color: var(--ink-soft); font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 9.5pt; } .meta-line { color: var(--ink-muted); font-size: 9pt; font-family: ui-monospace, Menlo, Consolas, monospace; } /* Summary tiles */ .tile-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.5em; margin: 1em 0; } .tile { border: 1px solid var(--rule); border-left: 3px solid var(--accent); padding: 0.55em 0.7em; background: var(--paper); } .tile.bad { border-left-color: var(--danger); } .tile.warn { border-left-color: var(--amber); } .tile-label { font-size: 7.5pt; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-muted); font-family: ui-monospace, Menlo, Consolas, monospace; } .tile-value { font-size: 16pt; font-weight: 700; color: var(--ink); line-height: 1.1; margin-top: 0.15em; } .tile-sub { font-size: 8pt; color: var(--ink-muted); margin-top: 0.1em; } /* Key/value block */ table.kv { width: 100%; border-collapse: collapse; margin: 0.5em 0; } table.kv td { padding: 0.3em 0.5em; border-bottom: 1px solid var(--rule); vertical-align: top; } table.kv td.k { width: 35%; color: var(--ink-muted); font-size: 9pt; letter-spacing: 0.04em; } /* Data tables */ table.data { width: 100%; border-collapse: collapse; margin: 0.4em 0 1em; font-size: 9pt; } table.data th, table.data td { text-align: left; padding: 0.35em 0.55em; border-bottom: 1px solid var(--rule); vertical-align: top; } table.data th { background: var(--paper-2); font-size: 8pt; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-muted); font-weight: 600; border-bottom: 1.5px solid var(--ink-muted); } table.data tbody tr:nth-child(even) td { background: var(--paper-2); } /* Pass/fail marker used in security + checks */ .mark { display: inline-block; padding: 0.1em 0.5em; font-size: 8pt; font-family: ui-monospace, Menlo, Consolas, monospace; letter-spacing: 0.06em; text-transform: uppercase; border-radius: 2px; border: 1px solid transparent; } .mark.ok { background: var(--accent-soft); color: var(--accent); border-color: #b9d3c2; } .mark.warn { background: var(--amber-soft); color: var(--amber); border-color: #e2d39b; } .mark.fail { background: var(--danger-soft); color: var(--danger); border-color: #dfb2a7; } .mark.muted { background: #f0ede5; color: var(--ink-muted); border-color: var(--rule); } /* Security two-column grid */ .sec-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1.2em; margin: 0.3em 0 1em; } .sec-item { display: flex; justify-content: space-between; padding: 0.3em 0; border-bottom: 1px solid var(--rule); font-size: 9pt; } .footer-note { margin-top: 1.5em; padding-top: 0.6em; border-top: 1px solid var(--rule); font-size: 8.5pt; color: var(--ink-muted); } .insight-severity { text-transform: uppercase; font-size: 7.5pt; letter-spacing: 0.08em; } .col-narrow { width: 90px; } .col-short { width: 70px; text-align: right; } </style></head><body> <div class="doc-header"> <div> <div class="brand"><strong>Status</strong> ยท property report</div> <h1>{{ property.name }}</h1> <a class="meta-url" href="{{ property.url }}">{{ property.url }}</a> </div> <div class="meta-line" style="text-align: right;"> generated ยท {% now "Y-m-d H:i" %}<br> operator ยท {{ property.user.username }} </div> </div> <h2>Live signals</h2> <div class="tile-grid"> <div class="tile {% if property.current_status != 200 %}bad{% endif %}"> <div class="tile-label">HTTP Status</div> <div class="tile-value">{{ property.current_status }}</div> <div class="tile-sub">{% if property.current_status == 200 %}OK ยท all clear{% else %}failing{% endif %}</div> </div> <div class="tile {% if property.invalid_cert %}bad{% endif %}"> <div class="tile-label">TLS cert</div> <div class="tile-value">{% if property.invalid_cert %}Invalid{% else %}Valid{% endif %}</div> <div class="tile-sub">chain + trust</div> </div> <div class="tile {% if property.has_security_issue %}warn{% endif %}"> <div class="tile-label">Security</div> <div class="tile-value">{% if property.has_security_issue %}Issues{% else %}Pass{% endif %}</div> <div class="tile-sub">header scan</div> </div> <div class="tile {% if property.avg_response_time > 500 %}warn{% endif %}"> <div class="tile-label">Response</div> <div class="tile-value">{{ property.avg_response_time }} <span style="font-size: 9pt; color: var(--ink-muted); font-weight: 500;">ms</span></div> <div class="tile-sub">rolling avg ยท 31 checks</div> </div> </div> <table class="kv"> <tr><td class="k">Alert state</td><td>{{ property.alert_state|title }}{% if property.last_alert_sent %} <span class="meta-line">ยท last alert {{ property.last_alert_sent|date:"Y-m-d H:i" }}</span>{% endif %}</td></tr> <tr><td class="k">Visibility</td><td>{% if property.is_public %}Public{% else %}Private{% endif %}</td></tr> <tr><td class="k">Registered</td><td>{{ property.created_at|date:"Y-m-d" }}</td></tr> <tr><td class="k">Total checks</td><td>{{ property.total_checks }}</td></tr> {% if property.recent_uptime_pct is not None %} <tr><td class="k">Uptime (last 100)</td><td>{{ property.recent_uptime_pct }}%</td></tr> {% endif %} </table> {% if property.lighthouse_scores %} <h2>Lighthouse</h2> <div class="tile-grid"> {% for category, score in property.lighthouse_scores.items %} <div class="tile {% if score < 50 %}bad{% elif score < 90 %}warn{% endif %}"> <div class="tile-label">{{ category }}</div> <div class="tile-value">{{ score }}<span style="font-size: 10pt; color: var(--ink-muted);">%</span></div> </div> {% endfor %} </div> {% endif %} <h2>Security ยท header scan</h2> <div class="sec-grid"> <div class="sec-item"><span>Use HTTPS</span><span class="mark {% if property.is_https %}ok{% else %}fail{% endif %}">{% if property.is_https %}pass{% else %}fail{% endif %}</span></div> <div class="sec-item"><span>Set MIME types</span><span class="mark {% if property.has_mime_type %}ok{% else %}fail{% endif %}">{% if property.has_mime_type %}pass{% else %}fail{% endif %}</span></div> <div class="sec-item"><span>Content sniff protection</span><span class="mark {% if property.has_content_sniffing_protection %}ok{% else %}fail{% endif %}">{% if property.has_content_sniffing_protection %}pass{% else %}fail{% endif %}</span></div> <div class="sec-item"><span>Clickjack protection</span><span class="mark {% if property.has_clickjack_protection %}ok{% else %}fail{% endif %}">{% if property.has_clickjack_protection %}pass{% else %}fail{% endif %}</span></div> <div class="sec-item"><span>XSS protection</span><span class="mark {% if property.has_xss_protection %}ok{% else %}fail{% endif %}">{% if property.has_xss_protection %}pass{% else %}fail{% endif %}</span></div> <div class="sec-item"><span>Hide server version</span><span class="mark {% if property.hides_server_version %}ok{% else %}fail{% endif %}">{% if property.hides_server_version %}pass{% else %}fail{% endif %}</span></div> <div class="sec-item"><span>HSTS enabled</span><span class="mark {% if property.has_hsts %}ok{% else %}fail{% endif %}">{% if property.has_hsts %}pass{% else %}fail{% endif %}</span></div> <div class="sec-item"><span>HSTS preload</span><span class="mark {% if property.has_hsts_preload %}ok{% else %}fail{% endif %}">{% if property.has_hsts_preload %}pass{% else %}fail{% endif %}</span></div> </div> {% if property.lighthouse_details.metrics %} <h2>Performance metrics</h2> <table class="data"> <thead> <tr><th>Acronym</th><th>Metric</th><th class="col-narrow">Value</th><th class="col-short">Weight</th><th class="col-short">Score</th></tr> </thead> <tbody> {% for metric in property.lighthouse_details.metrics %} <tr> <td><strong>{{ metric.acronym }}</strong></td> <td>{{ metric.title }}</td> <td class="mono">{{ metric.display_value|default:"โ" }}</td> <td class="col-short">{{ metric.weight }}%</td> <td class="col-short"><span class="mark {% if metric.score >= 0.9 %}ok{% elif metric.score >= 0.5 %}warn{% else %}fail{% endif %}">{{ metric.score|floatformat:"2" }}</span></td> </tr> {% endfor %} </tbody> </table> {% endif %} {% if property.lighthouse_details.opportunities %} <h2>Top opportunities</h2> <table class="data"> <thead> <tr><th class="col-short">Score</th><th>Audit</th><th>Detail</th><th class="col-narrow">Est. savings</th></tr> </thead> <tbody> {% for opp in property.lighthouse_details.opportunities %} <tr> <td class="col-short"><span class="mark {% if opp.score >= 0.9 %}ok{% elif opp.score >= 0.5 %}warn{% else %}fail{% endif %}">{{ opp.score|floatformat:"2" }}</span></td> <td>{{ opp.title }}</td> <td class="mono">{{ opp.display_value|default:"โ" }}</td> <td class="col-narrow">{% with saved=opp.savings_ms|format_ms_savings %}{% if saved %}<strong>{{ saved }}</strong>{% else %}โ{% endif %}{% endwith %}</td> </tr> {% endfor %} </tbody> </table> {% endif %} {% if property.crawler_insights %} <h2>Crawler insights</h2> {% regroup property.crawler_insights|dictsort:"type" by type as insight_type_groups %} {% for group in insight_type_groups %} <h3>{{ group.grouper|capfirst }} <span class="meta-line">({{ group.list|length }})</span></h3> <table class="data"> <thead> <tr><th class="col-narrow">Severity</th><th>URL</th><th>Issue</th><th>Item</th></tr> </thead> <tbody> {% for insight in group.list|dictsort:"severity" %} <tr> <td><span class="mark {% if insight.severity == 'error' %}fail{% elif insight.severity == 'warning' %}warn{% else %}muted{% endif %} insight-severity">{{ insight.severity }}</span></td> <td class="mono">{{ insight.url|url_path }}</td> <td>{{ insight.issue }}</td> <td class="mono">{{ insight.item|default:"โ" }}</td> </tr> {% endfor %} </tbody> </table> {% endfor %} {% endif %} <div class="footer-note"> Status ยท self-hosted website monitoring. Report generated from live data at the moment above. Metrics refresh every 3 minutes (HTTP), daily (Lighthouse), weekly (crawler). </div></body></html>
added
properties/templates/properties/property_report.md
@@ -0,0 +1,77 @@{% load properties_tags %}# {{ property.name }}**URL:** {{ property.url }}**Generated:** {% now "Y-m-d H:i" %}**Operator:** {{ property.user.username }}---## Live signals| Signal | Value | Status ||---|---|---|| HTTP status | `{{ property.current_status }}` | {% if property.current_status == 200 %}OK{% else %}failing{% endif %} || TLS cert | {% if property.invalid_cert %}invalid{% else %}valid{% endif %} | {% if property.invalid_cert %}fail{% else %}pass{% endif %} || Security headers | {% if property.has_security_issue %}issues{% else %}clean{% endif %} | {% if property.has_security_issue %}warn{% else %}pass{% endif %} || Avg response | `{{ property.avg_response_time }} ms` | {% if property.avg_response_time > 500 %}slow{% else %}ok{% endif %} || Alert state | `{{ property.alert_state }}` | {% if property.last_alert_sent %}last alert {{ property.last_alert_sent|date:"Y-m-d H:i" }}{% endif %} || Visibility | {% if property.is_public %}public{% else %}private{% endif %} | || Registered | {{ property.created_at|date:"Y-m-d" }} | || Total checks | {{ property.total_checks }} | |{% if property.recent_uptime_pct is not None %}| Uptime (last 100) | `{{ property.recent_uptime_pct }}%` | |{% endif %}{% if property.lighthouse_scores %}## Lighthouse scores| Category | Score ||---|---|{% for category, score in property.lighthouse_scores.items %}| {{ category|capfirst }} | `{{ score }}%` |{% endfor %}{% endif %}## Security ยท header scan| Check | Result ||---|---|| Use HTTPS | {% if property.is_https %}pass{% else %}fail{% endif %} || Set MIME types | {% if property.has_mime_type %}pass{% else %}fail{% endif %} || Content sniff protection | {% if property.has_content_sniffing_protection %}pass{% else %}fail{% endif %} || Clickjack protection | {% if property.has_clickjack_protection %}pass{% else %}fail{% endif %} || XSS protection | {% if property.has_xss_protection %}pass{% else %}fail{% endif %} || Hide server version | {% if property.hides_server_version %}pass{% else %}fail{% endif %} || HSTS enabled | {% if property.has_hsts %}pass{% else %}fail{% endif %} || HSTS preload | {% if property.has_hsts_preload %}pass{% else %}fail{% endif %} |{% if property.lighthouse_details.metrics %}## Performance metrics| Acronym | Metric | Value | Weight | Score ||---|---|---|---|---|{% for metric in property.lighthouse_details.metrics %}| **{{ metric.acronym }}** | {{ metric.title }} | `{{ metric.display_value|default:"โ" }}` | {{ metric.weight }}% | {{ metric.score|floatformat:"2" }} |{% endfor %}{% endif %}{% if property.lighthouse_details.opportunities %}## Top opportunities| Score | Audit | Detail | Est. savings ||---|---|---|---|{% for opp in property.lighthouse_details.opportunities %}| {{ opp.score|floatformat:"2" }} | {{ opp.title }} | `{{ opp.display_value|default:"โ" }}` | {% with saved=opp.savings_ms|format_ms_savings %}{% if saved %}**{{ saved }}**{% else %}โ{% endif %}{% endwith %} |{% endfor %}{% endif %}{% if property.crawler_insights %}## Crawler insights{% regroup property.crawler_insights|dictsort:"type" by type as insight_type_groups %}{% for group in insight_type_groups %}### {{ group.grouper|capfirst }} ({{ group.list|length }})| Severity | URL | Issue | Item ||---|---|---|---|{% for insight in group.list|dictsort:"severity" %}| {{ insight.severity }} | `{{ insight.url|url_path }}` | {{ insight.issue }} | `{{ insight.item|default:"โ" }}` |{% endfor %}{% endfor %}{% endif %}---_Report generated by Status โ self-hosted website monitoring. HTTP checks every 3 minutes, Lighthouse daily, crawler weekly._
modified
properties/urls.py
@@ -10,6 +10,5 @@ urlpatterns = [ path('<uuid:property_id>/status/', views.property_status, name='property_status'), path('<uuid:property_id>/recrawl/', views.property_recrawl, name='property_recrawl'), path('<uuid:property_id>/rerun-lighthouse/', views.property_rerun_lighthouse, name='property_rerun_lighthouse'), path('import/', views.import_properties, name='import_properties'), path('', views.properties, name='properties'),]
modified
properties/views.py
@@ -1,9 +1,5 @@import csvimport ioimport threadingimport uuidimport requestsfrom django.conf import settingsfrom django.contrib import messagesfrom django.core.files.storage import default_storage
@@ -139,15 +135,24 @@ def property(request, property_id): {"label": "Downtime", "count": downtime_pct}, ] if request.GET.get("report") == "": context["print"] = True html = render_to_string("properties/property.html", context) filename = f"reports/{uuid.uuid4()}.pdf" generate_pdf_from_html(html, filename) with open(default_storage.path(filename), "rb") as pdf: response = HttpResponse(pdf.read(), content_type="application/pdf") response["Content-Disposition"] = "inline; filename=report.pdf" # Report formats. `?report` (or `?report=pdf`) returns a printed PDF # rendered from the light-themed report template; `?report=md` returns a # plain-text Markdown report suited for piping into an LLM. if "report" in request.GET: fmt = request.GET.get("report") or "pdf" if fmt == "md": md = render_to_string("properties/property_report.md", context) response = HttpResponse(md, content_type="text/markdown; charset=utf-8") response["Content-Disposition"] = f'inline; filename="{property_obj.name}.md"' return response if fmt == "pdf": html = render_to_string("properties/property_report.html", context) filename = f"reports/{uuid.uuid4()}.pdf" generate_pdf_from_html(html, filename) with open(default_storage.path(filename), "rb") as pdf: response = HttpResponse(pdf.read(), content_type="application/pdf") response["Content-Disposition"] = "inline; filename=report.pdf" return response return render(request, "properties/property.html", context)
@@ -285,37 +290,3 @@ def property_rerun_lighthouse(request, property_id): return JsonResponse({"ok": True, **_serialize_status(property_obj)})def import_property(request, url): url = url.lower().strip() if not url.startswith("http"): url = "http://" + url try: r = requests.get(url) r.raise_for_status() except requests.exceptions.RequestException: return if not Property.objects.filter(url=r.url).exists(): property_obj = Property( url=r.url, user=request.user, ) property_obj.save()def import_properties(request): if not request.user.is_authenticated: return redirect("/") if request.method == "POST": file = request.FILES["csv_file"] reader = csv.reader(io.TextIOWrapper(file.file, encoding="utf-8")) for row in reader: try: url = row[0] threading.Thread(target=import_property, args=(request, url)).start() except IndexError: continue messages.success(request, "Properties imported successfully.") return redirect("properties") return redirect("properties")
modified
status/chromium.py
@@ -10,12 +10,13 @@ import uuidfrom django.core.files.storage import default_storage# Get chromium path, it's sometimes chromium and sometimes chromium-browser# Chromium/Chrome binary discovery โ distro names vary (Alpine: chromium,# Debian/Ubuntu: chromium-browser, the webdev container ships google-chrome).chromium = Noneif shutil.which("chromium"): chromium = "chromium"elif shutil.which("chromium-browser"): chromium = "chromium-browser"for binary in ("chromium", "chromium-browser", "google-chrome"): if shutil.which(binary): chromium = binary breakbase_command = [
@@ -30,7 +31,7 @@ base_command = [ "--disable-extensions", "--disable-in-process-stack-traces", "--disable-logging", "--window-size=1280x720", "--window-size=1280,720", "--hide-scrollbars",] if chromium else []
modified
status/static_src/index.js
@@ -1,3 +1,6 @@// fontsimport "@fontsource/monaspace-argon";// scriptsimport "./scripts/bootstrap.js";
added
status/static_src/styles/_variables.scss
@@ -0,0 +1,129 @@// Status โ warm-earth dark theme, aligned to blog.bythewood.me// Mossy forest green + warm amber on a deep earth background. Quieter// than true cyberpunk โ feels like a UNIX operator's console.// โโ Typography โโ$font-family-sans-serif: 'Monaspace Argon', ui-monospace, 'Cascadia Code', Consolas, 'SF Mono', Menlo, monospace;$font-family-monospace: 'Monaspace Argon', ui-monospace, 'Cascadia Code', Consolas, 'SF Mono', Menlo, monospace;$headings-font-family: null;$headings-font-weight: 700;$font-size-base: 0.92rem;$line-height-base: 1.7;$headings-line-height: 1.3;// โโ Core palette โ warm earth darks โโ$white: #ede8e0;$gray-100: #ddd7cd;$gray-200: #c4bdb2;$gray-300: #a09890;$gray-400: #847c72;$gray-500: #665f56;$gray-600: #4a443c;$gray-700: #332e28;$gray-800: #211e1a;$gray-900: #161310;$black: #0b0a08;$body-bg: #0e0d0a;$body-color: #ddd7cd;// โโ Accents โ muted forest green + warm amber โโ$green: #6b9e78;$green-bright: #7db88c;$green-dim: rgba(107, 158, 120, 0.12);$green-border: rgba(107, 158, 120, 0.22);$amber: #c9a84c;$amber-dim: rgba(201, 168, 76, 0.1);$amber-border: rgba(201, 168, 76, 0.22);$terracotta: #c47055;$terracotta-dim: rgba(196, 112, 85, 0.1);$terracotta-border: rgba(196, 112, 85, 0.22);// โโ Theme mapping โโ$primary: $green;$secondary: #4a443c;$success: $green;$info: #7eaab8;$warning: $amber;$danger: $terracotta;$light: #211e1a;$dark: #0e0d0a;// โโ Links โโ$link-color: $green-bright;$link-hover-color: #95cca2;$link-decoration: none;$link-hover-decoration: underline;// โโ Borders โโ$border-color: rgba(221, 215, 205, 0.06);$border-radius: 0.25rem;$border-radius-sm: 0.2rem;$border-radius-lg: 0.375rem;// โโ Cards โโ$card-bg: #13120e;$card-border-color: rgba(107, 158, 120, 0.1);$card-color: $body-color;$card-cap-bg: rgba(221, 215, 205, 0.02);// โโ Inputs โโ$input-bg: #13120e;$input-color: $body-color;$input-border-color: rgba(107, 158, 120, 0.18);$input-focus-bg: #18160f;$input-focus-color: $body-color;$input-focus-border-color: $green;$input-focus-box-shadow: 0 0 0 2px rgba(107, 158, 120, 0.15);$input-placeholder-color: #665f56;// โโ Navbar โโ$navbar-dark-color: rgba(221, 215, 205, 0.6);$navbar-dark-hover-color: $white;$navbar-dark-active-color: $white;// โโ List group โโ$list-group-bg: #13120e;$list-group-border-color: rgba(107, 158, 120, 0.08);$list-group-hover-bg: #1a1812;$list-group-action-color: #a09890;$list-group-action-hover-color: $white;$list-group-action-active-bg: #211e1a;$list-group-action-active-color: $white;$list-group-active-bg: $green-dim;$list-group-active-border-color: $green-border;$list-group-active-color: $green-bright;// โโ Breadcrumbs โโ$breadcrumb-active-color: $white;$breadcrumb-divider-color: #665f56;// โโ Dropdowns โโ$dropdown-bg: #13120e;$dropdown-border-color: rgba(107, 158, 120, 0.12);$dropdown-color: $body-color;$dropdown-link-color: #a09890;$dropdown-link-hover-color: $white;$dropdown-link-hover-bg: #1a1812;// โโ Close button โโ$btn-close-color: $body-color;// โโ Modal โโ$modal-content-bg: #13120e;$modal-content-border-color: rgba(107, 158, 120, 0.15);$modal-header-border-color: rgba(107, 158, 120, 0.08);$modal-footer-border-color: rgba(107, 158, 120, 0.08);$modal-backdrop-bg: #000;$modal-backdrop-opacity: 0.75;// โโ Progress โโ$progress-bg: rgba(221, 215, 205, 0.05);$progress-bar-bg: $green;// โโ Tables โโ$table-bg: transparent;$table-color: $body-color;$table-border-color: rgba(107, 158, 120, 0.08);
modified
status/static_src/styles/base.scss
@@ -1,44 +1,812 @@@use "variables" as *;// โโ CSS variable overrides โโ// Bootstrap 5.3 writes defaults into `:root,[data-bs-theme=light]`. Use// `html:root` so we outrank the default block and enforce the dark palette.html:root { --bs-body-bg: #{$body-bg}; --bs-body-color: #{$body-color}; --bs-body-bg-rgb: 14, 13, 10; --bs-body-color-rgb: 221, 215, 205; --bs-emphasis-color: #{$white}; --bs-secondary-bg: #13120e; --bs-tertiary-bg: #18160f; --bs-border-color: rgba(107, 158, 120, 0.12); --bs-link-color-rgb: 125, 184, 140; --bs-link-hover-color-rgb: 149, 204, 162; --bs-heading-color: #{$white};}// โโ Global โโbody { min-height: 100vh; min-height: 100vh; display: flex; flex-direction: column; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; letter-spacing: 0.01em; background-color: $body-bg !important; color: $body-color;}main { flex: 1; }.vh-100 { height: 100vh; }// โโ Logo (uptime-bars mark) โโ// Rendered as an inline SVG in the template so it scales cleanly. Matches the// favicon exactly..logo { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; text-decoration: none !important; transition: transform 250ms ease; svg { width: 100%; height: 100%; display: block; } &:hover { transform: translateY(-1px); } &.logo-sm { width: 18px; height: 18px; }}// โโ Navbar โโ.navbar { height: 52px; padding: 0; background: #090806 !important; border-bottom: 1px solid rgba(107, 158, 120, 0.06); .navbar-brand { display: flex; flex-direction: column; align-items: center; gap: 0.6rem; color: $white; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; font-size: 0.78rem; text-decoration: none; .brand-text { color: $white; } .brand-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: $green; } } .nav-link { font-size: 0.8rem; font-weight: 500; letter-spacing: 0.05em; text-transform: lowercase; padding-left: 1rem !important; padding-right: 1rem !important; color: rgba(221, 215, 205, 0.6); position: relative; transition: color 200ms ease; &::after { content: ''; position: absolute; bottom: 2px; left: 1rem; right: 1rem; height: 1px; background: $green; transform: scaleX(0); transition: transform 250ms ease; } &:hover, &.active { color: $white; } &:hover::after, &.active::after { transform: scaleX(1); } }}main { flex: 1;// โโ Breadcrumb bar โโ.breadcrumb-bar { background: rgba(107, 158, 120, 0.02); border-bottom: 1px solid rgba(107, 158, 120, 0.04); .breadcrumb { font-family: $font-family-monospace; }}.vh-100 { height: 100vh;// โโ Section labels โ amber caps, no prompt glyph โโ.section-label { font-size: 0.7rem; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: rgba(201, 168, 76, 0.75); font-family: $font-family-monospace;}.login-hero { background: var(--background-image); background-size: cover; background-position: center; background-repeat: no-repeat;// โโ Cards (base) โโ.card { background: $card-bg; border: 1px solid $card-border-color; transition: border-color 250ms ease; &:hover { border-color: rgba(107, 158, 120, 0.2); }}.logo { content: ""; display: block; background-image: linear-gradient(to right, rgb(14, 63, 244) 0px, rgb(132, 43, 255) 100%); width: 36px; height: 36px; margin-top: auto; margin-bottom: auto; transition: transform .2s ease-in-out;// โโ Status dot โโ.status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 0.55rem; vertical-align: middle; flex-shrink: 0; &.status-dot-lg { width: 14px; height: 14px; margin-right: 0; } &.is-up { background: $green; box-shadow: 0 0 0 3px rgba(107, 158, 120, 0.12); } &.is-down { background: $terracotta; box-shadow: 0 0 0 3px rgba(196, 112, 85, 0.12); } &.is-warn { background: $amber; box-shadow: 0 0 0 3px rgba(201, 168, 76, 0.12); } &.is-idle { background: #4a443c; }}// โโ Visibility toggle (segmented pill) โโ// Replaces the Bootstrap form-switch which was near-invisible on the dark// theme. Clear two-state UI โ active half highlighted, inactive half dim..toggle-label { font-family: $font-family-monospace; font-size: 0.68rem; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: #847c72; cursor: help;}.toggle-pill { display: inline-flex; align-items: stretch; border: 1px solid rgba(107, 158, 120, 0.22); border-radius: 999px; overflow: hidden; cursor: pointer; background: #0e0d0a; margin: 0; user-select: none; line-height: 1; input { position: absolute; opacity: 0; pointer-events: none; width: 0; height: 0; } .toggle-pill-seg { padding: 0.45rem 0.9rem; font-family: $font-family-monospace; font-size: 0.7rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: #665f56; transition: background 180ms ease, color 180ms ease; } // Default (unchecked) = Private is active input ~ .toggle-pill-private { background: rgba(201, 168, 76, 0.12); color: #ddc06a; } // Checked = Public is active input:checked ~ .toggle-pill-private { background: transparent; color: #665f56; } input:checked ~ .toggle-pill-public { background: $green-dim; color: $green-bright; } &:hover { transform: rotate(15deg) scale(1.1); border-color: rgba(107, 158, 120, 0.35); }}.navbar { height: 50px; padding: 0;// โโ Metric tile โโ// Every tile fills its grid cell so a row of them all share one height, even// when content length varies (e.g. one tile has a sub-line, others don't)..metric-tile { background: $card-bg; border: 1px solid $card-border-color; border-radius: $border-radius; padding: 1rem 1.15rem; height: 100%; display: flex; flex-direction: column; .metric-label { font-size: 0.68rem; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: #847c72; } .metric-value { font-size: 1.75rem; font-weight: 700; line-height: 1.1; color: $white; margin-top: 0.35rem; letter-spacing: -0.01em; } .metric-sub { font-size: 0.72rem; color: #847c72; margin-top: auto; // Pins sub to the bottom so all tiles baseline-align padding-top: 0.35rem; letter-spacing: 0.02em; } &.metric-accent-green { border-color: $green-border; .metric-value { color: $green-bright; } } &.metric-accent-amber { border-color: $amber-border; .metric-value { color: #ddc06a; } } &.metric-accent-danger { border-color: $terracotta-border; .metric-value { color: #d88870; } }}// โโ Chips (inline status/score) โโ.chip { display: inline-flex; align-items: center; gap: 0.4rem; font-family: $font-family-monospace; font-size: 0.7rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; padding: 0.3rem 0.6rem; border-radius: 2px; border: 1px solid transparent; line-height: 1; &.chip-ok { background: $green-dim; color: $green-bright; border-color: $green-border; } &.chip-down { background: $terracotta-dim; color: #e38871; border-color: $terracotta-border; } &.chip-warn { background: $amber-dim; color: #ddc06a; border-color: $amber-border; } &.chip-info { background: rgba(126, 170, 184, 0.08); color: #9ec5d2; border-color: rgba(126, 170, 184, 0.22); } &.chip-muted { background: rgba(221, 215, 205, 0.04); color: #847c72; border-color: rgba(221, 215, 205, 0.08); }}// โโ Footer โโfooter { padding: 6rem 0; padding-top: 4rem; padding-bottom: 3rem; position: relative; background: #090806; border-top: 1px solid rgba(107, 158, 120, 0.06); .h5 { font-size: 0.7rem; letter-spacing: 0.12em; text-transform: uppercase; color: rgba(201, 168, 76, 0.55); font-weight: 600; } p { color: rgba(221, 215, 205, 0.5); font-size: 0.85rem; line-height: 1.75; a { color: rgba(221, 215, 205, 0.75); text-decoration: underline; text-underline-offset: 2px; &:hover { color: $white; } } } .links { padding-top: 2rem; padding-bottom: 1rem; }}.footer-bar { background: #050403; border-top: 1px solid rgba(107, 158, 120, 0.04); padding: 0.8rem 0; color: rgba(221, 215, 205, 0.3); font-size: 0.78rem; small { color: rgba(221, 215, 205, 0.3); } &-link { color: rgba(221, 215, 205, 0.3); transition: color 200ms ease; &:hover { color: $white; } }}// โโ Uptime strip โโ.uptime-strip { display: flex; gap: 2px; align-items: flex-end; height: 20px; .uptime-tick { flex: 1 1 auto; min-width: 2px; height: 100%; background: #211e1a; border-radius: 1px; &.is-up { background: $green; opacity: 0.7; } &.is-down { background: $terracotta; opacity: 0.85; } &.is-warn { background: $amber; opacity: 0.7; } &.is-none { background: #18160f; } }}// โโ Terminal-style block โโ.terminal-block { background: rgba(9, 8, 6, 0.7); border: 1px solid rgba(107, 158, 120, 0.15); border-radius: $border-radius; font-family: $font-family-monospace; font-size: 0.8rem; color: $green-bright; padding: 1rem 1.2rem; line-height: 1.8; .t-line { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .t-prompt { color: $amber; margin-right: 0.4rem; } .t-out { color: $gray-200; } .t-comment { color: #665f56; } .t-key { color: $amber; } .t-val { color: $green-bright; } .t-cursor { display: inline-block; width: 7px; height: 0.9em; vertical-align: -2px; background: $green-bright; animation: t-blink 1s steps(1) infinite; }}@keyframes t-blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; }}// โโ Property row โโ.property-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 1rem 1.5rem; background: $card-bg; border: 1px solid $card-border-color; border-radius: $border-radius; padding: 1rem 1.15rem; margin-bottom: 0.75rem; transition: border-color 200ms ease; &:hover { border-color: rgba(107, 158, 120, 0.22); } &.is-down { border-color: rgba(196, 112, 85, 0.3); } .pr-main { min-width: 0; display: flex; flex-direction: column; gap: 0.35rem; } .pr-title { font-weight: 600; color: $white; display: flex; align-items: center; min-width: 0; a { color: inherit; text-decoration: none; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; &:hover { color: $green-bright; } } } .pr-url { font-size: 0.73rem; color: #847c72; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .pr-side { display: flex; flex-direction: column; justify-content: space-between; align-items: flex-end; gap: 0.75rem; min-height: 68px; // Matches the left column's natural height so actions pin to bottom } // Uniform stat grid โ each cell is label-over-chip so every column aligns // vertically regardless of whether the value is a pill or a number. .pr-meta { display: flex; gap: 0.85rem; align-items: flex-end; font-family: $font-family-monospace; .pr-meta-cell { display: flex; flex-direction: column; align-items: flex-start; gap: 0.2rem; min-width: 0; } .pr-meta-k { color: #665f56; text-transform: uppercase; font-size: 0.6rem; letter-spacing: 0.1em; line-height: 1; } } .pr-actions { display: flex; gap: 0.4rem; }}@media (max-width: 767px) { .property-row { grid-template-columns: 1fr; .pr-side { min-height: 0; align-items: stretch; } .pr-meta { flex-wrap: wrap; justify-content: flex-start; } .pr-actions { justify-content: flex-end; } }}// โโ Search form โโ.search-form { position: relative; .search-icon { position: absolute; left: 0.9rem; top: 50%; transform: translateY(-50%); color: #665f56; pointer-events: none; z-index: 2; } .search-input { padding-left: 2.5rem; font-size: 0.85rem; border: 1px solid rgba(107, 158, 120, 0.15); background: rgba(19, 18, 14, 0.7); color: $body-color; transition: border-color 200ms ease; &::placeholder { color: #665f56; font-style: normal; } &:focus { border-color: rgba(107, 158, 120, 0.4); box-shadow: 0 0 0 2px rgba(107, 158, 120, 0.1); background: #13120e; } }}// โโ Dashboard toolbar โโ// Input + button sharing one visual line โ both get the same height (38px)// so the CTA doesn't read as a different size from the field next to it..dashboard-toolbar { display: flex; gap: 0.5rem; align-items: stretch; .form-control { height: 38px; font-size: 0.85rem; } .btn { height: 38px; padding: 0 1.1rem; display: inline-flex; align-items: center; white-space: nowrap; }}// โโ Chart panel โโ.chart-panel { background: $card-bg; border: 1px solid $card-border-color; border-radius: $border-radius; padding: 1rem 1.15rem; .chart-panel-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.75rem; } .chart-panel-title { font-size: 0.68rem; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: rgba(201, 168, 76, 0.75); font-family: $font-family-monospace; }}// โโ Security list โโ.check-list { background: $card-bg; border: 1px solid $card-border-color; border-radius: $border-radius; overflow: hidden; .check-list-header { background: rgba(107, 158, 120, 0.04); padding: 0.6rem 1rem; font-size: 0.68rem; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: rgba(201, 168, 76, 0.75); font-family: $font-family-monospace; border-bottom: 1px solid rgba(107, 158, 120, 0.1); } .check-list-item { display: flex; justify-content: space-between; align-items: center; padding: 0.55rem 1rem; font-size: 0.85rem; border-bottom: 1px solid rgba(107, 158, 120, 0.04); &:last-child { border-bottom: none; } .check-name { color: $gray-300; display: flex; align-items: center; gap: 0.5rem; } }}// โโ Monitor card โโ// When used inside a flex/grid row, `h-100` class stretches the card to its// cell height. The body grows to fill any extra space so the two cards next// to each other stay visually balanced even with different dl row counts..monitor-card { background: $card-bg; border: 1px solid $card-border-color; border-radius: $border-radius; overflow: hidden; display: flex; flex-direction: column; .monitor-header { display: flex; justify-content: space-between; align-items: center; padding: 0.7rem 1rem; background: rgba(107, 158, 120, 0.04); border-bottom: 1px solid rgba(107, 158, 120, 0.08); flex-shrink: 0; .monitor-title { font-family: $font-family-monospace; font-size: 0.78rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: $white; } } .monitor-body { padding: 0.9rem 1rem; flex: 1 1 auto; dl { margin: 0; display: grid; grid-template-columns: max-content 1fr; gap: 0.4rem 1rem; } dt { font-size: 0.7rem; letter-spacing: 0.06em; text-transform: uppercase; color: #665f56; font-weight: 500; } dd { margin: 0; font-size: 0.8rem; color: $gray-200; font-family: $font-family-monospace; } }}// โโ Insight row (lighthouse opportunities / crawler issues) โโ.insight-header { display: grid; grid-template-columns: 110px 1fr 1fr 140px; gap: 1rem; padding: 0.55rem 1rem; background: rgba(107, 158, 120, 0.04); border: 1px solid rgba(107, 158, 120, 0.08); border-radius: $border-radius; font-family: $font-family-monospace; font-size: 0.68rem; letter-spacing: 0.08em; text-transform: uppercase; color: rgba(201, 168, 76, 0.75); margin-bottom: 0.6rem;}.insight-row { display: grid; grid-template-columns: 110px 1fr 1fr 140px; gap: 1rem; padding: 0.65rem 1rem; background: $card-bg; border: 1px solid $card-border-color; border-radius: $border-radius; margin-bottom: 0.4rem; font-size: 0.85rem; align-items: center; &:hover { border-color: rgba(107, 158, 120, 0.2); } .insight-col-trunc { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }}@media (max-width: 767px) { .insight-header { display: none; } .insight-row { grid-template-columns: 1fr; gap: 0.4rem; }}// โโ Auth split view โโ.auth-shell { min-height: calc(100vh - 52px); display: grid; grid-template-columns: 1fr 1fr; @media (max-width: 991px) { grid-template-columns: 1fr; } .auth-form-col { padding: 3rem 2rem; display: flex; align-items: center; justify-content: center; } .auth-visual-col { position: relative; overflow: hidden; background: linear-gradient(145deg, #090806 0%, #0e0d0a 60%, #13120e 100%); border-left: 1px solid rgba(107, 158, 120, 0.08); @media (max-width: 991px) { display: none; } } .auth-form-wrap { width: 100%; max-width: 380px; }}
modified
status/static_src/styles/bootstrap.scss
@@ -1,25 +1,151 @@@use "bootstrap/scss/bootstrap" as *;@use "variables" as *;@use "bootstrap/scss/bootstrap" with ( $font-family-sans-serif: $font-family-sans-serif, $font-family-monospace: $font-family-monospace, $headings-font-family: $headings-font-family, $headings-font-weight: $headings-font-weight, $font-size-base: $font-size-base, $line-height-base: $line-height-base, $headings-line-height: $headings-line-height, $white: $white, $gray-100: $gray-100, $gray-200: $gray-200, $gray-300: $gray-300, $gray-400: $gray-400, $gray-500: $gray-500, $gray-600: $gray-600, $gray-700: $gray-700, $gray-800: $gray-800, $gray-900: $gray-900, $black: $black, $body-bg: $body-bg, $body-color: $body-color, $primary: $primary, $secondary: $secondary, $success: $success, $info: $info, $warning: $warning, $danger: $danger, $light: $light, $dark: $dark, $link-color: $link-color, $link-hover-color: $link-hover-color, $link-decoration: $link-decoration, $link-hover-decoration: $link-hover-decoration, $border-color: $border-color, $border-radius: $border-radius, $border-radius-sm: $border-radius-sm, $border-radius-lg: $border-radius-lg, $card-bg: $card-bg, $card-border-color: $card-border-color, $card-color: $card-color, $card-cap-bg: $card-cap-bg, $input-bg: $input-bg, $input-color: $input-color, $input-border-color: $input-border-color, $input-focus-bg: $input-focus-bg, $input-focus-color: $input-focus-color, $input-focus-border-color: $input-focus-border-color, $input-focus-box-shadow: $input-focus-box-shadow, $input-placeholder-color: $input-placeholder-color, $navbar-dark-color: $navbar-dark-color, $navbar-dark-hover-color: $navbar-dark-hover-color, $navbar-dark-active-color: $navbar-dark-active-color, $list-group-bg: $list-group-bg, $list-group-border-color: $list-group-border-color, $list-group-hover-bg: $list-group-hover-bg, $list-group-action-color: $list-group-action-color, $list-group-action-hover-color: $list-group-action-hover-color, $list-group-action-active-bg: $list-group-action-active-bg, $list-group-action-active-color: $list-group-action-active-color, $list-group-active-bg: $list-group-active-bg, $list-group-active-border-color: $list-group-active-border-color, $list-group-active-color: $list-group-active-color, $breadcrumb-active-color: $breadcrumb-active-color, $breadcrumb-divider-color: $breadcrumb-divider-color, $dropdown-bg: $dropdown-bg, $dropdown-border-color: $dropdown-border-color, $dropdown-color: $dropdown-color, $dropdown-link-color: $dropdown-link-color, $dropdown-link-hover-color: $dropdown-link-hover-color, $dropdown-link-hover-bg: $dropdown-link-hover-bg, $btn-close-color: $btn-close-color, $modal-content-bg: $modal-content-bg, $modal-content-border-color: $modal-content-border-color, $modal-header-border-color: $modal-header-border-color, $modal-footer-border-color: $modal-footer-border-color, $modal-backdrop-bg: $modal-backdrop-bg, $modal-backdrop-opacity: $modal-backdrop-opacity, $progress-bg: $progress-bg, $progress-bar-bg: $progress-bar-bg, $table-bg: $table-bg, $table-color: $table-color, $table-border-color: $table-border-color,);// โโ Surface utilities โโ.bg-surface { background: #13120e !important;}.bg-surface-2 { background: #18160f !important;}.bg-deep { background: #09080626 !important; // very subtle warm black}.bg-deep,.bg-black { background-color: $black !important; background-color: #090806 !important;}.bg-teal-900 { background-color: $teal-900 !important;.border-subtle { border-color: rgba(107, 158, 120, 0.1) !important;}.bg-teal-800 { background-color: $teal-800 !important;.text-muted { color: #9e968a !important;}.text-green { color: $green-bright !important;}.text-amber { color: $amber !important;}// Legacy utility aliases used by existing templates.bg-teal-900 { background-color: #090806 !important; }.bg-teal-800 { background-color: #0e0d0a !important; }.link-footer { color: $gray-500; color: $gray-400; text-decoration: none; transition: color 100ms ease-in-out; transition: color 200ms ease; &:hover { color: $white; text-decoration: underline; }}
@@ -27,24 +153,161 @@ display: block; flex-wrap: nowrap; white-space: nowrap; margin-bottom: 0; .breadcrumb-item { font-size: 0.9em; font-size: 0.82em; white-space: nowrap; display: inline-block; a { color: $gray-500; color: $gray-400; text-decoration: none; transition: color .15s ease-in-out; transition: color 150ms ease; &:hover { color: $gray-200; color: $green-bright; } } &.active { color: $white; color: $gray-200; } }}// Buttons โ flat, no glow.btn { letter-spacing: 0.02em; font-weight: 500; transition: background-color 180ms ease, border-color 180ms ease, color 180ms ease;}.btn-primary { color: #0b0a08; background: $green; border-color: $green; &:hover, &:focus { color: #0b0a08; background: $green-bright; border-color: $green-bright; }}.btn-success { color: #0b0a08; background: $green; border-color: $green; &:hover, &:focus { color: #0b0a08; background: $green-bright; border-color: $green-bright; }}.btn-outline-light { border-color: rgba(221, 215, 205, 0.18); color: $gray-200; &:hover, &:focus { background: rgba(221, 215, 205, 0.05); border-color: rgba(221, 215, 205, 0.35); color: $white; }}.btn-outline-danger { color: $terracotta; border-color: $terracotta-border; &:hover, &:focus { background: $terracotta-dim; color: #d88870; border-color: rgba(196, 112, 85, 0.4); }}.btn-link { color: $green-bright;}// Alerts for dark.alert { border-radius: $border-radius; }.alert-info { background: rgba(126, 170, 184, 0.08); border-color: rgba(126, 170, 184, 0.2); color: #9ec5d2;}.alert-warning { background: $amber-dim; border-color: $amber-border; color: #ddc06a;}.alert-danger { background: $terracotta-dim; border-color: $terracotta-border; color: #e38871;}.alert-success { background: $green-dim; border-color: $green-border; color: $green-bright;}// Forms.form-control,.form-select { background-color: $input-bg; border-color: $input-border-color; color: $input-color; &:focus { background-color: $input-focus-bg; }}.form-floating > label { color: #665f56;}// Badges.badge { font-weight: 500; letter-spacing: 0.04em; padding: 0.35em 0.55em;}// Pagination.page-link { background: $card-bg; border-color: $card-border-color; color: $gray-300; &:hover { background: #1a1812; border-color: $green-border; color: $green-bright; }}.page-item.active .page-link { background: $green-dim; border-color: $green-border; color: $green-bright;}.page-item.disabled .page-link { background: $card-bg; border-color: $card-border-color; color: #4a443c;}
modified
status/templates/403.html
@@ -6,15 +6,19 @@{% block description %}You are not allowed to access this page.{% endblock %}{% block breadcrumbs_wrapper %}{% endblock %}{% block breadcrumbs %}{% endblock %}{% block main %}<div class="container"><div class="container py-5"> <div class="row"> <div class="col text-center py-5"> <h1 class="display-1">403</h1> <p>You are not allowed to access this page.</p> <div class="col-12 col-lg-8 offset-lg-2 text-center py-5"> <div class="section-label mb-3" style="display:inline-flex;">access denied</div> <div class="display-1 fw-bolder text-white mb-3" style="font-size: clamp(5rem, 16vw, 10rem); letter-spacing: -0.04em; text-shadow: 0 0 30px rgba(239, 106, 91, 0.3);"> <span style="color: #ef6a5b;">4</span><span style="color: #e6b84c;">0</span><span style="color: #ef6a5b;">3</span> </div> <p class="text-muted mb-4">Credentials lack the clearance required for this endpoint.</p> <a href="/" class="btn btn-primary">โ Back to base</a> </div> </div></div>
modified
status/templates/404.html
@@ -6,15 +6,19 @@{% block description %}That means the page you are looking for doesn't exist.{% endblock %}{% block breadcrumbs_wrapper %}{% endblock %}{% block breadcrumbs %}{% endblock %}{% block main %}<div class="container"><div class="container py-5"> <div class="row"> <div class="col text-center py-5"> <h1 class="display-1">404</h1> <p>That means the page you are looking for doesn't exist.</p> <div class="col-12 col-lg-8 offset-lg-2 text-center py-5"> <div class="section-label mb-3" style="display:inline-flex;">signal lost</div> <div class="display-1 fw-bolder text-white mb-3" style="font-size: clamp(5rem, 16vw, 10rem); letter-spacing: -0.04em; text-shadow: 0 0 30px rgba(239, 106, 91, 0.3);"> <span style="color: #ef6a5b;">4</span><span style="color: #58d4e6;">0</span><span style="color: #ef6a5b;">4</span> </div> <p class="text-muted mb-4">The endpoint you pinged does not resolve. No bytes returned.</p> <a href="/" class="btn btn-primary">โ Back to base</a> </div> </div></div>
modified
status/templates/500.html
@@ -6,15 +6,19 @@{% block description %}Something went wrong.{% endblock %}{% block breadcrumbs_wrapper %}{% endblock %}{% block breadcrumbs %}{% endblock %}{% block main %}<div class="container"><div class="container py-5"> <div class="row"> <div class="col text-center py-5"> <h1 class="display-1">500</h1> <p>Something went wrong.</p> <div class="col-12 col-lg-8 offset-lg-2 text-center py-5"> <div class="section-label mb-3" style="display:inline-flex;">server fault</div> <div class="display-1 fw-bolder text-white mb-3" style="font-size: clamp(5rem, 16vw, 10rem); letter-spacing: -0.04em; text-shadow: 0 0 30px rgba(239, 106, 91, 0.3);"> <span style="color: #ef6a5b;">5</span><span style="color: #ef6a5b;">0</span><span style="color: #ef6a5b;">0</span> </div> <p class="text-muted mb-4">Something threw on the way back. We've been notified.</p> <a href="/" class="btn btn-primary">โ Back to base</a> </div> </div></div>
modified
status/templates/base.html
@@ -14,6 +14,8 @@ <base href="{{ BASE_URL }}"> {% endif %} <link rel="icon" type="image/svg+xml" href="{% url 'favicon' %}"> {% block extra_head %}{% endblock %} <link href="{% static 'base.css' %}" rel="stylesheet">
@@ -24,29 +26,54 @@ {% include 'includes/messages.html' %} {% block nav %} <nav class="navbar bg-teal-900 d-print-none"> <nav class="navbar navbar-dark navbar-expand-md d-print-none"> <div class="container"> <a class="navbar-brand text-white" href="/"> <span class="display-1 fs-4 text-white text-decoration-none text-uppercase my-0">Status</span> </a> {% if user.is_authenticated %} <form method="post" action="{% url 'logout' %}" class="d-inline"> {% csrf_token %} <button type="submit" class="btn btn-sm btn-outline-light"> Logout </button> </form> {% else %} <a href="/accounts/login/" class="btn btn-sm btn-outline-light"> Login <a class="navbar-brand" href="/"> <span class="logo" aria-hidden="true"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"> <polyline points="2,34 18,34 24,28 30,14 36,52 42,20 48,34 62,34" fill="none" stroke="#6b9e78" stroke-width="6" stroke-linejoin="round" stroke-linecap="round"/> <circle cx="30" cy="14" r="3.5" fill="#c9a84c"/> </svg> </span> <span class="brand-text">Status</span> <span class="brand-dot" aria-hidden="true"></span> </a> {% endif %} <button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navMain" aria-controls="navMain" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navMain"> <ul class="navbar-nav ms-3"> {% if user.is_authenticated %} <li class="nav-item"><a class="nav-link {% if request.path == '/properties/' %}active{% endif %}" href="/properties/">Properties</a></li> <li class="nav-item"><a class="nav-link {% if request.path == '/accounts/profile/' %}active{% endif %}" href="/accounts/profile/">Profile</a></li> {% else %} <li class="nav-item"><a class="nav-link {% if request.path == '/' %}active{% endif %}" href="/">Home</a></li> <li class="nav-item"><a class="nav-link {% if request.path == '/changelog/' %}active{% endif %}" href="/changelog/">Changelog</a></li> {% endif %} </ul> <ul class="navbar-nav ms-auto"> {% if user.is_authenticated %} <li class="nav-item d-flex align-items-center"> <form method="post" action="{% url 'logout' %}" class="d-inline"> {% csrf_token %} <button type="submit" class="btn btn-sm btn-outline-light">Logout</button> </form> </li> {% else %} <li class="nav-item d-flex align-items-center"> <a href="/accounts/login/" class="btn btn-sm btn-outline-light">Login</a> </li> {% endif %} </ul> </div> </div> </nav> {% endblock %} {% block breadcrumb_wrapper %} <div class="bg-teal-800 py-2 d-print-none"> <div class="breadcrumb-bar py-2 overflow-auto d-print-none"> <div class="container"> {% block breadcrumbs %}{% endblock %} </div>
@@ -58,22 +85,25 @@ </main> {% block footer %} <footer class="mt-4 bg-black text-light d-print-none"> <footer class="d-print-none"> <div class="container"> <div class="row"> <div class="col-12 col-md-12 col-lg-4 mb-5 mb-lg-0"> <div class="h5 font-monospace mb-3">Status</div> <p>Made by <a href="https://isaacbythewood.com/" class="text-light">Isaac Bythewood</a>, a simple opinionated self-hosted website status monitoring solution.</p> <div class="row links"> <div class="col-12 col-lg-6 mb-4 mb-lg-0"> <div class="h5 mb-3">// Status</div> <p>Self-hosted website monitoring by <a href="https://isaacbythewood.com/">Isaac Bythewood</a>. HTTP checks every three minutes, Lighthouse audits, SEO crawler, security header analysis. Alerts via email + Discord. Own your uptime.</p> </div> <div class="col-6 col-md-4 offset-lg-4 col-lg-2"> <div class="h5 font-monospace mb-3">Pages</div> <div class="col-6 col-lg-2 offset-lg-2"> <div class="h5 mb-3">// Pages</div> <ul class="list-unstyled"> <li class="mb-2"><a href="/" class="link-footer">Home</a></li> <li class="mb-2"><a href="/changelog/" class="link-footer">Changelog</a></li> <li class="mb-2"><a href="https://github.com/overshard/status" class="link-footer" target="_blank">Source</a></li> </ul> </div> <div class="col-6 col-md-4 col-lg-2"> <div class="h5 font-monospace mb-3">Accounts</div> <div class="col-6 col-lg-2"> <div class="h5 mb-3">// Accounts</div> <ul class="list-unstyled"> {% if user.is_authenticated %} <li class="mb-2"><a href="/properties/" class="link-footer">Properties</a></li>
@@ -94,21 +124,26 @@ </div> </footer> <div class="bg-dark py-2 d-print-none"> <div class="footer-bar d-print-none"> <div class="container"> <div class="row"> <div class="col-sm-6 d-flex align-items-center justify-content-center justify-content-sm-start mt-3 mt-sm-0 order-1 order-sm-0"> <small class="text-light"> © {% now 'Y' %} Isaac Bythewood. Some rights reserved. </small> <div class="row align-items-center"> <div class="col-sm-6 d-flex align-items-center justify-content-center justify-content-sm-start order-1 order-sm-0"> <small>© {% now 'Y' %} Isaac Bythewood ยท Some rights reserved</small> </div> <div class="col-sm-6 d-flex justify-content-center justify-content-sm-end"> <a href="https://github.com/overshard/status" target="_blank" class="text-light fs-3 d-flex align-items-center" aria-label="GitHub"> <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="bi bi-github" viewBox="0 0 16 16"> <div class="col-sm-6 d-flex justify-content-center justify-content-sm-end py-3 py-sm-0"> <a href="https://github.com/overshard/status" target="_blank" class="footer-bar-link" aria-label="GitHub"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16"> <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/> </svg> </a> <a href="https://isaacbythewood.com/" target="_blank" class="logo shadow ms-4" aria-label="Isaac Bythewood"></a> <a href="https://isaacbythewood.com/" target="_blank" class="logo logo-sm ms-3" aria-label="Isaac Bythewood"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"> <polyline points="2,34 18,34 24,28 30,14 36,52 42,20 48,34 62,34" fill="none" stroke="#6b9e78" stroke-width="6" stroke-linejoin="round" stroke-linecap="round"/> <circle cx="30" cy="14" r="3.5" fill="#c9a84c"/> </svg> </a> </div> </div> </div>
modified
status/templates/registration/logged_out.html
@@ -2,7 +2,7 @@{% block breadcrumbs %}<nav style="--bs-breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='%236c757d'/%3E%3C/svg%3E");" aria-label="breadcrumb"><nav aria-label="breadcrumb"> <ol class="breadcrumb mb-0"> <li class="breadcrumb-item"><a href="/">Home</a></li> <li class="breadcrumb-item active" aria-current="page">{{ title }}</li>
@@ -13,8 +13,13 @@{% block main %}<div class="container my-5"> <h1>{{ title }}</h1> <p>You've signed out, you can sign in again if you want here:</p> <a href="{% url 'login' %}" class="btn btn-primary">Login</a> <div class="row"> <div class="col-lg-6 offset-lg-3 col-md-8 offset-md-2"> <div class="section-label mb-2">session ended</div> <h1 class="fw-bolder text-white" style="letter-spacing: -0.01em;">{{ title }}</h1> <p class="text-muted">You've been signed out. Hit the button below when you're ready to come back.</p> <a href="{% url 'login' %}" class="btn btn-primary">Login โ</a> </div> </div></div>{% endblock %}
modified
status/templates/registration/login.html
@@ -3,7 +3,7 @@{% block breadcrumbs %}<nav style="--bs-breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='%236c757d'/%3E%3C/svg%3E");" aria-label="breadcrumb"><nav aria-label="breadcrumb"> <ol class="breadcrumb mb-0"> <li class="breadcrumb-item"><a href="/">Home</a></li> <li class="breadcrumb-item active" aria-current="page">{{ title }}</li>
@@ -12,87 +12,66 @@{% endblock %}{% block nav %}{% endblock %}{% block main %}<div class="auth-shell"> <div class="auth-form-col"> <div class="auth-form-wrap"> <div class="section-label mb-2">authenticate</div> <h1 class="fw-bolder text-white" style="letter-spacing: -0.01em; font-size: 1.9rem;">{{ title }}</h1> <p class="text-muted small mb-4">Access is invite-only. Contact the operator for credentials.</p> {% if form.errors and not form.non_field_errors %} <div class="alert alert-warning py-2 small"> {% if form.errors.items|length == 1 %}{% translate "Please correct the error below." %}{% else %}{% translate "Please correct the errors below." %}{% endif %} </div> {% endif %}{% block breadcrumb_wrapper %}{% endblock %} {% if form.non_field_errors %} {% for error in form.non_field_errors %} <div class="alert alert-danger py-2 small">{{ error }}</div> {% endfor %} {% endif %} {% if user.is_authenticated %} <div class="alert alert-warning py-2 small"> {% blocktranslate trimmed %} You are authenticated as {{ username }}, but are not authorized to access this page. {% endblocktranslate %} </div> {% endif %}{% block main %}<div class="container-fluid"> <div class="row"> <div class="col-12 col-md-8 col-lg-6 vh-100 d-flex align-items-center"> <div class="container my-5"> <div class="row"> <div class="col-lg-6 offset-lg-3 col-sm-8 offset-sm-2 col-xs-12 offset-xs-0"> <h1>{{ title }}</h1> <p>You have to be invited to use this service, please contact the site owner to get a unique username and password.</p> </div> <form method="POST" action="{{ app_path }}"> {% csrf_token %} <input type="hidden" name="next" value="{{ next }}" /> <div class="form-floating mb-3"> <input type="text" class="form-control {% if form.username.errors %}is-invalid{% endif %}" name="username" id="id_username" placeholder="Username" value="{{ form.username.value|default:'' }}" required autofocus /> <label for="id_username">Username</label> </div> <div class="row"> <div class="col-lg-6 offset-lg-3 col-sm-8 offset-sm-2 col-xs-12 offset-xs-0"> {% if form.errors and not form.non_field_errors %} <p class="errornote"> {% if form.errors.items|length == 1 %} {% translate "Please correct the error below." %} {% else %} {% translate "Please correct the errors below." %} {% endif %} </p> {% endif %} {% if form.non_field_errors %} {% for error in form.non_field_errors %} <p class="errornote"> {{ error }} </p> {% endfor %} {% endif %} {% if user.is_authenticated %} <p class="errornote"> {% blocktranslate trimmed %} You are authenticated as {{ username }}, but are not authorized to access this page. Would you like to login to a different account? {% endblocktranslate %} </p> {% endif %} </div> <div class="form-floating mb-3"> <input type="password" class="form-control {% if form.password.errors %}is-invalid{% endif %}" name="password" id="id_password" placeholder="Password" required /> <label for="id_password">Password</label> </div> <div class="row"> <div class="col-lg-6 offset-lg-3 col-sm-8 offset-sm-2 col-xs-12 offset-xs-0"> <form method="POST" action="{{ app_path }}"> {% csrf_token %} <input type="hidden" name="next" value="{{ next }}" /> <div class="form-floating mb-3"> <input type="text" class="form-control {% if form.username.errors %}is-invalid{% endif %}" name="username" id="id_username" placeholder="Username" value="{{ form.username.value|default:'' }}" required autofocus /> <label for="id_username" class="form-label">Username</label> </div> <div class="form-floating mb-3"> <input type="password" class="form-control {% if form.password.errors %}is-invalid{% endif %}" name="password" id="id_password" placeholder="Password" required /> <label for="id_password" class="form-label">Password</label> </div> <div class="row"> <div class="col"> <button type="submit" class="btn btn-primary"> Login </button> <a href="{% url 'password_reset' %}" class="btn btn-link float-end"> Forgot password? </a> </div> </div> </form> </div> <div class="d-flex justify-content-between align-items-center mt-4"> <button type="submit" class="btn btn-primary">Login โ</button> <a href="{% url 'password_reset' %}" class="btn btn-link small">Forgot password?</a> </div> </form> </div> </div> <div class="auth-visual-col"> <div class="grid-plane" style="position:absolute; inset:0; height:100%;"> <div class="grid-plane-overlay"></div> <div class="grid-plane-grid"></div> </div> <div style="position:absolute; inset:0; display:flex; align-items:center; justify-content:center; padding: 2rem; z-index: 3;"> <div class="terminal-block" style="max-width: 380px; width: 100%;"> <span class="t-line"><span class="t-comment"># status ยท monitor</span></span> <span class="t-line"><span class="t-prompt">operator$</span><span class="t-out">whoami</span></span> <span class="t-line"><span class="t-val">anonymous</span></span> <span class="t-line"><span class="t-prompt">operator$</span><span class="t-out">login --mode=interactive</span></span> <span class="t-line"><span class="t-key">awaiting</span>=<span class="t-val">credentials</span> <span class="t-cursor"></span></span> </div> </div> <div class="col-md-4 col-lg-6 d-none d-md-block vh-100 login-hero" style="--background-image: url({% static 'images/home-hero.webp' %});"></div> </div></div>{% endblock %}{% block footer %}{% endblock %}
modified
status/templates/registration/password_change_done.html
@@ -2,7 +2,7 @@{% block breadcrumbs %}<nav style="--bs-breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='%236c757d'/%3E%3C/svg%3E");" aria-label="breadcrumb"><nav aria-label="breadcrumb"> <ol class="breadcrumb mb-0"> <li class="breadcrumb-item"><a href="/">Home</a></li> <li class="breadcrumb-item"><a href="{% url 'profile' %}">Profile</a></li>
@@ -15,10 +15,12 @@{% block main %}<div class="container my-5"> <div class="row"> <div class="col-lg-6 offset-lg-3 col-sm-8 offset-sm-2 col-xs-12 offset-xs-0"> <h1>{{ title }}</h1> <p>Your password was changed.</p> <p><a href="{% url 'profile' %}">Back to profile</a></p> <div class="col-lg-6 offset-lg-3 col-md-8 offset-md-2"> <div class="section-label mb-2">rotation complete</div> <h1 class="fw-bolder text-white" style="letter-spacing: -0.01em;">{{ title }}</h1> <p class="text-muted">Your password has been updated.</p> <a href="{% url 'profile' %}" class="btn btn-primary">โ Back to profile</a> </div> </div></div>{% endblock %}
modified
status/templates/registration/password_change_form.html
@@ -2,7 +2,7 @@{% block breadcrumbs %}<nav style="--bs-breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='%236c757d'/%3E%3C/svg%3E");" aria-label="breadcrumb"><nav aria-label="breadcrumb"> <ol class="breadcrumb mb-0"> <li class="breadcrumb-item"><a href="/">Home</a></li> <li class="breadcrumb-item"><a href="{% url 'profile' %}">Profile</a></li>
@@ -15,50 +15,36 @@{% block main %}<div class="container my-5"> <div class="row"> <div class="col-lg-6 offset-lg-3 col-sm-8 offset-sm-2 col-xs-12 offset-xs-0"> <h1>{{ title }}</h1> <p>Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one.</p> </div> </div> <div class="row"> <div class="col-lg-6 offset-lg-3 col-sm-8 offset-sm-2 col-xs-12 offset-xs-0"> <form method="POST"> {% csrf_token %} <div class="col-lg-6 offset-lg-3 col-md-8 offset-md-2"> <div class="section-label mb-2">rotate credentials</div> <h1 class="fw-bolder text-white" style="letter-spacing: -0.01em;">{{ title }}</h1> <p class="text-muted small mb-4">Confirm your existing password, then set a new one.</p> {% if form.errors %} <p>{% if form.errors.items|length == 1 %}Please correct the error below.{% else %}Please correct the errors below.{% endif %}</p> {% endif %} <p>Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly.</p> {% if form.errors %} <div class="alert alert-warning py-2 small">{% if form.errors.items|length == 1 %}Please correct the error below.{% else %}Please correct the errors below.{% endif %}</div> {% endif %} <form method="POST"> {% csrf_token %} <div class="form-floating mb-3"> <input type="password" class="form-control {% if form.old_password.errors %}is-invalid{% endif %}" name="old_password" id="id_old_password" placeholder="New password" autocomplete="new-password" required autofocus /> <label for="id_old_password" class="form-label">Old password</label> <input type="password" class="form-control {% if form.old_password.errors %}is-invalid{% endif %}" name="old_password" id="id_old_password" placeholder="Old password" autocomplete="current-password" required autofocus /> <label for="id_old_password">Old password</label> {{ form.old_password.errors }} </div> <div class="form-floating mb-3"> <input type="password" class="form-control {% if form.new_password1.errors %}is-invalid{% endif %}" name="new_password1" id="id_new_password1" placeholder="New password" autocomplete="new-password" required /> <label for="id_new_password1" class="form-label">New password</label> <label for="id_new_password1">New password</label> {% if form.new_password1.help_text %} <div class="border py-2 pb-0 rounded rounded-1 mt-1 small">{{ form.new_password1.help_text|safe }}</div> <div class="border border-subtle py-2 px-3 rounded mt-2 small text-muted">{{ form.new_password1.help_text|safe }}</div> {% endif %} {{ form.new_password1.errors }} </div> <div class="form-floating mb-3"> <input type="password" class="form-control {% if form.new_password2.errors %}is-invalid{% endif %}" name="new_password2" id="id_new_password2" placeholder="New password" autocomplete="new-password" required /> <label for="id_new_password2" class="form-label">New password confirmation</label> {% if form.new_password2.help_text %} <div class="border py-2 pb-0 rounded rounded-1 mt-1 small">{{ form.new_password2.help_text|safe }}</div> {% endif %} <input type="password" class="form-control {% if form.new_password2.errors %}is-invalid{% endif %}" name="new_password2" id="id_new_password2" placeholder="Confirm new password" autocomplete="new-password" required /> <label for="id_new_password2">New password confirmation</label> {{ form.new_password2.errors }} </div> <button type="submit" class="btn btn-primary"> Change my password </button> <button type="submit" class="btn btn-primary">Change password โ</button> </form> </div> </div>
modified
status/templates/registration/password_reset_complete.html
@@ -2,7 +2,7 @@{% block breadcrumbs %}<nav style="--bs-breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='%236c757d'/%3E%3C/svg%3E");" aria-label="breadcrumb"><nav aria-label="breadcrumb"> <ol class="breadcrumb mb-0"> <li class="breadcrumb-item"><a href="/">Home</a></li> <li class="breadcrumb-item"><a href="{% url 'login' %}">Login</a></li>
@@ -15,10 +15,12 @@{% block main %}<div class="container my-5"> <div class="row"> <div class="col-lg-6 offset-lg-3 col-sm-8 offset-sm-2 col-xs-12 offset-xs-0"> <h1>{{ title }}</h1> <p>Your password has been set. You may go ahead and log in now.</p> <p><a href="{{ login_url }}">Login</a></p> <div class="col-lg-6 offset-lg-3 col-md-8 offset-md-2"> <div class="section-label mb-2">reset complete</div> <h1 class="fw-bolder text-white" style="letter-spacing: -0.01em;">{{ title }}</h1> <p class="text-muted">Your password has been updated. Sign in whenever you're ready.</p> <a href="{{ login_url }}" class="btn btn-primary">Login โ</a> </div> </div></div>{% endblock %}
modified
status/templates/registration/password_reset_confirm.html
@@ -2,7 +2,7 @@{% block breadcrumbs %}<nav style="--bs-breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='%236c757d'/%3E%3C/svg%3E");" aria-label="breadcrumb"><nav aria-label="breadcrumb"> <ol class="breadcrumb mb-0"> <li class="breadcrumb-item"><a href="/">Home</a></li> <li class="breadcrumb-item"><a href="{% url 'login' %}">Login</a></li>
@@ -15,41 +15,33 @@{% block main %}<div class="container my-5"> <div class="row"> <div class="col-lg-6 offset-lg-3 col-sm-8 offset-sm-2 col-xs-12 offset-xs-0"> <h1>{{ title }}</h1> </div> </div> {% if validlink %} <div class="row"> <div class="col-lg-6 offset-lg-3 col-sm-8 offset-sm-2 col-xs-12 offset-xs-0"> <p>Please enter your new password twice so we can verify you typed it in correctly.</p> <div class="col-lg-6 offset-lg-3 col-md-8 offset-md-2"> <div class="section-label mb-2">set new password</div> <h1 class="fw-bolder text-white" style="letter-spacing: -0.01em;">{{ title }}</h1> {% if validlink %} <p class="text-muted small mb-4">Enter it twice so we can verify.</p> <form method="POST"> {% csrf_token %} <input type="hidden" autocomplete="username" value="{{ form.user.get_username }}" /> <div class="form-floating mb-3"> <input type="password" class="form-control {% if form.new_password1.errors %}is-invalid{% endif %}" name="new_password1" id="id_new_password1" placeholder="New password" autocomplete="new-password" required autofocus /> <label for="id_new_password1" class="form-label">New password</label> <label for="id_new_password1">New password</label> {{ form.new_password1.errors }} </div> <div class="form-floating mb-3"> <input type="password" class="form-control {% if form.new_password2.errors %}is-invalid{% endif %}" name="new_password2" id="id_new_password2" placeholder="Confirm password" autocomplete="new-password" required /> <label for="id_new_password2" class="form-label">Confirm password</label> <label for="id_new_password2">Confirm password</label> {{ form.new_password2.errors }} </div> <button type="submit" class="btn btn-primary"> Change my password </button> <button type="submit" class="btn btn-primary">Update password โ</button> </form> {% else %} <div class="alert alert-warning py-2 small"> The reset link was invalid, possibly because it's already been used. <a href="{% url 'password_reset' %}">Request a new one</a>. </div> {% endif %} </div> </div> {% else %} <div class="row"> <div class="col-lg-6 offset-lg-3 col-sm-8 offset-sm-2 col-xs-12 offset-xs-0"> <p>The password reset link was invalid, possibly because it has already been used. Please request a new password reset.</p> </div> </div> {% endif %}</div>{% endblock %}
modified
status/templates/registration/password_reset_done.html
@@ -2,7 +2,7 @@{% block breadcrumbs %}<nav style="--bs-breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='%236c757d'/%3E%3C/svg%3E");" aria-label="breadcrumb"><nav aria-label="breadcrumb"> <ol class="breadcrumb mb-0"> <li class="breadcrumb-item"><a href="/">Home</a></li> <li class="breadcrumb-item"><a href="{% url 'login' %}">Login</a></li>
@@ -15,12 +15,11 @@{% block main %}<div class="container my-5"> <div class="row"> <div class="col-lg-6 offset-lg-3 col-sm-8 offset-sm-2 col-xs-12 offset-xs-0"> <h1>{{ title }}</h1> <p>We've emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly.</p> <p>If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder.</p> <div class="col-lg-6 offset-lg-3 col-md-8 offset-md-2"> <div class="section-label mb-2">dispatched</div> <h1 class="fw-bolder text-white" style="letter-spacing: -0.01em;">{{ title }}</h1> <p class="text-muted">If an account with that email exists, a reset link is on its way. Check spam if it doesn't appear shortly.</p> <a href="{% url 'login' %}" class="btn btn-outline-light">โ Back to login</a> </div> </div></div>
modified
status/templates/registration/password_reset_form.html
@@ -2,7 +2,7 @@{% block breadcrumbs %}<nav style="--bs-breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='%236c757d'/%3E%3C/svg%3E");" aria-label="breadcrumb"><nav aria-label="breadcrumb"> <ol class="breadcrumb mb-0"> <li class="breadcrumb-item"><a href="/">Home</a></li> <li class="breadcrumb-item"><a href="{% url 'login' %}">Login</a></li>
@@ -15,23 +15,18 @@{% block main %}<div class="container my-5"> <div class="row"> <div class="col-lg-6 offset-lg-3 col-sm-8 offset-sm-2 col-xs-12 offset-xs-0"> <h1>{{ title }}</h1> <p>Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one.</p> </div> </div> <div class="row"> <div class="col-lg-6 offset-lg-3 col-sm-8 offset-sm-2 col-xs-12 offset-xs-0"> <div class="col-lg-6 offset-lg-3 col-md-8 offset-md-2"> <div class="section-label mb-2">recover access</div> <h1 class="fw-bolder text-white" style="letter-spacing: -0.01em;">{{ title }}</h1> <p class="text-muted small mb-4">Enter your email โ we'll dispatch a reset link.</p> <form method="POST"> {% csrf_token %} <div class="form-floating mb-3"> <input type="text" class="form-control {% if form.email.errors %}is-invalid{% endif %}" name="email" id="id_email" placeholder="Email" value="{{ form.email.value|default:'' }}" required autofocus /> <label for="id_email" class="form-label">Email</label> <input type="email" class="form-control {% if form.email.errors %}is-invalid{% endif %}" name="email" id="id_email" placeholder="you@domain" value="{{ form.email.value|default:'' }}" required autofocus /> <label for="id_email">Email</label> </div> <button type="submit" class="btn btn-primary"> Reset my password </button> <button type="submit" class="btn btn-primary">Send reset link โ</button> </form> </div> </div>
@@ -23,6 +23,7 @@ const fixDatamapsStrictMode = {export default defineConfig({ plugins: [fixDatamapsStrictMode], base: "/static/", build: { outDir: resolve(__dirname, "status/static"), emptyOutDir: true,