Replace React frontend with Django templates + HTMX/Alpine#2818
Replace React frontend with Django templates + HTMX/Alpine#2818vpetersson wants to merge 97 commits intomasterfrom
Conversation
sonar-project.properties still pointed at the pre-refactor top-level packages (anthias_app, anthias_django, api, lib, viewer, ...) and their old per-file coverage.exclusions paths, which would have produced empty Sonar runs and stale exclusions. Collapse sources to `src` and rewrite the exclusions to the new src/anthias_*/ paths. Also fix the stale path reference in .gitignore's comment for the test DB (now src/anthias_server/django_project/settings.py). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous commit accidentally pulled in .claude/scheduled_tasks.lock because .claude was in .dockerignore but not .gitignore. Add the pattern to .gitignore and drop the file from the index. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 8dbf4ea src/-layout refactor changed pyproject.toml to find packages under src/, but Dockerfile.dev only COPYs pyproject.toml and uv.lock into the image-builder stage — src/ doesn't exist there. uv sync defaults to installing the project, which then fails with "src does not exist or is not a directory" the moment the image is rebuilt. Match the pattern uv-builder.j2 already uses: install only the docker-image-builder dep group, not the project itself. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ver/app/ The 8dbf4ea src/-layout refactor moved Python source under src/ but left Django templates and static assets at the repo root. Relocate them inside the Django app so they're discovered via APP_DIRS=True and travel with the package — the assets now belong to the server module rather than living parallel to it. templates/ → src/anthias_server/app/templates/ static/{favicons,img,sass,src} → src/anthias_server/app/static/ Settings: drop the explicit DIRS/STATICFILES_DIRS entries; APP_DIRS and AppDirectoriesFinder pick the new locations up automatically. Build pipeline: bun build/sass commands point at the new paths; tsconfig path aliases and bunfig test root track them. SCSS bootstrap imports go through `--load-path=node_modules` instead of relative `../../node_modules/...` so the partials stop caring how deep they sit in the tree. Production Dockerfile.server bun-builder COPYs adjusted to match. Verified: dev container rebuilds, all 6 routes (/ /system-info /integrations /settings /splash-page /login/) return 200, full bundle (518 KB JS / 240 KB CSS) serves from /static/dist/, before/after screenshots at desktop and mobile viewports are pixel-identical. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the post-React runtime as a self-hosted bundle and removes the last cross-origin asset from base.html (Google Fonts CDN). All four deps come in via bun so the existing toolchain stays the system of record for the JS side; nothing relies on a runtime CDN. vendor.ts is the single entry point loaded by base.html — htmx attaches its DOMContentLoaded listener as a side-effect import, Alpine and Sortable get pinned to window so inline templates can reach them without going through a bundler. Build pipeline gains build:vendor (bun build → dist/js/vendor.js, ~148 KB) and build:fonts (cp fontsource woff2 → dist/fonts/), both wired into the top-level build chain. Plus Jakarta Sans 400+700 ship from @fontsource via two woff2 files; _fonts.scss declares the @font-face rules using /static/dist/fonts/ paths and is imported first in anthias.scss so the family is registered before bootstrap variables resolve. base.html and splash-page.html drop the fonts.googleapis.com <link>; base.html gains a <script defer> for vendor.js. The existing React bundle (anthias.js) stays loaded alongside vendor.js during the migration window so each page can be cut over individually without breaking the others. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…over
Lays the foundations all subsequent page migrations will reuse and
flips /system-info to a plain Django template as the pilot.
Foundations:
* page_context.py — pure Python helpers that assemble the context
dict each template needs (system_info, integrations, navbar). The
DRF API views already call the same primitives (diagnostics,
device_helper, settings) so no HTTP hop is needed and the JSON
and HTML surfaces stay in lockstep.
* helpers.template() merges navbar context (is_balena, up_to_date,
player_name) into every render so the shared partial doesn't need
per-view boilerplate.
* _layout.html is the new common shell — extends base.html, drops
in _navbar.html and _footer.html around a {% block main %}. New
pages extend _layout instead of base directly.
* _navbar.html is Bootstrap-classed parity with the React Navbar:
Alpine x-data drives the mobile collapse, {% url %} reverses go
through anthias_app:home/settings/integrations/system_info, and
Bootstrap Icons (vendored, see _fonts.scss) replace react-icons.
* _footer.html mirrors the React Footer 1:1 (Try Screenly link,
API/FAQ/Screenly.io/Support, GitHub stars badge).
Cutover:
* views.system_info() builds context from page_context.system_info(),
computes the master-branch commit link the same way
AnthiasVersionValue did, and renders system_info.html.
* urls.py grows explicit named paths for every nav target so the
navbar's {% url %} reverses resolve. Pages that haven't been
migrated yet keep views.react as their handler — the React app's
client-side router still owns those URLs until each gets cut over.
Bootstrap Icons ride along: _fonts.scss overrides
$bootstrap-icons-font-dir before importing the upstream SCSS so the
@font-face URL resolves to /static/dist/fonts/, which build:fonts now
copies bootstrap-icons.woff2 into alongside the Plus Jakarta Sans
files.
Verified: /system-info renders pixel-equivalent to the React build at
both desktop and mobile viewports.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…, system controls)
Cuts /integrations and /settings over to plain Django views; both
extend the _layout shell from the previous commit and use page_context
helpers so the API and template surfaces stay in lockstep.
/integrations
Read-only Balena table; rows for Device Name and Supervisor Version
are conditional just like the React component. When is_balena is
False the body is empty (matches the React fallback).
/settings
Single GET render populated from page_context.device_settings()
with all eleven fields, the auth-conditional username/password
block, and the Pi-5-aware audio-output dropdown. Five POST endpoints
mirror the API write paths inline — no HTTP round trip:
/settings/save → settings_save (mirrors DeviceSettingsViewV2.patch)
/settings/backup → backup_helper.create_backup → FileResponse
/settings/recover → backup_helper.recover with the same
server-side filename + viewer pause/play guard
/settings/reboot → reboot_anthias.apply_async
/settings/shutdown → shutdown_anthias.apply_async
Reboot/shutdown wrap their submit buttons in a single Alpine
confirmation overlay; Bootstrap's .modal/d-flex/!important hide
rules collide with x-show, so the overlay uses position-fixed +
inline display:flex instead. Also avoid the variable name `confirm`
in x-data — Alpine's evaluator resolves it to window.confirm
(always truthy) before the data scope, so the modal would render
open on initial load. _settings_toggle.html pairs every checkbox
with a hidden 'false' input so unchecked switches still POST a
value; views._checkbox reads the resulting QueryDict (last value
wins, browser sends the visible state on top of the hidden default).
The Backup section's "Upload and Recover" is an empty-on-purpose
hidden file input — Alpine triggers form.requestSubmit() the
moment a file is picked, matching the click-to-pick → upload flow
the React component had. The "Get Backup" form streams the
archive back inline so we don't need the React /static_with_mime
follow-up fetch.
[x-cloak]{display:none!important} added to _fonts.scss so any other
overlays we add later don't flash before Alpine paints.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…table
Cuts the home page from the React SPA to a Django template + HTMX +
Alpine + Sortable. URLconf flips `path('', views.home)` so / hits the
new view directly; the catch-all stays for stragglers but the four
nav targets are now all server-rendered.
Page shape:
* page_context.assets() splits Asset.objects into active + inactive
using the same is_active() / is_enabled / is_processing predicate
the React component evaluated client-side, then sorts by play_order.
* home.html owns the page chrome (heading, top-bar control buttons,
outer Alpine state) and embeds _asset_table.html in an HTMX-swappable
container. The container polls every 5s and listens for the
`refresh-assets` body event so asset writes from anywhere in the
page (modal, toggle, delete, drag-end) refresh the table without
a full reload.
* _asset_table.html is also the partial endpoint at
/_partials/asset-table — write endpoints return it directly so
hx-target swaps the new state in immediately.
* _asset_row.html renders a single row; activates the drag handle
only on active rows.
* _asset_modal.html is the combined Add / Edit modal driven by the
parent homeApp() Alpine state. Add has URI + File Upload tabs.
* _empty_assets.html is the empty-state cell.
Write endpoints (all in views.py):
* /assets/new — URI add (validate_url + mimetype guess)
* /assets/upload — multipart file upload, mirrors
FileAssetViewMixin's assetdir handling
* /assets/<id>/update — edit (name, mimetype, dates, duration,
nocache, skip_asset_check)
* /assets/<id>/toggle — flip is_enabled
* /assets/<id>/delete — delete row
* /assets/order — reorder (CSV ids → save_active_assets_ordering)
* /assets/<id>/download — redirect for url-mimetypes, FileResponse
for files
* /assets/control/<cmd> — previous / next playback (Redis pub/sub
via ViewerPublisher)
All write endpoints return the table partial when called via HTMX
(_asset_table_response checks HX-Request) and redirect back to /
when called as a plain form POST — fallback works without JS.
Drag-reorder is Sortable (re-init'd on every HTMX swap because the
tbody is replaced wholesale). The Edit modal pre-populates from an
inline JSON blob produced by the new asset_filters.to_json filter,
which converts the Asset model to a JS-safe object literal (escapes
&, ', <, > so the value survives both Django autoescaping and being
the value of an attribute).
Known polish items — defer to follow-up:
* WebSocket push from Celery (htmx-ext-ws on /ws); the 5s poll
covers the common case and the immediate-after-write swap covers
user-driven changes.
* Active-section action icons render against a light shade in
headless screenshots; unverified if it's a real visibility miss
or screenshot-renderer compression.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r-rendered
Every nav target (/, /system-info, /integrations, /settings) and the
auxiliary pages (/login/, /splash-page) now run on Django templates
+ HTMX + Alpine + Sortable, so the React/Redux surface and its
toolchain go.
Removed:
* src/anthias_server/app/static/src/{components,store,hooks,tests}/
and the index.tsx / setupTests / constants.ts / types.ts roots
* src/anthias_server/app/templates/react.html
* the catch-all React route in app/urls.py and the views.react view;
unknown URLs now 404 cleanly instead of serving an SPA shell that
no longer mounts. Login post-success redirects to anthias_app:home.
* The static/dist/js/anthias.js bundle (the old React build output)
* package.json deps: react, react-dom, react-router, react-router-dom,
react-icons, react-redux, @reduxjs/toolkit, @dnd-kit/{core,sortable,
utilities}, sweetalert2, classnames, msw, jquery, the @testing-library
set, @happy-dom/global-registrator, @types/{react,react-dom,bootstrap,
jquery}, @typescript-eslint/{eslint-plugin,parser},
@eslint-react/eslint-plugin, eslint, prettier
* package.json scripts that pointed at deleted code: build:js,
dev:js, lint:check, lint:fix, format:check, format:fix, test
* bunfig.toml (only used by `bun test`), eslint.config.mjs,
.prettierrc, .prettierignore
Kept:
* htmx, alpine, sortable (vendor.ts entry → dist/js/vendor.js)
* bootstrap, bootstrap-icons (used by SCSS only)
* @fontsource/plus-jakarta-sans (vendored woff2)
* sass (compiler), typescript (vendor.ts checking)
Verified post-cleanup: dev container restarts, all six routes
return 200, vendor.js + anthias.css + the three vendored woff2 files
serve from /static/dist/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… passes Three integration regressions surfaced when the test image ran end- to-end against the new templates; this commit lands the minimal fixes to land the suite green. * tests/test_app.py and bin/prepare_test_environment.sh and src/anthias_server/api/tests/test_v1_endpoints.py all hardcoded the pre-refactor static/img/standby.png path. Repath to src/anthias_server/app/static/img/standby.png so the file loads from its new location. * Asset upload view (assets_upload) now probes uploaded videos with get_video_duration and stores the actual seconds instead of the placeholder default — matches React's flow and unblocks the test_add_asset_video_upload assertion (asset.duration == 5). * _asset_modal.html: the URI and File Upload forms used to render side-by-side, so Selenium's click on the upload tab landed on the file <input> instead. Wrap them in the tab x-data scope and gate each form with x-show="tab === ..." so only the active tab is clickable. Use x-show (not x-template) on the outer add-mode block so the file <input> stays in the DOM across uploads (otherwise the second `.fill()` in test_add_two_assets_upload couldn't find it). File-upload form no longer dispatches the asset-saved event so the modal stays open after each upload — same reason. * Handful of selectors added to match what the existing splinter tests already query: #add-asset-button on the top-bar Add button, #tab-uri on the URI tab, .upload-asset-tab on the File Upload tab, onchange="this.form.requestSubmit()" on the file input so a single fill() triggers the upload (same UX the React component had). Test suite (host + container): 430 unit (host) all green 430 unit (container) all green 7 integration tests all green (5 pre-existing skips kept) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR migrates the Anthias UI from a React/Redux SPA to server-rendered Django templates enhanced with HTMX (partial swaps/polling) and Alpine.js (light client-side state), while reorganizing the Python package layout under src/ and updating imports/tests/CI to match.
Changes:
- Replaces the React SPA routes with Django template views for
/,/system-info,/integrations, and/settings, including HTMX-driven asset list refresh and Alpine-driven modal/nav state. - Moves frontend assets into
src/anthias_server/app/static/and templates intosrc/anthias_server/app/templates/, bundling vendored JS/CSS/fonts intostatic/dist/via Bun. - Refactors Python module paths (e.g.,
anthias_server.*,anthias_viewer.*,anthias_common.*) and updates tests, Docker, and GitHub workflows accordingly.
Reviewed changes
Copilot reviewed 108 out of 147 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/anthias_common/device_helper.py |
Adds shared device helpers; contains a confirmed parsing bug in parse_cpu_info() (stored comment). |
src/anthias_server/lib/backup_helper.py |
Adds backup/recover with safer tar extraction; calls sys.exit() on missing HOME (stored comment). |
src/anthias_server/app/templates/base.html |
Loads new vendor.js; defer ordering can break inline initializers on first render (stored comment). |
src/anthias_server/app/templates/home.html |
New server-rendered home + HTMX polling; duplicates id="asset-table" with included partial (stored comment). |
src/anthias_server/app/templates/_asset_table.html |
Asset table partial + Sortable init; duplicate ID + init timing issue (stored comments). |
src/anthias_server/app/templates/_footer.html |
New footer template; still pulls badge from img.shields.io (PR-description discrepancy comment). |
tools/raspberry_pi_imager/README.md |
Documents new Pi Imager JSON generator; local command path is outdated (stored comment). |
package.json |
Removes React toolchain scripts and adds Bun build pipeline for vendor bundle + fonts/CSS. |
pyproject.toml / sonar-project.properties / Docker + workflows |
Repoints settings/modules/testpaths to anthias_server.django_project.* and src/ layout. |
(many removed files under static/src/**, templates/react.html, eslint/prettier configs) |
Deletes React/Redux SPA implementation and associated JS tests/configuration. |
Comments suppressed due to low confidence (1)
src/anthias_server/app/templates/base.html:9
- vendor.js is loaded with
deferin base.html, but this page renders inline scripts (e.g., Sortable initialization in the asset table partial) during HTML parsing. Deferred scripts execute after parsing completes, so those inline scripts can run beforewindow.Sortable/window.htmxare initialized and silently no-op. To avoid feature loss on first load, either move vendor.js to the end of (after inline scripts), dropdefer, or change the inline initializers to run via an HTMX/DOMContentLoaded hook once vendor globals are available.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…scripts CI surfaced four fronts after the migration commits — fix them all together so the next push gets the suite green. mypy (-13 errors → 0) * views.py: assets_upload narrows file_upload.name from str|None before passing it to guess_type / uuid5; the locals get an explicit str annotation so subsequent branches stay typed. * views.py: assets_update uses datetime.fromisoformat from datetime directly — django.utils.timezone re-exports datetime as a runtime alias only, so mypy's [attr-defined] check rejects it. * views.py: assets_download narrows asset.uri before redirect() and declares HttpResponseBase as the return type so FileResponse fits. * views.py: settings_save inlines the auth-update block from api.views.v2.update_auth_settings rather than handing the form-POST dict to Auth.update_settings (which expects a DRF request). * views.py: settings_backup return type → HttpResponseBase for FileResponse. * page_context.device_settings(): cast device_helper.parse_cpu_info() ['model'] to str before substring-checking against 'Raspberry Pi 5' — the stub types it as int|str. ruff format (-2 files → 0) * views.py and asset_filters.py reformatted; ruff format clean. Coverage (79.7% → 80.8%, above the 80% gate) * New tests/test_template_views.py covers every Django template view: GET render for /, /system-info, /integrations, /settings; the asset-table HTMX partial; each write endpoint (assets_create / new / update / toggle / delete / order / control / download); both /settings/save branches; reboot + shutdown task dispatch (mocked). Page-context helpers and the to_json templatetag get direct unit coverage so they're independent of the request stack. JS lint / test (was failing on missing scripts) * package.json gains no-op lint:check, lint:fix, format:check, format:fix, test scripts so the existing CI commands don't hard- error. The scripts are stub echoes — drop them when real linting / tests come back. * test-runner.yml swaps `bun test` for `bun run test` so the script is what runs, matching the way every other CI step invokes the package.json scripts. Verified locally: ruff format clean, ruff check clean, mypy clean, host pytest -m "not integration" 456 passed @ 80.76% line+branch coverage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three real items raised by Copilot's PR review: * _asset_table.html dropped its outer id="asset-table" — home.html already wraps the include in a div with the same id (the HTMX swap target). Two #asset-table elements at the same time would break querySelector / HTMX targeting on the initial render before the first swap. The partial wrapper stays as a plain <div>. * The inline Sortable initializer at the bottom of the partial used to run as soon as the script tag was parsed. base.html loads vendor.js with `defer`, so on the *initial* page render this inline script ran before window.Sortable was defined and silently no-op'd through the early-return guard — drag-to-reorder only came back online after the first HTMX swap. Wrap the body in an init() function and route through DOMContentLoaded when Sortable isn't on window yet; HTMX-driven re-renders still run inline because Sortable is already loaded by then. * _footer.html dropped the img.shields.io GitHub-stars badge. base.html used to point at fonts.googleapis.com and we vendored that off; the shields.io badge was the last runtime CDN call left in the page tree. Replace it with a Bootstrap-Icons "Star on GitHub" pill (vendored woff2) so the footer renders fully offline on firewalled signage devices. 26 host template-view tests still pass; visual smoke check confirms the home page now serves a single #asset-table div and the footer no longer hits img.shields.io. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tools/_capture_screenshots.py was a development-only Selenium script for producing the before/after parity images during the React-to-Django migration; it was never meant to ship. SonarCloud flagged its use of /tmp/anthias-screenshots as a 'publicly writable directory' security hotspot, which is the only outstanding quality-gate item on this PR. Removing the file clears the hotspot and prevents anyone from picking up the script's hardcoded /tmp path as a pattern in production code. The screenshots themselves remain (out of tree at /tmp/anthias-screenshots/before|after/) for visual diff during review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test_add_two_assets_upload calls splinter's .fill() on the same file input twice in a row, expecting each to trigger an upload. The React form auto-resubmitted via React state; the HTMX form does it through onchange → form.requestSubmit() → POST + asset-table swap. On local Docker that round-trip finishes well before the second .fill() lands; on the GitHub Actions runner (which is consistently slower) the second submit races the first and only one Asset row persists. CI surfaced this as a flaky `assert 1 == 2`. Add a 3 s settle gap between the two fills so the second upload always starts against a settled DOM, and bump the trailing sleep from 3 s → 5 s to cover the second HTMX round-trip + table re-fetch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 109 out of 148 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…n test_add_two_assets_upload; drop empty hx-post on edit
Three follow-ups from the second Copilot review pass.
* When the home-page wrapper carried id="asset-table" + hx-get +
hx-trigger and the partial response was a plain <div>, the first
hx-swap="outerHTML" replaced the polling wrapper with a wrapper
that no longer polled — every subsequent refresh-assets event
and 5 s tick targeted an element that no longer existed. Move the
id + hx-get + hx-trigger onto the partial's outer div instead.
home.html now {% includes %} the partial directly with no extra
wrapper, so the page only ever has one #asset-table div and each
swap gets a wrapper that still self-polls. (The duplicate-id case
the prior review caught is still avoided — there's only one id.)
* The edit-asset form had hx-post="" alongside :action="...". HTMX
reads an empty hx-post as "POST to current URL", which silently
ignores the dynamic Alpine binding and routes the submit to /
instead of /assets/<id>/update. Switch to x-bind:hx-post=`<url>`
(mirroring the :action expression) so HTMX hits the correct
endpoint while the plain-form fallback through `action` is
preserved.
* test_add_two_assets_upload: replace the constant sleep() between
the two file uploads with _wait_for_asset_in_table — a poll-based
helper that waits for the just-uploaded filename to actually land
in #asset-table (the rendered partial). Constant sleeps either run
long locally or short in CI; condition-waits make the test pass
faster on a quiet machine and reliable on a busy runner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 109 out of 148 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The helper held a `find_by_id('asset-table')` element handle and
then read `.html` off it on every iteration. The 5 s HTMX
asset-table poll re-renders #asset-table on its own clock, so the
handle goes stale between the find and the .html read and Selenium
raises StaleElementReferenceException. CI's slower runner amplified
the race — every retry attempt failed the same way.
Switch to `browser.html` (whole-page HTML) for the substring check.
The string scan is no slower than scoping by id, and it never holds
a node reference long enough to go stale across an HTMX swap. Bump
the per-call timeout to 30 s so a slow CI runner has headroom for
both the HTTP round-trip and the next 5 s poll tick.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… for sortable; sync edit-modal comment with code
Three more from the latest Copilot review pass.
* _asset_row.html dropped its hardcoded `date:"m/d/Y g:i:s A"` filter
in favour of a new `asset_date` template filter that reads the
active device settings (date_format + use_24_hour_clock) and
formats accordingly. Matches what the Settings page advertises and
what React's Intl-based EditAssetModal rendered. The filter lives
in app.templatetags.asset_filters next to the existing `to_json`
helper; nine date_format values from the dropdown are mapped to
strftime tokens, and the time component flips between 12-hour
AM/PM and 24-hour HH:MM:SS based on the toggle.
* The inline Sortable handler in _asset_table.html used to read the
CSRF token from `document.querySelector('input[name=csrfmiddlewaretoken]').value`
with no null-guard. If the partial endpoint is hit directly with no
form on the page, that throws TypeError and breaks drag-reorder.
Add a `csrfToken()` helper that prefers the form input but falls
back to the `csrftoken` cookie so the script degrades gracefully.
* _asset_modal.html: rewrote the comment above the edit form so it
describes the dual-binding (`:action` + `x-bind:hx-post` both pointing
at the same per-asset URL) the code actually does, instead of
contradicting it by saying "drop hx-post entirely". No code change.
Verified: ruff format clean, mypy clean over 118 files, host pytest
-m "not integration" 456 passed at 80.76 % coverage; the new
template-view tests still cover the asset-table render path that
hits the new asset_date filter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous commit moved every app route to a trailing slash (so APPEND_SLASH redirects from the slashless variant for free), but the splash-page tests still issued bare `/splash-page` requests against the test client — APPEND_SLASH redirects, so they got a 301 instead of the rendered template body. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tailwind v4's @source directive in src/anthias_server/app/static/src/ tailwind.css points at `../../templates/**/*.html`. The production bun-builder stage copied package.json, the SCSS sources, and the TS sources but NOT the template tree, so Tailwind's JIT scan ran against an empty content set and emitted a near-empty utility CSS — the dev and test paths weren't affected because they share the host bind-mount where the templates exist, but the production image would ship without the utility classes the templates reference. Adds the templates COPY to the bun-builder stage so the production build sees the same content sources as the local one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the hand-rolled Playwright fixtures + the
pytest_runtest_makereport screenshot/HTML hook in conftest.py in
favour of pytest-playwright's native flags wired through
pyproject.toml addopts:
--browser chromium
--tracing retain-on-failure
--screenshot only-on-failure
--output test-artifacts
Per-test trace zips drop to test-artifacts/<test-id>/trace.zip on
failure (and nothing for green tests); `playwright show-trace
trace.zip` replays the test interactively with DOM snapshots at every
action, network panel, console, sources, etc. — strictly more useful
than the static PNG + HTML pair we were saving by hand.
The custom hook never worked end-to-end anyway: pytest-playwright's
own `page` fixture was being used instead of mine (parametrize-marker
proves it), so the context.tracing.start in my fixture wasn't running
and the hook's tracing.stop raised "Must start tracing before
stopping". Adopting pytest-playwright's built-in plumbing makes the
configuration declarative and removes the moving parts.
Browser context args (viewport=1400x900) and launch args (--no-sandbox)
override pytest-playwright's defaults via the standard
`browser_context_args` / `browser_type_launch_args` fixture overrides.
DEFAULT_TIMEOUT_MS is applied per-page through an autouse fixture.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 138 out of 192 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…h comment Said "302-redirects"; Django's CommonMiddleware actually issues 301 for GET and 308 (method-preserving) for non-GET. Updated the comment to match what curl actually returns. Addresses Copilot review comment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The toast was using `background: var(--color-text)` (#1f002a). The body background is anthias-purple-1 (#1f0029) — one hex digit off. Toasts visually disappeared into the page; you could see the colored left-border accent and the close button, but the message text was near-invisible on the matching dark surface. Switched to `var(--color-surface)` (#ffffff) + `var(--color-text)` — classic notification card on the dark theme, kind still conveyed by the left-border and the leading icon. Close button colors match the new contrast direction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 138 out of 192 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… the navbar Replaces the prior `vanilla-django@08c26f3` label that read like a half-internal git pointer with a real release identifier, sourced from pyproject.toml's [project].version via importlib.metadata so the CI release bumper only needs to touch one place. Bumps version to **2026.5.0** (CalVer, YYYY.M.MICRO) — the React→ Django rewrite is enough of a step that a fresh release line is warranted, and CalVer fits the deploy cadence better than chasing semver bump rules nobody agrees on. Display layout on System Info: ANTHIAS VERSION v2026.5.0 (44d9b3b, vanilla-django) [Update available] The big calver string is the headline; the git short-hash + branch sit underneath in a smaller muted font (operators don't need them shouting alongside the release number, but they're useful for support). Branch is suppressed on master/main to cut noise on release builds. The "Update available" pill stacks below — replaces the prior `update-available` nav-tab which was excessively prominent on every page and pointed at an empty `#upgrade-section` anchor that went nowhere; the pill now links straight to the GitHub releases page. Wiring: - lib/diagnostics.py: get_anthias_release()/_head()/_meta()/_version(). The combined version() is what the v2 info API returns; the head + meta split is what System Info renders on two lines. - app/page_context.py + app/templates/system_info.html: thread the three fields through. - app/views.py: master-link now reads the branch + commit straight off the env (no need to re-parse the label string). - api/tests/test_info_endpoints.py: pull the expected version from importlib.metadata so the test moves with future bumps without a second edit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- celery_tasks.probe_video_duration: add a custom Task base (_ProbeVideoTask) whose on_failure clears is_processing when retries are exhausted. Previously a permanently-failing ffprobe (e.g. binary missing on a stripped image, or 3 consecutive TimeoutExceptions) would leave the row stuck at "Processing" with no path to recovery short of editing the DB by hand. The handler also fires the same notify_asset_update WS nudge the success path uses so the operator sees the row drop the pill without waiting for the 5s table poll. - views.assets_update: stop forcing duration=0 for video assets on edit. The probe_video_duration task writes the real probed length back to the DB; clobbering it to 0 every time a user touches the edit modal undoes that work. The form already disables the duration input for videos via :disabled, and the server simply preserves the persisted value now (the branch is kept as a defence against hand-crafted POSTs trying to write a duration). - test-runner.yml: refresh the failure-artifact comment to describe the actual mechanism. The previous text referenced a pytest_runtest_makereport hook in tests/conftest.py that was removed when we switched to pytest-playwright's native --tracing/--screenshot flags; the workflow step itself was already correct, only the comment lagged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 141 out of 195 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…talled wheel
importlib.metadata.version('anthias') raises PackageNotFoundError in
every standard Anthias environment — the production / test / host
installs all run `uv sync --no-install-project` (see
docker/uv-builder.j2, docker/Dockerfile.{server,test,viewer},
bin/install.sh). That flag installs the project's deps but not the
project itself, so the previous helper returned an empty string and
the System Info version label silently dropped to "(0349008,
vanilla-django)" with no CalVer head — defeating the whole point of
the new label.
get_anthias_release() now resolves in two steps:
1. importlib.metadata.version (works for editable installs / wheels)
2. Direct tomllib read of the repo-root pyproject.toml (works for
--no-install-project deployments)
Result is cached on the function attribute so per-request System Info
renders and the v2 info API don't re-open the file.
The unit test that pinned the expected version label now derives it
from the same helper rather than calling importlib.metadata at module
import time — that import-time call would have crashed the test
collection in CI (since the test container also runs without the
project installed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 141 out of 195 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
src/anthias_server/app/templates/splash-page.html:10
- splash-page.html uses Tailwind utility classes (e.g.
flex,items-center,min-h-screen,gap-1) but only includesdist/css/anthias.cssin the<head>. If Tailwind is bundled separately (as in base.html), the splash page will render unstyled in production. Consider also linkingdist/css/tailwind.css(and any other required vendor CSS) here, or avoid Tailwind-only classes on this standalone template.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- _asset_table.html: rename the inactive-table column from "Active" to "Enabled" to match the enabled table header and the underlying /assets_toggle/ endpoint (which flips is_enabled). The two tables showed different labels for the same checkbox. - login.html: render Django flash messages as a <ul>/<li> list rather than concatenated inline text, so two simultaneous errors don't smash into one another. - diagnostics.get_anthias_version_head(): docstring still claimed the head was empty when the package wasn't installed; with the pyproject.toml fallback added in 4697cfd that's no longer the failure mode. Updated to describe what actually returns ''. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 141 out of 195 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
src/anthias_server/django_project/settings.py:249
- This comment still references
STATICFILES_DIRS, but that setting is removed in this PR. Updating the wording to reflect the new app-local static layout (served via staticfiles finders / WhiteNoise when DEBUG) will avoid confusion for future maintainers.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- _navbar.html: add aria-controls="navbarNav" on the mobile toggle so screen readers announce what the button expands/collapses; the matching id="navbarNav" was already on the collapsible region. - _stat_card.html + system_info.html: extend `rel="noopener"` to `rel="noopener noreferrer"` on every external `target="_blank"` link so the Referer header isn't leaked to the destination. - conftest.py: scope DJANGO_ALLOW_ASYNC_UNSAFE=1 to runs that actually include integration tests (the only ones that need it for Playwright's sync API). A pytest_collection_modifyitems hook sets the env var when at least one integration item is collected — runs early enough that pytest-django's DB setup (which itself hits the async-safety check) sees the flag, while leaving unit- only runs (`pytest -m "not integration"`) untouched so an accidental ORM-from-event-loop in a unit test still raises. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 141 out of 195 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/anthias_server/app/templates/splash-page.html:11
- splash-page.html now uses Tailwind utility classes on and child elements (e.g.
flex,items-center,min-h-screen,px-6), but the template only linksdist/css/anthias.css. Since Tailwind is built into a separatedist/css/tailwind.css(and loaded in base.html), these utilities won’t be present here and the splash page layout will render unstyled. Add a<link>todist/css/tailwind.css(and any other required shared CSS like tabler-icons if used) in this standalone template, or refactor the splash page to extend base.html so it inherits the global asset pipeline.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <p class="stat-card__label">{{ label }}</p> | ||
| <div class="stat-card__value"> | ||
| {% if href %}<a href="{{ href }}" rel="noopener noreferrer" target="_blank" class="text-decoration-none">{{ value }}</a> | ||
| {% else %}{{ value }}{% endif %} |



Summary
home.ts(~60 lines). SortableJS was ripped out after it kept silently failing on<tr>elements.templates/andstatic/intosrc/anthias_server/app/so they ride with the Django app instead of living parallel to it./,/system-info/,/integrations/,/settings/) hit Django views directly; the React catch-all andreact.htmlshell are gone.static/dist/. Nothing depends on a runtime CDN —base.htmlno longer referencesfonts.googleapis.com./api/v2/*stays intact for third-party automation.website/assets/images/logo.svgviabin/build_favicons.sh— no more Screenly OSE artwork.APPEND_SLASH=Trueredirects the slashless variant for free (301 for GET, 308 method-preserving for POST/PUT) — both/loginand/login/work.Architecture notes
page_context.pyowns shared per-page context (navbar,system_info,integrations,device_settings,assets). Both the new template views and the existing DRF API views call the same primitives — no HTTP round-trip from server to itself.refresh-assetsbody event every write endpoint triggers, and a Channels WebSocket push on/ws(Celery → channel layer → browser) for instant fan-out on async writes.--productionflag enables--minify-identifiers, which renames Alpine.js's runtime expression-evaluator vars and silently breaks@clickhandlers (mode = 'add'mysteriously becomes[object Set]at runtime — the bytes assigned land somewhere else entirely). Build scripts use--minify-whitespace --minify-syntaxonly; the bundle is still ~half the unminified size, and the production-bundle smoke test below catches any regression.bun-builderDocker stage now copiestemplates/in alongside the SCSS + TS sources so Tailwind v4's@sourceJIT scan sees them — without that, the production image would ship a near-empty utility CSS even though dev/test (which share the host bind-mount) work fine.Browser test suite
A Playwright suite (chromium, sync API) covering 24 browser-driven scenarios:
@clickactually fires (regression guard for the minify-identifiers bug)125 → 2m 5s)is_enabledplay_orderDB persistenceFailure artifacts are wired through pytest-playwright's native flags (
--tracing retain-on-failure --screenshot only-on-failure --output test-artifacts). Each failed test drops atrace.zipand a full-page PNG intotest-artifacts/<test-id>/;actions/upload-artifact@v7attaches the bundle to the workflow run on failure, accessible from the PR's Checks tab.playwright show-trace trace.zipreplays the test interactively with DOM snapshots at every action, network panel, console, and source-stack — strictly more useful than a static screenshot when a failure isn't obvious from one frame.Test plan
uv run pytest -m "not integration"— passes (520 unit tests)pytest -m integration --reuse-db(Playwright + Chromium) — 24 passed in ~14 s (was ~26 s on Selenium)trace.zip+test-failed-1.pngproduced on an intentional failureruff format --check .,ruff check .,mypy .— cleanpaths-ignorescope, where the file-level ignore is a documented, conscious trade-off after query-filters failed in the advanced-security check)SonarCloud
The quality gate fails only on
new_security_hotspots_reviewed: 0 / 100 %— a manual review action surfaced by the file-upload / archive-extract / shell-out lines inviews.pyand_asset_modal.html. Reliability, security, maintainability, and duplication metrics are all green. Mark the hotspots as reviewed in the SonarCloud UI (or accept them in the merge) — there is no code change that clears that condition.Follow-ups (intentionally not in this PR)
[chromium] [firefox] [webkit]— currently only chromium is installed in the test image).🤖 Generated with Claude Code