mirror of
https://github.com/Screenly/Anthias.git
synced 2026-06-10 09:08:09 -04:00
* feat(ui): modernize splash, login, and error pages Splash gets a gradient background with an elevated card, status pill (pulsing while detecting, solid when ready), monospace IP pills, and an SVG QR code generated client-side from the qrcode npm package via a new bun-bundled splash.ts entry. Login mirrors the pre-auth gradient treatment with a centred light card. Adds 400/403/404/500 templates that share the same layout via _error.html so Django stops falling back to its default plain-HTML error pages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ui): use dark-text logo on light auth/error cards logo-full.svg has the wordmark filled white (designed for the dark navbar). On the white auth card it disappears. dark.svg is the same mark with a black wordmark — the right asset for light backgrounds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review): address Copilot feedback on splash/login/error pages - Strip the global 4.5rem navbar offset on full-bleed pre-auth pages via a body_class block, so login + error cards aren't shifted down 72 px on screens that don't render the navbar. - Hide the splash CTA copy ("Open this on your phone…") until at least one IP arrives — on cold boot the page now stays on the pulsing "Detecting network…" chip instead of pointing operators at addresses that don't exist yet. - Neutralize the 403 copy (IP allow-list / internal-auth rejections are not fixable by re-auth) and drop the misleading "Sign in" CTA in favour of "Back to dashboard". - Reverse the dashboard CTA URL through {% url %} instead of a hard- coded "/". - Add tests/test_error_pages.py covering all four templates via Django's default 4xx/5xx handlers, so a broken extends chain or missing static reference can't slip through to a real outage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tests): satisfy mypy + ruff format on test_error_pages.py - Add type annotations on the rf fixture and parametrized callable view argument so mypy stops warning on the no-untyped-def rule. - Hoist the import + run ruff format to drop the in-function import and tidy the layout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review): second Copilot pass on splash/login/error pages - Splash drops the unused tailwind.css fetch (no Tailwind utility classes on this page) and adds a viewport meta tag so phones scanning the QR don't fall back to the 980 px layout viewport. - _error.html no longer extends base.html: that was pulling vendor.js on every public 404 and opening a /ws connection on crawler/bot traffic. Standalone shell loads only tabler-icons + anthias.css. - .splash-qr swaps `aspect-ratio: 1/1` for an explicit `width = height = clamp()` pair, since aspect-ratio is Chromium 88+ and the Qt5 webview on Pi 1-4 is older — without the fallback the QR slot would collapse on those engines. - views.login now honours `next` on POST through url_has_allowed_host_and_scheme so safe same-host destinations are preserved; off-host and scheme-mismatched values fall back to the dashboard so the login flow can't be turned into an open redirect. - Add 403_csrf.html so Django's CSRF failure view (csrf_failure) brands the page instead of falling back to the stock plain-HTML. - Convert multi-line {# #} comments to {% comment %} blocks where they were leaking into the rendered output. - Add tests/test_login_view.py (5 cases: GET render, next round-trip, safe-next redirect, off-host rejection, error round-trip), bundle resolution check in test_splash_page.py, and 403_csrf in the parametrized error-template suite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tests): split splash bundle test — finder check is integration-only The unit-test job runs before bin/prepare_test_environment.sh, which is what invokes \`bun run build\`. Asserting finders.find() resolves dist/js/splash.js in the unit suite was therefore guaranteed to fail in CI even though the test passes locally (where the bundle exists). Split into two: the template-reference check stays in the unit suite (catches a dropped <script> tag), and the build-artifact check moves to @pytest.mark.integration so it runs after the build step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sonar): clear 7 PR-new findings, restore Quality Gate - splash.ts: prefer globalThis over window (S7764) and split render() into showEmpty/showStatusReady/paintIpPills/paintQr to drop cognitive complexity from 20 to under the 15 threshold (S3776). - splash-page.html inline script: globalThis.__splashIpsUrl (S7764). - test_login_view.py: centralize the test password literal behind a module-level _FIXTURE_PASSWORD constant with a single NOSONAR suppression — same pattern test_splash_page.py uses for IP literals — instead of three S2068 hits (one per call site). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review): third Copilot pass — auth next, login WS, splash overflow - BasicAuth.authenticate accepts the request and appends ?next=<full_path> when redirecting to /login, so deep links from /settings/, /system-info/, etc. round-trip the operator's original destination through the form. NoAuth.authenticate keeps the no-op behaviour. authenticate_if_needed threads the request through. - login.html no longer extends base.html — same justification as the earlier _error.html change: base.html pulls vendor.js, which opens /ws on every render. login is a public pre-auth surface and has no business holding a WebSocket. Standalone shell mirrors _error.html. - .splash-page drops `overflow: hidden`. The splash is opened from phones (QR scan) as well as the device webview; locking scroll hides the IP pills + QR when content spills past the viewport on smaller screens / larger system text. - Add two test_auth.py cases covering the new ?next round-trip and the request-threading through authenticate_if_needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review): only carry next for safe GET navigations BasicAuth.authenticate previously attached ?next=<full_path> for every unauthenticated request. Two cases that broke: - Non-GET methods (POST/PUT/PATCH/DELETE): the post-login redirect bounces the browser back as a GET, hitting routes that only accept the original method → 405. Drop next for unsafe methods. - htmx partial endpoints (HX-Request: true): the dashboard polls fragments such as assets_table_partial every 5s. A session expiry mid-poll would otherwise serialize the partial URL into next, landing the operator on a bare table fragment after sign-in instead of the parent page. Detect via the HX-Request header and drop next. Extracts the heuristic into _is_safe_login_next_source() with two new test_auth.py cases covering the unsafe-method and htmx-partial paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
96 lines
3.1 KiB
Python
96 lines
3.1 KiB
Python
"""Smoke coverage for the branded 4xx/5xx error templates.
|
|
|
|
Django only swaps in 400/403/404/500.html when DEBUG=False. The dev
|
|
test client runs with DEBUG=True by default, so a broken extends
|
|
chain, missing static reference, or context-processor-dependent tag
|
|
in 500.html would only surface during a real production outage.
|
|
These tests exercise the templates directly and via the prod handler
|
|
path.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Callable
|
|
|
|
import pytest
|
|
from django.http import HttpResponse
|
|
from django.template.loader import get_template
|
|
from django.test import Client, RequestFactory, override_settings
|
|
from django.views.defaults import (
|
|
bad_request,
|
|
page_not_found,
|
|
permission_denied,
|
|
server_error,
|
|
)
|
|
|
|
_ERROR_TEMPLATES = [
|
|
'400.html',
|
|
'403.html',
|
|
'403_csrf.html',
|
|
'404.html',
|
|
'500.html',
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def rf() -> RequestFactory:
|
|
return RequestFactory()
|
|
|
|
|
|
@pytest.mark.parametrize('name', _ERROR_TEMPLATES)
|
|
def test_error_template_renders_without_request_context(name: str) -> None:
|
|
"""500.html in particular renders with no RequestContext (no context
|
|
processors, no `request`). All four must render with an empty
|
|
context — that's the strongest pre-prod check we can write."""
|
|
html = get_template(name).render({})
|
|
assert 'auth-card' in html
|
|
assert 'error-card' in html
|
|
# The dashboard CTA goes through {% url %}, so a successful render
|
|
# also proves the URL reverser is wired up.
|
|
assert 'href="/"' in html or 'href="/dashboard' in html
|
|
|
|
|
|
@override_settings(DEBUG=False, ALLOWED_HOSTS=['*'])
|
|
def test_404_handler_uses_branded_template() -> None:
|
|
"""End-to-end: with DEBUG off, an unknown URL hits Django's
|
|
default 404 handler, which loads our 404.html."""
|
|
client = Client()
|
|
response = client.get('/this-path-does-not-exist')
|
|
assert response.status_code == 404
|
|
body = response.content.decode()
|
|
assert 'Page not found' in body
|
|
assert 'auth-card' in body
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'view, expected_status',
|
|
[
|
|
(bad_request, 400),
|
|
(permission_denied, 403),
|
|
(page_not_found, 404),
|
|
],
|
|
)
|
|
def test_default_handler_returns_branded_body(
|
|
rf: RequestFactory,
|
|
view: Callable[..., HttpResponse],
|
|
expected_status: int,
|
|
) -> None:
|
|
"""Django's default 4xx handlers render `<code>.html` from the
|
|
project template path. This bypasses URL routing and goes straight
|
|
at the handler — useful because page_not_found et al. take a
|
|
mandatory `exception` argument that the test client can't supply."""
|
|
request = rf.get('/anything')
|
|
response = view(request, exception=Exception('test'))
|
|
assert response.status_code == expected_status
|
|
assert b'auth-card' in response.content
|
|
|
|
|
|
def test_500_handler_returns_branded_body(rf: RequestFactory) -> None:
|
|
"""500's signature differs (no `exception` kwarg) and the renderer
|
|
skips context processors, so test it on its own."""
|
|
request = rf.get('/anything')
|
|
response = server_error(request)
|
|
assert response.status_code == 500
|
|
assert b'auth-card' in response.content
|
|
assert b'500' in response.content
|