mirror of
https://github.com/Screenly/Anthias.git
synced 2026-06-10 09:08:09 -04:00
da53e045a6801b5056fb448e68e55da7bf106913
174 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
10c68b26cc |
feat(viewer,build,balena): add arm64/Qt6 pi3-64 board and the Rock Pi 4 fleet; keep 32-bit pi3 as legacy (#2985)
* feat(viewer,build): add arm64/Qt6 pi3-64 board; keep 32-bit pi3 as legacy Revises issue #2906 Phase 2. The original plan (delete the Qt 5 toolchain, force Pi 2/Pi 3 onto Qt 6) is abandoned: Qt 5 was fixed up on master and stays. Instead, add a NEW board target `pi3-64` — a 64-bit (arm64) Qt 6 viewer image for Raspberry Pi 3 hardware on a 64-bit OS — as its own image stream, disk image, and balena fleet. The legacy 32-bit armhf/Qt5 `pi3` board is left untouched and flagged as legacy/maintenance. pi3-64 mirrors the existing `pi4-64` path (Qt 6, eglfs_kms; video played in-process by AnthiasViewer's QtMultimedia pipeline — QMediaPlayer + the ffmpeg/libavcodec backend with V4L2 HW decode, no external player). VideoCore IV is H.264-only HW decode. Board selection is by `uname -m`: a Pi 3 on a 64-bit OS gets `pi3-64`, a 32-bit OS keeps `pi3` (the model string is identical on both arches). - image_builder: pi3-64 build params (arm64) + is_qt6; constants. - Dockerfile.viewer.j2 + start_viewer.sh: pi3-64 shares the pi4-64 eglfs KMS path; renamed board-agnostic eglfs-kms-pi4.json -> eglfs-kms.json. - Detection: install.sh / upgrade_containers.sh (aarch64 Pi 3 -> pi3-64). - Runtime: media_player force_mpv set (selects MPVMediaPlayer, the QtMultimedia D-Bus shim); processing codec grid {'h264'}. - CI: docker-build matrix + mirror-latest-tags. - Balena (fleet screenly_ose/anthias-pi3-64, device type raspberrypi3-64): disk-image + manual-deploy workflows, balena_ota_deploy.sh, balena_fleet_maintenance.py, balena_unpin_devices.py, deploy_to_balena.sh, balena-host-config.json. - Pi Imager: SUPPORTED_BOARDS += pi3-64 (non-maintenance); pi3 stays legacy. - Docs + tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(website): link the Pi 3 (64-bit) bullet like its siblings Copilot review: the list is introduced as 'links to the images', so the new pi3-64 entry should be navigable like the surrounding bullets. Link the label to the release-images section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(balena): add the Rock Pi 4 fleet (screenly_ose/anthias-rockpi4) Wires the anthias-rockpi4 balena fleet (device type rockpi-4b-rk3399) into the OTA deploy + disk-image pipeline. The fleet has no board-specific image build: it runs the generic arm64 containers, so bin/balena_ota_deploy.sh / bin/deploy_to_balena.sh map the rockpi4 board to the <short-hash>-arm64 image tags (and strip the /dev/vchiq mount — no VideoCore on RK3399), and the disk-image preflight verifies the arm64 images exist. Root-cause fix for the fleet's codec gate: balena ships no anthias_host_agent service, so host:board_subtype was never published and resolve_device_key() stayed 'arm64' — whose HW-decode set is empty, rejecting every video upload. The model-string → subtype table moves to the dependency-free anthias_common.device_helper.detect_board_subtype (single source, imported by host_agent), and anthias_common.board.get_board_subtype now falls back to reading /proc/device-tree/model in-container when Redis has no value. The device tree is kernel-global — the same mechanism get_device_type has always used for Pi detection — so the rockpi4 fleet resolves its {h264, hevc} envelope without a host-side daemon, and compose installs whose host_agent died self-heal too. - build-balena-disk-image.yaml: rockpi4 in both matrices, fleet + rockpi-4b-rk3399 image cases, arm64 images in the preflight check. - deploy-balena-manual.yaml: rockpi4 board option. - balena-host-config.json: rockpi4 declared {} (config.txt is RPi-only; the reconcile hard-fails on a missing key). - balena_fleet_maintenance.py / balena_unpin_devices.py: fleet added. - tests: get_board_subtype Redis-first + device-tree-fallback order; detect_board_subtype patch targets follow the move. - docs: board-enablement, balena-fleet-host-config, installation-options. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1f438d2af0 |
perf(viewer): render video via QML VideoOutput in a QQuickWidget (#2975)
* perf(viewer): render video via QML VideoOutput in a QQuickWidget - replace the QGraphicsVideoItem-on-raster-QGraphicsView substrate: QVideoFrame::toImage did an RHI offscreen render + GPU->CPU readback per frame, capping presentation at 8.3 fps (Pi 4) / 10-12 fps (Pi 5) with a saturated GUI thread while HW decode ran fine (issue 2967). Validated on both testbeds: Pi 4 30.0 fps presented at 64% total CPU, Pi 5 26.6 fps at 13-35% - VideoOutput keeps frames on the GPU: scene-graph textures with shader YUV->RGB, composited through the same QQuickRenderControl FBO machinery QWebEngineView already uses (eglfs-safe, inherits whole-screen rotation -- re-validated under QT_QPA_EGLFS_ROTATION) - log frames-rendered (QQuickWindow::afterRendering) next to frames-delivered in playback-stats so presentation-side drops are visible -- the sink-only counter is how the 8 fps regression shipped unnoticed; connection is retried from play() so the counter can't silently stay dead - fail hard (qFatal) when the QML scene is unavailable instead of decoding video to nowhere: crash-respawn is supervised and loud, a silent black-screen kiosk is not - video-rotate maps to VideoOutput.orientation (still a defensive no-op; every platform rotates the whole screen) - ship qt6-declarative-dev + qml6-module-qtquick/-qtmultimedia in the Qt6 viewer images; drop the now-unused multimediawidgets - run the C++ tests with QT_QUICK_BACKEND=software so the QML scene loads under the offscreen platform Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(image-builder): align gstreamer-drop version comment to Qt 6.5 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
94814958a3 |
fix(viewer): play pi2/pi3 video via GStreamer HW pipeline (replace VLC) (#2972)
- Replace VLCMediaPlayer with GstFbdevMediaPlayer: spawn `gst-launch-1.0 playbin` with a fully-hardware video sink — v4l2h264dec (bcm2835 codec) decodes, v4l2convert (bcm2835 ISP) HW-scales + converts YUV to the framebuffer format, fbdevsink paints /dev/fb0. No DRM master / X / Wayland needed, which a bare uid-1000 viewer with no compositor cannot acquire. - Restores hardware video on the Qt5 linuxfb boards after #1980 dropped the Broadcom mmal_vout; drives the same VPU + ISP silicon mmal used. Measured on a Pi 3: 1080p30 -> rgb565 at ~40 fps, zero dropped frames. (An interim ffmpeg -> fbdev approach was abandoned: HW decode worked but CPU YUV->rgb565 convert via swscale managed only ~6 fps — no NEON path — and CPU scaling is unaccelerated. v4l2convert moves that to the ISP.) - playbin gives container-agnostic demux, auto-plugged HW decoder, and graceful optional-audio. Loop the slot via re-launch-on-EOS; kill by process group so no gst-launch orphan keeps the framebuffer. - Rotation rides `videoflip`, inserted only when the panel is rotated. - Add gstreamer1.0-{tools,plugins-base,plugins-good,plugins-bad} to the Qt5 viewer image; drop the now-unused vlc apt package + python-vlc dep (+ its mypy override). Sync the upload-gate comment in processing.py. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6b3f638c60 |
fix(viewer): retry AnthiasViewer spawn so armv7 WebEngine-init crash self-heals (#2969)
* fix(viewer): skip the Writeback connector in the eglfs headless guard #2962 added wait_for_eglfs_display so a screenless eglfs (Pi 4) board waits for a display instead of crash-looping on Qt's "no screens available". eglfs_has_display() treated any connector status other than "disconnected" as a present display (to tolerate bridges that report "unknown"). balenaOS 2026.x exposes a KMS `card0-Writeback-1` virtual connector that ALWAYS reports "unknown". On a headless Pi 4 (both HDMI ports "disconnected") the writeback connector's "unknown" satisfied the guard, so it skipped the wait, launched eglfs, and the viewer crash-looped on "no screens available" / "AnthiasViewer exited before emitting D-Bus handshake" — exactly the failure #2962 was meant to prevent. Confirmed on multiple live pi4 on 2026.1.0 (card0-HDMI-A-1/-2 = disconnected, card0-Writeback-1 = unknown). Skip `*Writeback*` connectors so only real display outputs (HDMI/DSI/DP/…) count. A genuinely headless board now waits gracefully; the bridge-"unknown" hedge is preserved for real connectors. Verified locally for headless, connected, and bridge-unknown layouts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(viewer): retry AnthiasViewer spawn so armv7 WebEngine-init crash self-heals - Wrap the AnthiasViewer launch in a capped exponential-backoff retry loop (BROWSER_SPAWN_MAX_ATTEMPTS) instead of raising on the first failed handshake - Convert the tight container restart loop on Pi 2/Pi 3 into an in-process retry that self-heals on a later launch - Publish viewer:webview_status to Redis (retrying/failed) so a stuck board is distinguishable from an empty playlist - Add WebviewLaunchError + _spawn_webview_once helper; throttle repeat warnings to avoid flooding journald - Cover retry-then-succeed and exhaust-then-raise paths in tests - Document the armv7 WebEngine-init crash + retry stop-gap in docs/board-enablement.md The 32-bit Qt5 viewer intermittently aborts during Chromium/WebEngine init (malloc(): unaligned tcache chunk detected) ~75-90% of launches; reproduced on a 64-bit Pi 3B+. No userspace mitigation fixes the corruption, but a fresh launch clears it ~10-25% of the time, so retrying catches a good launch within a few attempts (validated on-device: handshake on attempt 6). Clean fix is arm64/Qt6 on 64-bit OS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(viewer): address review — bound status-beacon Redis, funnel CommandNotFound - Give the webview health beacon a dedicated Redis client with short socket timeouts (connect_to_redis gains opt-in timeout params, defaulting to the historical blocking behaviour) so a Redis stall can't hang viewer startup inside the spawn-retry loop - Wrap sh.CommandNotFound into WebviewLaunchError in _spawn_webview_once so a missing binary is reported + handled on the same path as every other launch failure instead of escaping the retry loop - Reword the board-enablement note so it describes the WebEngine-init observation without referencing a --no-sandbox flag the viewer doesn't receive - conftest: accept the new connect_to_redis kwargs in the fake factory Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(viewer): chain final launch error; harden second redis test patch - Chain the exhausted-retries WebviewLaunchError from last_error so the traceback preserves the underlying failure (timeout / early-exit / wrapped CommandNotFound) - conftest: the autouse _mock_redis fixture's connect_to_redis patch now accepts *args, **kwargs too (matches the import-time patch) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(viewer): scope spawn-retry by call site; drop write-only status beacon Addresses self-review findings on the retry mechanism: - Mid-playback respawn (view_image/view_webpage, on the asset_loop thread) now uses a small, short budget (BROWSER_SPAWN_INLINE_*) so a persistent crash can't freeze the loop (no rotations/skips/standby, watchdog starved) for minutes; startup keeps the generous budget. A persistent mid-run failure raises and the container restart re-rolls. - Permanent failures (missing binary) raise WebviewBinaryMissingError and short-circuit the retry instead of burning the full backoff budget. - _spawn_webview_once now reaps the terminated process (SIGTERM, wait, SIGKILL) on the handshake-timeout path so a retry can't overlap two AnthiasViewers contending for the framebuffer / D-Bus name. - Reset the stale `browser` global before re-spawning. - Poll spawned process every 0.25s (was 1s) so a fast init crash is noticed promptly in the retry loop. - Drop the write-only viewer:webview_status Redis beacon (no reader existed) and revert the connect_to_redis timeout-param widening + conftest churn; operator-visible status is the throttled log output. - Tests: cover early-exit, terminate-on-timeout, missing-binary short-circuit, backoff growth, and the inline budget cap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(viewer): clamp max_attempts to >=1; correct retry-logging comment - Guard load_browser against a non-positive max_attempts (would skip the loop and raise a confusing "0 attempts; last error: None") - Reword the comment: the first failure logs its reason AND a retry line, so it's not literally "one log line per attempt" Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(viewer): clamp backoff_cap and startup_timeout in load_browser - A backoff_cap below 1s would devolve into a tight retry loop; a negative one would make sleep() raise ValueError mid-retry and mask the real launch error - Clamp a negative startup_timeout to 0 (immediate-timeout attempt) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cca69b594d |
fix(balena): repair Pi 4 graphics overlay + manage fleet host config as IAC (#2947) (#2949)
* fix(viewer): detect Pi 4 eglfs DRM card at runtime so boot doesn't hang (#2947) - vc4-drm (display) and v3d (render-only) race during probe, so the display node is card0 on some boots/images and card1 on others - #2905 hardcoded /dev/dri/card1; when vc4 loses the race eglfs opens the render-only node, finds no connectors, and the device hangs on the balena splash forever - start_viewer.sh now picks the card that owns connectors at runtime and rewrites QT_QPA_EGLFS_KMS_CONFIG before launch - prefers a connected connector, falls back to any card exposing connectors (excludes the connector-less v3d node) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(balena): repair Pi 4 graphics overlay, manage fleet host config as IAC (#2947) Root cause of the Pi 4 boot-splash hang: the anthias-pi4 fleet's dtoverlay was stored as the malformed value `"vc4-kms-v3d"` — literal double-quotes, on the legacy RESIN_ prefix — unlike every other board's clean `vc4-kms-v3d`. The quotes stop the firmware loading the overlay, so the Pi 4 fell back to firmware-KMS; since #2905 the viewer renders through Qt eglfs_kms, which needs full KMS, so the display never came up and the device hung on the splash. (linuxfb, used before #2905, didn't care, which is why this surfaced now.) The malformed value was a manual dashboard edit — the config was never codified. - add balena-host-config.json: declarative per-board config.txt knobs, reconciled from the live fleets and corrected (clean pi4 dtoverlay; drop bogus `dtparam=...,vc4-kms-v3d`; standardize pi2/pi3 off the RESIN_ prefix; drop pi5 gpu_mem which a Pi 5 ignores; add cma-512 to pi5 per docs/board-enablement.md) - build-balena-disk-image.yaml reconciles each fleet to the file: upsert under the canonical BALENA_ prefix, then prune anything not in the file (incl. legacy RESIN_HOST_CONFIG_* dupes). Supervisor vars untouched. - docs/balena-fleet-host-config.md documents the mechanism + the full per-board audit; modernize the self-hosted doc's `env add`->`env set` - drop `--pin-device-to-release` from `balena preload` (added in #2098) so flashed devices track the latest stable release instead of freezing; correct installation-options.md / faq.yaml accordingly Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): harden fleet host-config reconcile against fleet-wide wipe (#2947) Review of the prune step surfaced three failure modes: - An empty desired set (absent board key, or jq/file parse failure not caught by set -e through a process substitution) made the prune delete every config var on the fleet, incl. dtoverlay=vc4-kms-v3d. Now resolve the board config with `jq -ec .boards[$b]` and hard-fail if it's null or the file is invalid; a `{}` board (x86) is truthy so it still reconciles to "no config.txt". - The prune selector `test("HOST_CONFIG")` was an unanchored substring match — a var merely containing HOST_CONFIG (e.g. BALENA_HOST_CONFIG- URATION_BACKUP) would be pruned. Anchored to `^(BALENA|RESIN)_HOST_CONFIG_`. - A transient `balena env list` / jq failure in the prune's process substitution was swallowed (pipefail doesn't propagate out of `<(...)`), silently skipping the prune and leaving stale RESIN_ duplicates. Capture the listing into a var first so the failure aborts the step. Also folds the duplicate jq pass over balena-host-config.json into the single `board_json` resolve. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(balena): describe current host config, not the one-time cleanup Replace the point-in-time "Audit" table (old quoted/broken values, was→fixed narrative) with a forward-looking per-key rationale. The cleanup history lives in the PR / git history; the doc should describe what each setting is and why. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(ci): drop the inline rationale comment from the preload step Move the explanation out of the workflow and into history (this is where `git blame` on the preload step lands): `--commit latest` seeds the image with the current release's container images so a freshly flashed device boots fully offline. We deliberately do NOT pass `--pin-device-to-release`: pinning (added in #2098) froze flashed devices to the downloaded release, so they never received OTA fixes. Anthias balena devices should track the fleet's latest stable release, so the device joins the fleet unpinned and auto-updates from here on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b57c1a16a7 |
feat(viewer,server): 1 GB SBC enablement — low-RAM degradation gates (#2915)
Boards with < 1.5 GiB MemTotal (Pi 2/Pi 3 1GB, Pi 4 1GB, Rock Pi 4 1GB, generic-arm64 1GB SKUs) OOM-cycled when QtMultimedia loaded a 4K HEVC asset alongside the two QtWebEngine renderers introduced by #2905. On-device repro on a 1 GB Rock Pi 4 confirmed `global_oom` on the container's bash process followed by a restart loop, plus the kernel keeping sshd in banner-exchange until power-cycle. This patch puts the device into a graceful degraded mode before the OOM cascade fires. * bin/upgrade_containers.sh — exports ANTHIAS_LOW_RAM=1 when TOTAL_MEMORY_KB < 1572864 (1.5 GiB), 0 otherwise. Threshold cleanly splits the 1 GB SKUs from the 2 GB+ SKUs in the supported fleet. * docker-compose.yml.tmpl — forwards ANTHIAS_LOW_RAM to the viewer container. * anthias_host_agent — publishes host:total_mem_kb to Redis alongside the existing host:board_subtype so server-side gates can read MemTotal without re-opening /proc/meminfo themselves. * anthias_common.board — adds LOW_RAM_THRESHOLD_KB, get_total_mem_kb(), is_low_ram_device() helpers. * anthias_webview (view.cpp) — when ANTHIAS_LOW_RAM=1, aliases webView2 onto webView1 so the rest of the dual-buffer logic still runs but never spawns a second Chromium renderer (~100 MB physical RAM saved per device). UX: page swap is in-place with a brief blank during load, no preloaded crossfade. * anthias_server.processing — extends the codec gate with a low-RAM 1080p resolution cap. Above-1080p uploads on low-RAM boards are rejected at upload with the existing recipe machinery, extended to inject `-vf scale=1920:1080:force_original_aspect_ratio=decrease` so the operator's re-encode lands within the envelope. If an upload fails BOTH codec and resolution gates, the codec message wins but the recipe folds in the downscale so a single re-encode satisfies both. * Diagnostics page — Memory card surfaces a "Low-RAM mode" badge with the threshold MiB so operators can see why the device degraded. /api/v2/info's `memory` field gains a `low_ram: bool` for API clients. * docs/board-enablement.md — rewrote the stale `--hwdec=drm-copy` / per-codec dispatch text (removed in #2905); documented the known rkvdec mainline limitation on Armbian 6.18 (HEVC stateless engages via `-hwaccel drm` but produces decode errors; H.264 has no v4l2_request binding in +rpt1 7.1.3) and the new low-RAM mode. Tests cover the four matrix cells (low/high-RAM × in/over-cap), the recipe shape with and without `cap_to_1080p`, the cap defence against unknown / zero dimensions, host_agent's MemTotal parser, and the API endpoint's new low_ram field. Out of scope (separate work): HEVC HW-decode on arm64 — depends on an upstream rkvdec driver fix landing in Debian-shipped Armbian kernels; Anthias does not maintain its own kernel/distro. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
57b4f25c77 |
feat(viewer,server): per-board HW decode dispatch + codec gate on upload (#2885)
* perf(viewer): pi4-64/pi5 use mpv --vo=gpu --gpu-context=drm On Pi the connector's preferred mode is usually 4K (most modern TVs report 3840x2160 in their EDID), and the previous --vo=drm path ran a CPU zimg upscale from 1080p source to that 4K output. On a 4-core A72 that's the bottleneck — mpv VO drops 59-75 frames per 30s on a stock 1080p H.264 signage clip. Pi5's A76 is faster but the same upscale path is still the limit. Switching the VO to GL with the DRM context (mpv --vo=gpu --gpu-context=drm) hands the upscale to the V3D and leaves everything else identical — mpv still owns DRM master, still reads --drm-mode=1920x1080@60 (kept), still runs in --vd-lavc-threads=4 software decode (mpv 0.40 in Debian Trixie has v4l2m2m-copy but not v4l2request, so --hwdec=auto-safe falls back to software on this asset; that hasn't changed). Measured on a 4K-connected Pi4-64 Rev 1.5, same clip, same 30 s window: --vo=drm : 59-75 vo drops / 30 s --vo=gpu --gpu-context=drm (this patch) : 3-6 vo drops / 30 s `decoder-frame-drop-count` is 0 in both — the regression was purely on the VO side, and shifting scaling off the CPU is what buys the headroom. x86 (cage + --gpu-context=wayland) is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf(viewer): drop --drm-mode pin on Pi4-64/Pi5 under --gpu-context=drm The previous commit moved Pi4-64/Pi5 to `mpv --vo=gpu --gpu-context=drm` but kept the `--drm-mode=1920x1080@60` pin from the old --vo=drm path. On-device testing showed the pin *hurts* throughput under GBM: 294 vo drops/30s with the pin, 3-6 without, on the same 4K-connected Pi4 and the same H.264 clip. The pin existed in the first place to dodge CPU zimg upscale to 4K, which the A72 couldn't keep up with on the legacy --vo=drm path. Under --gpu-context=drm the V3D does the scaling for free at the connector's preferred mode, so the workaround is no longer needed and is in fact harmful. `--vd-lavc-threads=4` stays — software decode under --hwdec=auto-safe (mpv 0.40 has v4l2m2m-copy but not v4l2request) still benefits from explicit threading. Verified on a 4K-connected Pi4-64 across H.264 (30/24 fps) and HEVC clips: 2-6 vo drops/30s in every case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(viewer): consolidate Qt6 boards onto cage + Wayland, pin Pi 4 to 1080p Folds in PR #2883: Pi 4-64 / Pi 5 now run under cage with mpv on --vo=gpu --gpu-context=wayland, joining x86 and arm64 on a single Wayland-based display stack. Drops the --vo=drm legacy path entirely from MPVMediaPlayer. Qt 5 boards (pi2 / pi3) stay on linuxfb via VLCMediaPlayer — out of scope here. Replaces the perf branch's `--vo=gpu --gpu-context=drm` standalone fix with the consolidated cage path. The previous standalone finding (3-6 vo drops / 30 s on Pi 4 at 4K) was a Pi-without-cage optimization; once Pi runs under cage like every other Qt6 board, the same trick applies via wayland but cage's composite step adds its own pass and the V3D on Pi 4 can't keep up at 4K (738 vo drops / 30 s measured at native 4K under cage). Fix: move the 1080p mode pin one layer up from app code to host config — the new ansible/.../cmdline.txt.j2 conditional appends `video=HDMI-A-1:1920x1080@60 video=HDMI-A-2:1920x1080@60` when `device_type == 'pi4-64'`. With output pinned to 1080p there's no upscale anywhere in the pipeline, matching the bandwidth profile of today's --vo=drm production setup. Pi 5 / x86 / arm64 keep the connector's preferred mode (typically 4K). Pi 5's V3D 7.1 has roughly 2× Pi 4's throughput; x86 iGPUs handle 4K via VAAPI; arm64 SBC perf varies by SoC. Other notable changes folded in from #2883: * tools/image_builder/utils.py — `cage` + `qt6-wayland` move out of the per-board branch into the shared is_qt6 block. `wlr-randr` (was x86-only) goes in the shared block too since rotation now happens via wlr-randr on every Qt6 board. `va-driver-all` stays x86-only (no VAAPI on Pi / ARM SoCs). * docker/Dockerfile.viewer.j2 — QT_QPA_PLATFORM=wayland gated on is_qt6 instead of board in ('x86', 'arm64'). * bin/start_viewer.sh — case on DEVICE_TYPE: every Qt6 board takes the cage + sudo path. Pi2 / Pi3 stay on the legacy direct-sudo path. * src/anthias_viewer/media_player.py — single --vo=gpu --gpu-context=wayland for all reachable device types. The per-board rotate_args block is gone: every Qt6 device inherits the transform from cage via wlr-randr, so mpv would double-rotate if it set --video-rotate. * tests/test_media_player.py — parametrised tests for all four Qt6 boards (x86, arm64, pi4-64, pi5) hitting the same VO path; rotation tests assert mpv *never* sets --video-rotate under cage. * website/data/faq.yaml — rotation entry points at Settings page / wlr-randr; resolution entry calls out the Pi 4 1080p pin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ansible): propagate tags into boot.yml include_tasks The `Configure boot partition` task in system/tasks/main.yml was tagged `touches-boot-partition` / `raspberry-pi` but those tags weren't propagated to the tasks inside boot.yml — Ansible's default include_tasks behaviour matches the include against --tags but leaves the included tasks tag-less, so they get filtered back out. Running `ansible-playbook ... --tags touches-boot-partition` therefore did nothing. Use the explicit `apply: tags:` form so the include's tags are copied onto each task in boot.yml. With this, the standalone "re-render boot config" workflow actually works, which matters on Pi 4 now that the 1080p HDMI mode pin in cmdline.txt.j2 needs to land without re-running the whole playbook. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(viewer): keep Pi 4 on linuxfb; only Pi 5 / x86 / arm64 go cage On-device testing on a Pi 4 Model B Rev 1.5 with a 4K HDMI display showed cage+wayland is fundamentally too heavy for the V3D 6.0: --vo=drm (existing, no cage) : 59-75 drops/30s --vo=gpu --gpu-context=drm (no cage, GPU scale): 3-6 drops/30s --vo=gpu --gpu-context=wayland (cage, even at : 730+ drops/30s, 1080p HDMI cmdline pin to avoid 4K scale) mpv at 99% CPU running ~1/4× real time The 1080p HDMI pin doesn't recover Pi 4 — cage's composite pass costs more than the V3D 6.0 has spare bandwidth for, regardless of output resolution, with the webview running in the background or not. Pi 5's V3D 7.1 has roughly 2× the throughput and is expected to keep up; x86 / arm64 already shipped on cage and remain unchanged. Net result: * Pi 4-64 stays on Qt linuxfb (no compositor) with mpv on --vo=gpu --gpu-context=drm. mpv writes straight to KMS via libgbm and lets the V3D do video scaling — keeping the standalone perf-branch finding that drops from 59-75 → 3-6 on the same clip. * Pi 5 / x86 / arm64 stay (or move) onto cage + qt6-wayland + wlr-randr with mpv on --vo=gpu --gpu-context=wayland. * Pi 2 / Pi 3 stay on the Qt5 + VLC + linuxfb track they were already on. * The Pi 4 1080p HDMI cmdline pin added in the previous commit is reverted (no longer needed without cage). * Rotation handling: mpv emits --video-rotate=N on Pi 4 (no compositor to apply the transform) and skips it on the cage boards (wlr-randr handles it there). Goal-wise this is the partial-consolidation we agreed to as last resort: three of four Qt6 boards share one Wayland stack, Pi 4 keeps the framebuffer path for as long as the V3D 6.0 + mpv 0.40 combo lacks the headroom. Pi 4 remains in scope for revisiting once mpv ships the v4l2request hwdec. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(viewer): mirror host render-GID for all Qt 6 boards, not just cage mpv uses /dev/dri/renderD128 for --vo=gpu on every Qt 6 board now — wayland (cage path on x86 / arm64 / pi5) and drm (linuxfb path on Pi 4) both go through Mesa GL. The render-GID mirror was inside the cage branch of start_viewer.sh, so Pi 4's mpv ran as viewer user, hit the render node owned by GID 992, got "Permission denied", and bailed with "Failed initializing any suitable GPU context!". Hoist the render-GID setup above the per-board case so it runs for every Qt 6 board. cage / linuxfb branching stays as-is. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(viewer): Pi 4 stays on --vo=drm (Qt linuxfb DRM master contention) Earlier commits switched Pi 4 to mpv --vo=gpu --gpu-context=drm based on a 3-6 vo-drop/30 s measurement. That test was run as root in a fresh container — no Qt linuxfb in the picture. In the production viewer where AnthiasWebview holds the framebuffer via Qt linuxfb, --vo=gpu fails: failed to open /dev/dri/renderD128: Permission denied [vo/gpu/drm] Failed to acquire DRM master: Permission denied [vo/gpu] Failed initializing any suitable GPU context! Error opening/initializing the selected video_out (--vo) device. Video: no video Mesa GBM holds DRM master persistently and contends with Qt linuxfb's framebuffer use. mpv's classic --vo=drm has its own master juggling (briefly grab → render → drop) that coexists fine with linuxfb — that's why master's existing Pi 4 config works. Revert Pi 4 mpv flags to the production master config: --vo=drm --drm-mode=1920x1080@60 --vd-lavc-threads=4 The standalone perf-finding from this branch's earlier history turns out not to apply in production; retracted from the roll-up. Pi 5 / x86 / arm64 unchanged (they're on cage + --vo=gpu --gpu-context=wayland, which has its own DRM master flow via cage). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(viewer): cage opens on the first connected connector, not HDMI-A-1 Without `-o`, cage uses whatever output the DRM backend enumerates first — typically HDMI-A-1 on Pi 5 (closer to USB-C) and the on-board panel / first HDMI on x86 / arm64. If the operator plugs into the *other* port (Pi 5 HDMI-A-2, or any DP connector on x86), cage renders to a disconnected connector and the screen stays black. start_viewer.sh now iterates /sys/class/drm/card*-*, picks the first connector whose status reads "connected", strips the cardN- prefix to get the bare name cage expects (HDMI-A-1, HDMI-A-2, DP-1, eDP-1, …), and passes it via `-o`. Falls back to letting cage pick if nothing is connected yet — the display may come up via HPD after cage starts, or this is a build/CI host with no display at all. Caught while end-to-end testing on the rig: Pi 5 cable on HDMI-A-2 went to a black screen even though `cat /sys/class/drm/card1-HDMI-A-2/status` reported "connected" and cage / the viewer were running. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(viewer): mpv from apt.raspberrypi.com on Pi 4 / Pi 5, hwdec auto-copy Stock Debian Trixie's mpv 0.40 is compiled without `v4l2request` hwdec, so Pi 5's Hantro stateless decoder is invisible to it and mpv falls back to software decode for every H.264 / H.265 source. Pi 4's V4L2 M2M decoder is reachable via `v4l2m2m-copy` but mpv's `--hwdec=auto-safe` whitelist explicitly excludes that method, so auto-detect picked software there too. Two changes, applied together because they only make sense together: * Pi 4 / Pi 5 viewer images now pull mpv (and the FFmpeg library family it depends on) from `archive.raspberrypi.com/debian trixie main`. The Pi-tuned build ships `v4l2request` hwdec (Pi 5) and a maintained `v4l2m2m-copy` (Pi 4). An apt-pin restricts the Pi repo to the mpv + libav* packages only, so curl / ca-certificates / etc. continue to come from stock Debian and the rest of the image stays on the same baseline. * `MPVMediaPlayer.play()` switches `--hwdec=auto-safe` → `--hwdec=auto-copy`. auto-copy is the same family but with a broader whitelist that *includes* the v4l2-family copy hwdecs. Net effect: x86 still picks vaapi-copy (unchanged), Pi 4 picks v4l2m2m-copy, Pi 5 picks v4l2request, arm64 falls through to software (no v4l2request in stock Debian mpv, no vendor-tuned Rockchip plugin in stock either — Tier-2 follow-up). Plus an `ANTHIAS_DEBUG_DROPS=1` env knob: when set on the viewer container, mpv's stdout/stderr go to `/data/.anthias/mpv.log` (host-bound) instead of `/dev/null`, and `--no-terminal` is dropped so the status line ("AV: ... Dropped: N") is emitted. Lets us read per-asset frame-drop counts straight from the production viewer pipeline (no custom harness, no rebuild) during the test-grid runs. Default (unset) preserves the silent behaviour. Also: drops the `cage -o <connector>` autodetect attempt — cage 0.1.x in Trixie doesn't accept `-o`, just `-m last`. Use that instead so cage opens on the most-recently-connected output regardless of HDMI-A-N enumeration order. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(viewer): use deb-packaged Pi keyring for archive.raspberrypi.com apt update against http://archive.raspberrypi.com/debian trixie was failing in the Pi 4 / Pi 5 viewer image builds: Sub-process /usr/bin/sqv returned an error code (1): Signing key on CF8A1AF502A2AA2D763BAE7E82B129927FA3303E is not bound: No binding signature at time … Policy rejected non-revocation signature (PositiveCertification) requiring second pre-image resistance SHA1 is not considered secure since 2026-02-01 Pi's bare `raspberrypi.gpg.key` URL still serves the original 2012-vintage RSA 2048 key with SHA1 binding signatures that Trixie's sqv refuses to certify under the post-2026-02-01 crypto policy. The deb-packaged keyring inside `raspberrypi-archive-keyring_2025.1+rpt1_all.deb` ships the *same* key fingerprint but with rebuilt binding signatures that sqv accepts — that's the keyring Pi OS Trixie itself installs, which is why `apt update` against this exact repo works on a real Pi 5 device today. Fetch the deb directly with curl, extract its bundled `.pgp` keyring, and point `signed-by=` at the installed copy. The pin block restricts what packages the Pi repo can supply (mpv + libav* + ffmpeg + libpostproc — the FFmpeg family), so the rest of the image keeps its stock-Debian baseline. Also extend the pin to cover libpostproc* and ffmpeg, since mpv's apt deps drag those into the Pi-tagged version on install; without the pin extension, apt rejected the resolve with "broken packages". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(viewer): per-codec hwdec on Pi via Lua hook mpv 0.40's `--hwdec` accepts a single value at startup, so we can't ask it to try v4l2m2m-copy for H.264 *and* drm-copy for HEVC out of the box. The Pi-tuned mpv from archive.raspberrypi.com supports both hwdec methods but each covers a different codec subset: * v4l2m2m-copy — Pi 4's V3D V4L2 M2M decoder. H.264 works; Pi 5's Hantro G2 is V4L2-stateless-only so this no-ops there. * drm-copy — FFmpeg's `v4l2_request_hevc` hwaccel. HEVC only, works on both Pi 4 and Pi 5. Add a small `on_load` Lua hook (inlined as `_PI_HWDEC_LUA`, written to /tmp on first play(), loaded with `--script=`) that checks `video-codec-name` and picks the right hwdec at file open. Net effect: Pi 4 H.264 → v4l2m2m-copy (HW) Pi 4 HEVC → drm-copy (HW) Pi 5 H.264 → v4l2m2m-copy (no device, falls back to SW — only path until mpv re-adds v4l2_request_h264 hwdec) Pi 5 HEVC → drm-copy (HW) The base `--hwdec=auto-copy` startup value still applies on x86 / arm64 (vaapi-copy on Intel/AMD; software fall-back on Rockchip), where the hook isn't loaded. Verified on real hardware: $ mpv ... --script=/tmp/anthias-pi-hwdec.lua test_hevc.mp4 [pi-hwdec] codec=hevc -> hwdec=drm-copy Using hardware decoding (drm-copy). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(viewer,server): HW-decode everywhere on Pi 4 / Pi 5 / x86 The previous per-codec Lua hook in media_player.py was a silent no-op: mpv's video-codec-name property is empty at every script event before hwdec init (on_load, on_preloaded), so --hwdec=auto-copy leaked through. auto-copy's upstream whitelist excludes v4l2m2m-copy, so H.264 on Pi 4 fell back to software despite the V3D V4L2 M2M decoder being available. Viewer (src/anthias_viewer/media_player.py) - Replace the Lua hook with ffprobe-driven dispatch from Python at launch time. ffprobe is in the viewer image; the call is ~50 ms. - Per-board mapping: Pi 4 → {h264: v4l2m2m-copy, hevc: drm-copy}; Pi 5 → {hevc: drm-copy}. Pi 5 H.264 falls back to auto-copy because mpv has no v4l2-request H.264 hwdec for the Hantro G1, and passing v4l2m2m-copy there just logs "Could not find a valid device" before SW-falling-back. - Live-verified on Pi 4: "Using hardware decoding (v4l2m2m-copy)" for 1080p H.264 and "Using hardware decoding (drm-copy)" for HEVC at 1080p and 4K. Asset processor (src/anthias_server/processing.py) - Pi 5 profile drops H.264 from passthrough_video_codecs — Pi 5 has no mpv H.264 HW path, so H.264 uploads must transcode to HEVC at upload time to keep the HW-decode-everywhere contract. - Pi 4 profile adds passthrough_video_max_pixels for H.264, capped at 1080p (1920*1080). 4K H.264 clears the codec gate but the V3D H.264 envelope tops at 1080p60, so the cap forces it through a libx265 re-encode at upload time. HEVC keeps no cap (the dedicated HEVC block handles 4Kp60). - _ffprobe_summary now returns video_pixels alongside codec / container / audio_codec; _video_can_passthrough enforces the per-codec pixel cap when the profile declares one. Tests - test_media_player.py: new per-board hwdec tests (Pi 4 H.264 → v4l2m2m-copy; Pi 5 H.264 → auto-copy; both → drm-copy for HEVC; auto-copy fallback when ffprobe fails; no probe on x86 / arm64). - test_processing.py: matrix tests updated to include video_pixels; parametrised rows now exercise Pi 5 H.264-no-passthrough and the Pi 4 4K H.264 cap. New end-to-end tests prove _run_video_normalisation transcodes Pi 5 H.264 → HEVC and Pi 4 4K H.264 → HEVC. Docs (docs/board-enablement.md, new) - Goal + per-board HW-decode capability table. - Asset processor codec policy spelled out as a contract. - BBB test bed recipe (source clips, libx265 transcode commands, ANTHIAS_DEBUG_DROPS=1, mpv.log slicing). Follow-up: Pi 5 4K HEVC HW The Hantro G2 decoder can't allocate 4K dst buffers from Pi 5's default 64 MB CMA ("v4l2_request_hevc_start_frame: Failed to get dst buffer") and SW-falls-back. Adding cma=512M to the kernel cmdline does NOT work — the kernel takes the cmdline value over the device-tree linux,cma node, orphaning rpi-hevc-dec ("Failed to probe hardware -517") and unpopulating /dev/video*, which kills HEVC HW at every resolution. The right fix is a dtparam/dtoverlay in /boot/firmware/config.txt that resizes the existing DT-declared region without orphaning the codec's reserved-mem reference. Until that lands, the pi5 profile should downscale 4K → 1080p HEVC. Documented in cmdline.txt.j2 and docs/board-enablement.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(viewer,server): mock _probe_video_codec; fix mypy on Popen IO types CI failures on the previous commit ( |
||
|
|
2d7b92c006 |
Hugo website: docs migration, API reference, FAQ, and SEO (#2807)
* Move website to Hugo * Rewrite in progress * Add Hugo-native API reference page and fix CSS build path Two related changes for the Hugo site: 1. CSS build target: package.json's css:build/css:watch wrote to assets/styles/style.css, but baseof.html uses a plain <link href> that Hugo serves from static/. The merge left a stale 14K static copy alongside the freshly-built 23K asset copy, so pages rendered with most utility classes undefined. Build target is now static/assets/styles/style.css, matching the convention used by every other website asset. 2. Hugo-native API docs at /api/. The OpenAPI spec is loaded from data/openapi.yaml (generated via `manage.py spectacular`) and rendered in layouts/_default/api.html and a recursive schema partial. Endpoints are grouped by tag with anchor jumps, color- coded method badges, params/request/response tables, and inline $ref resolution. Renders all 18 v2 endpoints across 9 tags with the existing Tailwind theme. No third-party JS bundle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Move documentation under Hugo and redirect old GitHub paths Migrates docs/ markdown into website/content/docs/ rendered with a new docs/ layout (list + single) and Tailwind prose styling. Images and the d2 diagram move to website/static/docs/. Internal links rewritten from /docs/foo.md to /docs/foo/, and GitHub-style alerts pre-converted to bold-labeled blockquotes since the goldmark alert extension is not enabled on this Hugo version. The original docs/*.md files are kept as redirect stubs that point at https://anthias.screenly.io/docs/... so external links into the GitHub docs tree still resolve to a useful page. Root README.md links updated to point at the website URLs. Hugo nav now exposes Docs alongside Features / Get Started / API / FAQ. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix factual inaccuracies in migrated docs against the codebase Reviewed all docs against the current source. Concrete fixes: * _index.md: container names use the post-rebrand `anthias-` compose project prefix (e.g. `anthias-anthias-server-1`, `anthias-redis-1`) rather than the legacy `screenly-` form. Replaced `docker-compose logs` with `docker compose logs` and added the optional `anthias- caddy` sidecar to the container table. * developer-documentation.md: fixed leading-letter typo ("unning"), and replaced the old Django test-runner invocation with the pytest commands used by the suite today (`pytest -n auto -m "not integration"` and `pytest -m integration`). * balena-fleet-deployment.md: corrected the supported board list ($BOARD_TYPE) to match `bin/deploy_to_balena.sh --help` (`pi2`, `pi3`, `pi4-64`, `pi5` — no `pi1` or plain `pi4`). Updated registry reference from Docker Hub to GHCR. * migrating-assets-to-screenly.md: `cd ~/screenly` → `cd ~/anthias` (post-rebrand install path). * raspberry-pi5-ssd-install-instructions.md: fixed "Opitions" and "uinsg" typos in the boot-order steps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Polish docs styling: callouts, syntax highlighting, hierarchy Reworks docs prose styling so the migrated pages don't read like default-Hugo-render-output: * Headings: in-body H1/H2 collapse to a section divider style with a top border so they don't compete with the dark page hero. H4-H6 become small uppercase eyebrows. Markdown sources mix #/##/#### inconsistently — the visual scale now compresses gracefully. * Alerts: a render-blockquote hook detects the bold-label preamble produced by our preprocessor (`> **Note**` etc.) and emits a typed `<blockquote class="docs-alert docs-alert-note">` so each kind gets its own colored border + label (note/tip/important/warning/caution). * Syntax highlighting: enable Hugo Chroma with the github style, noClasses=false. Generated chroma.css ships as a static asset and is loaded alongside style.css. `pre`/`<code>` get a light surface that the chroma token colors sit on top of. * Inline code, lists, links, tables, and images all get a small rebalance — bullet color, link underline weight, image shadow, table border-radius — to match the brand-purple theme. * Footer: the Resources / Docs link pointed at the legacy github.com/.../docs/README.md path; now points at /docs/. Added an API Reference link alongside. * Stripped a stray `<br>` in the Pi5 SSD doc that was creating a random gap between a blockquote and its illustrative screenshot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Make x86/PC docs consistent and more user-friendly The migrated docs used four different forms — "x86", "x86 device", "PC (x86) devices", and "PC (x86 Devices)" — depending on the page. Standardize on **PC (x86)** as the user-facing label (PC is what people search for; x86 stays as the architecture qualifier). Also rewrites x86-installation.md from a flat bullet dump into a clearer five-step walkthrough — what you need, download, flash, install Debian, prep the system, run the installer — and crosslinks the right anchor in installation-options.md so PC users can hand off to the scripted install without scrolling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Expand FAQ with forum-driven questions and refactor to data file The FAQ had six entries that didn't reflect what people actually ask on forums.screenly.io. Reviewed the all-time top topics and added the ones that show up over and over: portrait rotation, YouTube playback, Wi-Fi setup, static IP assignment, audio output, resolution / 4K, black-screen troubleshooting, transitions, asset storage / backup, SSH, HTTPS pointer, commercial-use clarity, getting logs, and a link to the API reference. Refactored the layout so it reads from data/faq.yaml grouped by section (About, Installation & updates, Display & playback, Operations) and renders each answer through markdownify. This makes adding new entries a one-paragraph YAML edit instead of duplicating ~15 lines of accordion markup. Answers reuse the .docs-prose styling so code, links, lists, and inline pre snippets all match the docs pages. Also tightened the "Accessing the REST API" section in /docs/ to point at the new /api/ page first, with the live ReDoc URL on the device as a secondary callout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Correct rotation FAQ — Anthias renders via linuxfb + DRM, no Wayland Verified in code: docker/Dockerfile.viewer.j2 sets QT_QPA_PLATFORM=linuxfb, webview/build_qt{5,6}.sh both pass -skip wayland to the Qt build, and viewer/media_player.py invokes mpv with --vo=drm. There is no Wayland compositor in the runtime stack on any board. Replaced the previous "Pi 5 with Wayland uses a different stack" hand-wave with the actual fallback: if /boot/firmware/config.txt's display_rotate=N doesn't stick on a Pi 5 / KMS pipeline, append video=HDMI-A-1:...,rotate=N to /boot/firmware/cmdline.txt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Tighten three FAQ answers after a code-driven validation pass * SSH: previous answer claimed SSH was on by default for both the Anthias disk image and the scripted install. Anthias's installer doesn't touch sshd at all, so the answer now distinguishes between the prebuilt images (SSH on) and a self-flashed Raspberry Pi OS Lite (SSH must be pre-enabled). * Audio output: static/src/components/settings/audio-output.tsx hides the 3.5mm option on Pi 5 because the hardware lacks the jack. Call that out so Pi 5 users don't go looking for a missing dropdown item. * Black screen: replaced the `xset dpms force on` suggestion. Anthias has no X server on any board (Qt runs on linuxfb, mpv on --vo=drm), so xset can't toggle DPMS. Pointed users at re-seating HDMI or checking the TV's input as a more grounded recovery. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Correct features-page claim — Anthias detects display state, can't toggle it The "Display power control" card promised programmatic on/off toggling of the connected screen for energy savings. That isn't a real feature. lib/diagnostics.py only calls libcec's tv.is_on() to *query* the TV's power state — there's no power-on / standby command path anywhere in the codebase. The result surfaces read-only as display_power on the System Info page (static/src/components/ system-info.tsx). Replaced the card with what's actually shipping: HDMI-CEC display state *detection*, visible on the System Info page. Verified the rest of the page against code while I was in there. Accurate as written: image/video/webpage assets (Qt webview + mpv), scheduling (start_date/end_date/duration on the asset model), drag- drop playlists (@dnd-kit/sortable), shuffle (settings.shufflePlaylist), 1080p output (mpv pinned to 1920x1080@60 on pi4-64/pi5 in viewer/media_player.py), real-time WebSocket sync (Django Channels + Redis pub/sub), REST API (drf-spectacular), four-container compose topology, backup/restore, optional basic auth (lib/auth.py BasicAuth), System Info page fields (loadavg/free_space/uptime/anthias_version), and the supported hardware list (matches ansible/site.yml's device_type assertion). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Punchier homepage tagline: "Free digital signage for everyone." Replaces "Open source digital signage for any screen" with a shorter, benefit-led headline. The new line breaks naturally across two lines on desktop (free digital signage / for everyone.) and stays single- line on mobile to avoid an awkward orphan. Subtitle is unchanged — it still does the explanatory work (Pi or PC, schedule images/videos/webpages, no subscriptions). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * SEO sweep: per-page meta, FAQPage / TechArticle JSON-LD, robots.txt Two functional gaps in the existing setup: * og:description and twitter:description were hardcoded to a single marketing line on every page, while the per-page <meta name= description> already pulled from front matter. So the Slack/Twitter/ Discord card preview always read the same blurb regardless of which page you shared. Now both the OG and Twitter description reflect the page's own .Params.description. * Page titles drifted: most pages embedded "Anthias" in the title string, but the docs pages were just "Documentation" / "Installation Options" / etc. — fine for the H1, weak for SERPs. Title now appends " | Anthias" only when the page title doesn't already contain the brand, so existing branded titles stay clean and docs pages get a brand suffix automatically. Other tightening: * Added FAQPage structured data on /faq/ generated from data/faq.yaml so Google can surface FAQ rich results. * Added TechArticle structured data on individual /docs/ pages. * og:type now flips to "article" on docs pages. * og:image:alt + twitter:image:alt populated. * theme-color set to the brand purple for mobile browser chrome. * JSON-LD home schema URL now uses site.BaseURL instead of a hardcoded production URL — important for staging / dev parity. * <html lang> reads site.LanguageCode instead of a fixed "en". * Added a real robots.txt that points crawlers at /sitemap.xml (Hugo already generates the sitemap, but a robots.txt makes the pointer explicit and unblocks tooling that looks for it). Replaced placeholder image alt text in the docs ("balena-ss-01", "imager-01", "rpi-eeprom-update", etc.) with descriptive captions — better for screen readers and image-search SEO. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Move site assets into Hugo's expected layout and rename /docs URLs Two related cleanups. ASSETS — site assets were split across website/static/assets/ (the shadowed copy hugo.toml's [[module.mounts]] directed traffic to) and website/assets/ (an unused duplicate). Hugo's own build report showed "Processed images: 0" because nothing actually flowed through Pipes. * Removed the [[module.mounts]] override so Hugo uses default layout: assets/ for Pipes-processable resources, static/ for served-as-is files. * Used `git mv` to record the docs/ image and stylesheet renames as history-preserving moves rather than delete+add diffs. * Removed the duplicate website/static/assets/images/ directory — files already lived in website/assets/images/. * Bun's css:build/css:watch now write to assets/styles/style.css so Tailwind output flows through Hugo Pipes. * baseof.html loads style.css and chroma.css via resources.Get + fingerprint, with SRI integrity attributes. Each deploy produces a fresh content-hashed URL (/styles/style.<hash>.css), so the browser cache invalidates correctly without manual cache-busting. * Logos, social icons, hero raster (overview*.png), favicon, and plus/minus accordion icons all flow through resources.Get for consistent asset handling. * Added layouts/_default/_markup/render-image.html so markdown image references in /docs are looked up via resources.Get and emitted with loading="lazy" decoding="async". URL RENAMES — the docs URLs were verbatim copies of the original GitHub filenames, which made for noisy URLs like /docs/raspberry-pi5-ssd-install-instructions/. Slugged each page and left aliases for the old paths so Hugo emits a meta-refresh redirect: /docs/installation-options/ → /docs/install/ /docs/balena-fleet-deployment/ → /docs/balena/ /docs/x86-installation/ → /docs/pc/ /docs/raspberry-pi5-ssd-install-instructions/ → /docs/pi5-ssd/ /docs/migrating-assets-to-screenly/ → /docs/migrate-to-screenly/ /docs/qa-checklist/ → /docs/qa/ /docs/developer-documentation/ → /docs/development/ Cross-doc links inside /docs and the README + repo-root docs/ stub files all point at the new URLs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix lint + mypy on raspberry_pi_imager test (carried over from rebase) The test_build_pi_imager_json.py file landed in `88d3881b Move website to Hugo` with two pre-existing CI failures: * ruff format --check: a few helper definitions had a stale line break the formatter wanted to collapse. * mypy: `make_image_metadata(board: str) -> dict` is missing the generic type parameters that the project's mypy config flags as type-arg. Annotated as `dict[str, Any]`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Self-host Plus Jakarta Sans via @fontsource (drop Google Fonts CDN) Removes the third-party Google Fonts <link>. SonarCloud's Web:S5725 hotspot was flagging the link as a resource-integrity (SRI) risk — SRI is impossible against Google Fonts because the served stylesheet rotates per User-Agent and the woff2 URLs change with the font CSS. Self-hosting the same font from npm via @fontsource removes the cross-origin resource entirely. How it's wired: * `bun add -D @fontsource/plus-jakarta-sans` for the font binaries. * `scripts/install-fonts.ts` is a small bun script that, given the installed package, copies woff2 files for latin + latin-ext at weights 400/500/600/700/800 to `static/fonts/` (so Hugo serves them at `/fonts/...`) and emits a combined `assets/fonts/plus-jakarta-sans.css` with the urls rewritten to absolute /fonts/... paths and the woff fallback stripped. * `package.json` adds `fonts:install`, and chains it through `css:build` / `css:watch` so Tailwind always sees the generated CSS up to date. * `main.css` @imports the generated CSS — Tailwind/Lightning CSS inlines the @font-face rules into the final fingerprinted style.<hash>.css. * `.gitignore` excludes `assets/fonts/` and `static/fonts/` since both are deterministically regenerated from node_modules. * `baseof.html` no longer pulls from fonts.googleapis.com. Total payload: 10 woff2 files (~136KB), but each is loaded on-demand by unicode-range — typical English-only visitors fetch ~50KB of fonts, served from same-origin. The second Web:S5725 hotspot (gtag.js from googletagmanager.com) is unchanged in this commit — Google's tag manager script is updated server-side without a stable hash, so SRI cannot apply. That one needs a product call (keep with dismissal, drop GA, or move to a privacy-first SRI-friendly alternative). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address SonarCloud code-smell findings on the website Cleared the unrelated SonarCloud findings raised on this PR: * `install-fonts.ts`: `fs` and `path` imports use the `node:` prefix (typescript:S7772). The new prefixed form is the bun-recommended one, no behavior change. * `_markup/render-image.html`: rewrote the comment that referenced `<img>` literally — Web:ImgWithoutAltCheck was treating the word inside the Hugo comment block as an actual element with no alt. * `_default/faq.html`: replaced the accordion's `<div role="region">` with a real `<section>` element (Web:S6819). The aria-labelledby binding stays, so the accessible name resolution is identical and the semantics are now native rather than ARIA-emulated. * `assets/styles/chroma.css`: stripped the two stray-semicolon lines left over from the sed pass that emptied the github-style backdrop (css:S1116). The remaining `.chroma { -webkit-text-size-adjust: none }` rule is what's actually load-bearing. * `_default/baseof.html`: - accordion JS now reads `this.dataset.accordion` instead of `this.getAttribute('data-accordion')` (javascript:S7761). - GA bootstrap uses `globalThis.dataLayer` instead of `window.dataLayer` (javascript:S7764). Same semantics in any browser context, no globalThis polyfill needed for our targets. * `layouts/index.html`: dropped the deprecated `scrolling="0"` attribute from the GitHub stars iframe (Web:S1827); replaced with the equivalent `overflow-hidden` Tailwind class. The Web:S5725 SRI hotspot on the gtag.js script (line 162 of baseof.html) is the only remaining finding. Google Tag Manager is versioned server-side without a stable hash, so SRI fundamentally can't apply — that one is being kept and dismissed in the SonarCloud UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address Copilot review + trigger marketing deploy on release publish Copilot review: * api.html: request-body renderer only looked at application/json, so endpoints whose only content type is multipart/form-data (file uploads) or application/x-www-form-urlencoded would render an empty Request body section. Pick application/json first if present, otherwise fall back to the first listed content type, and label the rendered schema with its actual content type. * build_pi_imager_json.py: every requests.get() now sets a 30s timeout and calls raise_for_status() so a slow/rate-limited GitHub API doesn't hang the deploy job and a 4xx/5xx fails fast with a clear message rather than a confusing KeyError on response.json(). * docs/raspberry-pi5-ssd-install-instructions.md: "Other HAT's" → "Other HATs". * docs/qa-checklist.md: dropped the spurious "a" in "Change a the start and end dates". * deploy-website.yaml: jq's has() takes one key, so the validation step `has("name", "description", ...)` was actually a syntax error on every run — rewrote as `all($k; $entry | has($k))` over the required-key list. * layouts/_default/get-started.html: the "Documentation" CTA pointed at the old GitHub markdown file; now links to /docs/ to match the navbar / footer. * website/README.md: rewrote the project-structure tree to match what's actually in the repo (data/, scripts/, layouts/docs/, Goldmark _markup/ hooks etc.) and documented the bun pipeline — `hugo server` alone leaves /fonts/* as 404s because the woff2 files are gitignored and materialized by `bun run fonts:install`. Marketing deploy on release publish: `build-balena-disk-image.yaml` cuts the GitHub release with the *.img.zst artefacts as its final step; until now the marketing site only re-deployed on master push or manual dispatch, so rpi-imager. json on the live site lagged the freshest disk images by however long it took someone to push an unrelated website change. Hooking deploy-website.yaml to `on: release: types: [published]` makes the site rebuild as soon as the release exists, which is exactly when the GitHub API starts surfacing the new assets the JSON generator queries. `prerelease=true` releases are included because that's what build-balena-disk-image.yaml currently flags every release as. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address second round of Copilot review * installation-options.md: balenaEthcher → balenaEtcher. * balena-fleet-deployment.md: includa → include. * developer-documentation.md: spash screen → splash screen. * qa-checklist.md: enabling **Show splash screen** is supposed to *display* the splash, not hide it — flipped "is not being displayed" → "is being displayed". Also clickin → clicking. * raspberry-pi5-ssd-install-instructions.md: `sudo apt update -y` isn't valid (apt's -y is only for install / upgrade), so the copy-paste step would error. Dropped the `-y` from update; the full-upgrade line keeps it because that's where it actually does something. * deploy-website.yaml: the jq required-keys check was missing `icon` and `website`, which build_pi_imager_json.py's REQUIRED_FIELDS already enforces in the Python tests. Added them so the runtime validation matches the generator's contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address third round of Copilot review * website/.gitignore: `_.log` was a typo from the original Hugo bootstrap — it doesn't match anything. Replaced with the intended `*.log` so log files are actually ignored. * website/package.json: rewrote the `dev` script to capture both child PIDs and trap EXIT/INT/TERM so Ctrl-C (or hugo crashing) takes the Tailwind watcher down with it. Mirrors the pattern in the repo-root package.json's `dev`. * docs/raspberry-pi5-ssd-install-instructions.md: "Early Pi 5's" → "Early Pi 5s" (no apostrophe on plurals). * docs/qa-checklist.md: "make sure that the screen in standby mode" → "make sure that the screen is in standby mode" (missing verb). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Rewrite raspberry_pi_imager tests in pytest style The file was unittest.TestCase classes — pytest discovers and runs those, but the boilerplate doesn't earn its keep. Each test method re-declared `@patch('...requests.get')` and rebuilt the same MagicMock setup, and the per-board cases lived as 5+3+2+2 separate methods that should have been one parametrize each. Reworked as flat module-level functions backed by three fixtures: * `mock_requests_get` — patches the module's `requests.get` and yields the mock so each test sets `return_value` / `side_effect` directly. * `mock_release_assets` — preconfigured to return the canned release asset list, used by the `get_asset_list` cases. * `mock_full_build` — wires up the three call shapes `build_imager_json()` makes (latest, asset list, per-asset json). Per-board cases collapse into `@pytest.mark.parametrize`: get_board_from_url's positive cases, the non-image-returns-None cases, the maintenance-mode boards, and the modern boards. Coverage is the same — 21 collected cases (pytest fans the parametrize out from 12 test methods to 21 ids), all passing in 0.12s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix PR checks and Copilot review items --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5e00c8ba25 |
refactor(docker): drop celery image, restore base apt layer dedup (#2776)
* refactor(docker): drop celery image, restore base apt layer dedup
- Delete Dockerfile.celery.j2; compose now runs celery on the
anthias-server image with a `command:` override.
- Make viewer extend Dockerfile.base.j2 (mirroring test); drop 17
packages duplicated between viewer and base_apt_dependencies, plus
4 within-list duplicates.
- Move `# syntax=docker/dockerfile:1.4` to line 1 of every rendered
Dockerfile. It previously lived in uv-builder.j2 line 1 and got
bumped mid-file for server by the bun-builder prelude, silently
disabling the 1.4 frontend and breaking cache-key parity with
viewer — the actual blocker for layer dedup.
- Collapse CI matrix from (board × service) to (board) so all
services for a board build on the same runner with the same
buildkit cache, producing byte-identical apt layer digests at the
registry.
- Add ENV DJANGO_SETTINGS_MODULE to the server image so the merged
image runs both server and celery CMDs.
- Update all five compose templates (prod, balena prod, balena dev,
dev, test) to redirect anthias-celery at the server image with a
command: override. dev compose pins an explicit `image:` tag so
both services share the locally-built SHA.
- Remove old anthias-celery / srly-ose-celery containers in
upgrade_containers.sh so the recreated container can take the name.
Verified end-to-end on x86: server and viewer apt layers share a
single digest; SHARED SIZE jumps from 132 MB to 1.216 GB; merged
image runs both workloads in compose (celery task round-trips
through Redis to SUCCESS).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* perf(docker): cache buildkit layers in GHCR registry across CI runs
Add a --cache-backend / $BUILDX_CACHE_BACKEND option to
tools.image_builder with two modes:
- `local` (default): writes to /tmp/.buildx-cache/<board>/.
Unchanged from before; right for local dev.
- `registry`: pushes BuildKit cache to
ghcr.io/screenly/anthias-<service>:buildcache-<board>. Reuses the
GHCR login already done by docker-build.yaml, no extra tokens or
third-party actions needed.
Wire CI to use registry mode on push events (master) so subsequent
runs of the same board pull cached layers — the ~825 MB extracted
apt install per service goes from ~3 min cold to a few seconds
warm. workflow_dispatch on a non-master branch falls back to local
mode (effectively no-cache) so manual runs can't pollute the master
cache.
Drop the old actions/cache@v5 step that mirrored
/tmp/.buildx-cache/<board> through actions/cache — registry cache
is per-step rather than one big tarball, so it survives the GitHub
Actions cache 10 GB-per-repo eviction better.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(image-builder): move local cache out of /tmp to user XDG cache dir
SonarCloud python:S5443 flagged the previous /tmp/.buildx-cache/
default as a security hotspot — `/tmp` is world-writable, so on a
multi-user host another account could in principle tamper with the
buildkit cache. Switch to $XDG_CACHE_HOME/anthias-buildx/<board>/
(default ~/.cache/anthias-buildx/), which is per-user by default
and follows XDG Base Directory convention.
CI is unaffected: docker-build.yaml uses --cache-backend=registry
on push events, which pushes cache to GHCR and never touches the
local path. Local dev users with stale state in
/tmp/.buildx-cache/<board>/ can rm it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(docker): correct cache-backend comments to match real behavior
Two doc fixes per Copilot review on #2776:
- tools/image_builder/__main__.py: the cache-backend rationale
block still referenced /tmp/.buildx-cache/<board>; update to
$XDG_CACHE_HOME/anthias-buildx/<board> so it matches the
implementation moved in
|
||
|
|
4333fffafa |
refactor(messaging): replace ZMQ with Redis for all viewer signalling, drop pyzmq (#2760)
* refactor(messaging): replace ZMQ pub/sub with Redis for server→viewer commands Server-to-viewer command bus moves off pyzmq onto Redis pub/sub on the 'anthias.viewer' channel, since Redis is already the broker for Celery and the channel layer for Django Channels — no reason to run a second message bus. - settings.ZmqPublisher → settings.ViewerPublisher (redis.publish). - viewer/zmq.py → viewer/messaging.py with ViewerSubscriber backed by redis.pubsub(); the two ZmqSubscriber threads in viewer.main collapse into one, since both former publishers (anthias-server and the host-side wifi-connect script) now fan into the same Redis channel. - viewer-subscriber-ready gating preserved: set after subscribe() returns, same semantics as before. - ZmqConsumer / ZmqCollector (viewer→server reply path) and pyzmq itself are intentionally left in place; PR2 migrates the reply bus and PR3 removes pyzmq + libzmq from the dep tree and Dockerfiles. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: publish host-side wifi-connect messages via Redis, not ZMQ The captive-portal flow (`setup_wifi`, `show_splash`) used to publish on ZMQ port 10001 from the host, with a second ZmqSubscriber inside the viewer connected to host.docker.internal:10001 picking it up. The previous commit collapsed the viewer down to a single Redis-backed subscriber, so this script's ZMQ publishes were going nowhere. Switch the script to redis.publish() against the same anthias.viewer channel. The Redis client is already wired here for the viewer-subscriber-ready gate, and the wifi-connect container runs in network_mode: host, so loopback to redis on 127.0.0.1:6379 (already exposed via the redis service's port mapping) keeps working unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(messaging): replace ZMQ reply bus with Redis BLPOP + correlation IDs Drops the second ZMQ leg — the viewer→server reply path — in favor of Redis BLPOP keyed by a UUID correlation ID. Same channel layer that PR1 moved the command bus onto, so the entire viewer messaging path now runs on Redis. Wire format extends the existing 'command¶meter' encoding: the 'current_asset_id' command (currently the only request-reply command) now carries the correlation ID in the parameter slot, and the viewer LPUSHes its JSON reply onto 'anthias.reply.<corr-id>' (with a 30s EXPIRE so unread replies don't accumulate). The server BLPOPs that key. This also fixes a latent correctness bug: ZmqCollector had no correlation, so concurrent /v1 ViewerCurrentAsset callers could mismatch replies. That hazard was masked today by uvicorn running single-worker; with Redis + correlation IDs, the reply path is now safe across concurrent callers. - settings.ZmqConsumer / ZmqCollector → settings.ReplySender / ReplyCollector (BLPOP). 'import zmq' drops out — pyzmq itself is removed in the next commit. - lib.errors.ZmqCollectorTimeoutError → ReplyTimeoutError (the only catch site is implicit — it bubbles to a 500 — so the rename is mechanical). - viewer/__init__.py: send_current_asset_id_to_server takes a correlation ID and uses ReplySender. The 'current_asset_id' command handler in the dispatch table threads the parameter (now the corr ID) into the function call. - api/views/v1.py ViewerCurrentAssetViewV1: generates a UUID, sends it with the command, BLPOPs on it. - api/tests/test_v1_endpoints.py: ZmqCollector mock → ReplyCollector; side_effect signature relaxed to '*_' since recv_json now takes two positional args (corr, timeout_ms). - stubs/redis-stubs/client.pyi: add rpush() and blpop() narrowed to decode_responses=True return shapes (the rest of the stub follows the same convention). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: drop pyzmq + libzmq, finalize ZMQ→Redis migration With both legs of the viewer signalling path on Redis (PR1: command bus, PR2: reply bus), the pyzmq runtime dependency and the libzmq* build deps are no longer used. - pyproject.toml: remove pyzmq==23.2.1 from server, viewer, wifi-connect, and mypy dep groups (4 places). - uv.lock: regenerated; pyzmq + transitive py drop out. - tools/image_builder/{__main__,utils}.py: remove libzmq3-dev / libzmq5-dev / libzmq5 from the base apt list and from the viewer context's apt list. docker/uv-builder.j2 likewise drops libzmq3-dev from both the prebuilt-uv branch and the pip-fallback branch (32-bit ARM). The rendered docker/Dockerfile.* artifacts are gitignored, so no committed Dockerfile churn here — they regenerate cleanly via `python -m tools.image_builder --dockerfiles-only`. - send_zmq_message.py → send_viewer_message.py. The script already publishes via Redis (fixed in the PR1 follow-up); rename + update callers (bin/start_wifi_connect.sh, docker/Dockerfile.wifi-connect.j2) now that the ZMQ name is misleading. - bin/start_server.sh: drop the stale "single-worker because ZmqPublisher binds 10001" comment. The publisher is now a Redis client — no port bind, multi-worker is safe whenever the operator wants to opt in (not changed in this PR). - CLAUDE.md: update the architecture description (ZMQ ports 10001 / 5558 are gone, Redis carries the viewer signalling traffic now). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: post-merge cleanup — re-flow ruff fmt + drop stale ZMQ refs Three small clean-ups discovered while running CI locally after the master merge ( |
||
|
|
7476a43b27 |
chore: drop wifi-connect service end-to-end (#2763)
The anthias-wifi-connect captive-portal helper has been pinned to
balena-os/wifi-connect v4.11.1 (Feb 2023) for ~3 years; upstream
dropped the ARMv6 binary back in v4.4.6 so Pi 1 was silently
shipping a wifi-connect container with no binary inside, and the
host script `bin/start_wifi_connect.sh` had a `set -e`-vs-`$?` bug
that made the captive-portal branch unreachable. nmcli/nmtui covers
the supported install path.
Removing the whole service rather than bumping it: there are no
production users left and bumping would require rewriting both the
architecture-to-asset matcher (Rust target triples now) and the
unzip step (tar.gz now).
Removed
- Container build: docker/Dockerfile.wifi-connect[.j2],
`wifi-connect` group in pyproject.toml + uv.lock,
`wifi-connect` entry in image_builder SERVICES,
`get_wifi_connect_context()`,
`wifi-connect` cell in CI matrix +
docker-build.yaml retag SERVICES list.
- Compose: `anthias-wifi-connect` service from prod / balena
/ balena-dev templates, plus the now-unused
`host.docker.internal:host-gateway` extra_hosts
on `anthias-viewer`.
- Helper scripts: bin/start_wifi_connect.sh,
start_wifi_connect_service.sh,
send_zmq_message.py.
- Viewer plumbing: the second ZmqSubscriber bound to
host.docker.internal:10001, the
`viewer-subscriber-ready` Redis flag, the
`setup_wifi` / `show_splash` / `show_hotspot_page`
handlers and their entries in the `commands`
dict, the `mq_data` / `load_screen_displayed`
globals, and the now-unused `redis_connection`
parameter on `ZmqSubscriber`.
- Server: `/hotspot` URL route, `views_files.hotspot`,
`HOTSPOT_FILE` / `INITIALIZED_FLAG` constants,
`HotspotViewTest`, templates/hotspot.html,
static/img/wifi-off.svg, /data/hotspot dir
creation in bin/start_viewer.sh.
- Host: sudoers entry for /usr/local/sbin/wifi-connect,
ansible/roles/network template + vars.
- Docs: docs/wifi-setup.md, the Wi-Fi Setup section and
container row in docs/README.md, the
wifi-connect.service line and stale
`initialized` flag bullet in
docs/developer-documentation.md, the
"Reset Wi-Fi → hotspot page" step in
docs/qa-checklist.md.
Migration paths kept (intentional)
- bin/upgrade_containers.sh now runs `docker rm -f` on
anthias-wifi-connect and srly-ose-wifi-connect alongside the
existing nginx/websocket cleanup, so on next pull devices drop
the stale container.
- ansible/roles/network/tasks/main.yml stops, disables, and
removes /etc/systemd/system/wifi-connect.service, then notifies
a new `Reload systemd daemon` handler. Idempotent on fresh
installs.
Verified
- `ruff check` + `ruff format --check`: clean.
- Strict `mypy .` (django-stubs + drf-stubs plugins): 97 files,
0 issues.
- `ansible-lint ansible/`: passes at the `production` profile.
- All three compose templates render and parse via
`docker compose config`.
- `python -m tools.image_builder --dockerfiles-only` generates
the remaining 5 services with no Dockerfile.wifi-connect
produced.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f421130b24 |
refactor(server): collapse nginx + websocket containers into uvicorn (#2757)
* refactor(server): collapse nginx + websocket containers into uvicorn
Replace the nginx + gunicorn + gevent-websocket trio with a single
uvicorn ASGI server inside `anthias-server`:
* HTTP, /static/, /anthias_assets/, /static_with_mime/, and /hotspot
are now served from Django (WhiteNoise + small file-serving views in
`anthias_app/views_files.py` that re-implement nginx's IP allowlists).
* WebSockets move from a separate gevent process talking ZMQ to Django
Channels with a Redis-backed channel layer, fanned out by celery via
`channel_layer.group_send`.
* TLS termination is handled by uvicorn directly when SSL_CERTFILE /
SSL_KEYFILE are set; `bin/enable_ssl.sh` now writes a compose
override (no longer ansible) and a companion `bin/disable_ssl.sh`
removes it. Cert + key live under `~/.anthias/ssl/`.
* `bin/upgrade_containers.sh` removes the legacy `anthias-nginx` and
`anthias-websocket` containers on upgrade so they don't linger.
* Drop `gunicorn`, `gevent`, `gevent-websocket`, and the `websocket`
uv group from `pyproject.toml`; add `channels`, `channels-redis`,
`daphne`, `uvicorn[standard]`, and `whitenoise`.
Notes on hardening: `--forwarded-allow-ips` defaults to off so the IP
allowlist can't be bypassed via a spoofed `X-Forwarded-For`; operators
behind a reverse proxy can opt in via the `FORWARDED_ALLOW_IPS` env
var. Backup uploads previously sized by nginx's `client_max_body_size
4G` are preserved by setting `DATA_UPLOAD_MAX_MEMORY_SIZE = None`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address review feedback on uvicorn migration
* Drop USE_X_FORWARDED_HOST (inconsistent with the deliberate
--forwarded-allow-ips hardening; without a proxy, X-Forwarded-Host is
client-controlled).
* Remove daphne — uvicorn runs production and the test environment now
uses it too (bin/prepare_test_environment.sh).
* Replace _safe_join's parents-membership check with Path.is_relative_to.
* Drop AllowedHostsOriginValidator wrapper (no-op under ALLOWED_HOSTS=['*'])
and document where to put it back if hosts are ever locked down.
* Rename DOCKER_CIDR → DOCKER_BRIDGE_CIDR with a comment that this is
defense-in-depth, not a real perimeter (LAN clients via the published
port also appear in 172.16/12).
* Add anthias_app/tests.py covering the IP allowlists, mime override,
hotspot gating, and traversal/symlink rejection in _safe_join (17 tests).
* Note the single-worker ZmqPublisher bind constraint in start_server.sh
so a future scale-up doesn't EADDRINUSE on tcp://0.0.0.0:10001.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(security): clear SonarCloud hotspots on uvicorn migration
* Restrict views_files.anthias_assets / static_with_mime / hotspot to
GET via @require_GET (Sonar S3752, x3): they are read-only file
servers and should reject other methods at the view boundary.
* Mark RFC1918 / Docker-bridge CIDR literals as NOSONAR S1313 (x4):
they are intentional, well-known private network ranges.
* Mark `http://*` in CSRF_TRUSTED_ORIGINS as NOSONAR S5332 with a
comment explaining devices ship over HTTP and operators opt into TLS
via bin/enable_ssl.sh.
Existing 17 view tests continue to pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: clear remaining static-analysis findings
* ruff format -- the previous tests.py reformatted itself; CI's
`ruff format --check` now passes.
* CodeQL py/path-injection on _safe_join: rewrite using
os.path.realpath + os.path.commonpath, which CodeQL recognises as a
sanitiser for path-injection sinks. Behaviour is identical to the
Path.is_relative_to version (both reject `..` and symlink escapes;
the 17 tests in anthias_app/tests.py still pass).
* SonarCloud NOSONAR markers: switch to the codebase's bare `# NOSONAR`
form (matches host_agent.py and tests/test_backup_helper.py); the
earlier `# NOSONAR <rule>` form was not being honoured.
* Centralise the test-fixture IPs in module-level constants so S1313
is suppressed in one place rather than at every callsite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(security): inline path-injection check in views
CodeQL only treats os.path.commonpath as a sanitiser when the check
sits in the same function as the file-system sink — calling
_safe_join() from a separate function still leaves the open()/isfile()
sinks tainted (4 alerts on PR #2757).
Repeat the realpath + commonpath check inline in anthias_assets and
static_with_mime so CodeQL can prove the post-check path stays under
the configured root. _safe_join is kept for the SafeJoinTest unit
tests and as a documented helper.
Existing 17 tests in anthias_app/tests.py continue to pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(security): use realpath+startswith path sanitiser for CodeQL
CodeQL's path-injection model recognises the canonical
`realpath(...).startswith(base + sep)` pattern but apparently not
`os.path.commonpath(...) == root` in this codepath. Switch the inline
check in anthias_assets and static_with_mime to startswith so the
analyser can prove the post-check path stays under the configured
root.
Behaviour is identical: traversal and symlink-escape still 404
(verified by SafeJoinTest + view tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address Copilot review feedback
* lib/utils.py imported channels/asgiref at module level. The viewer
container imports lib.utils via viewer/__init__.py but its uv
dependency group does not ship channels, so the viewer would
ImportError on startup. Move the channels imports into
YoutubeDownloadThread.run() (server/celery-only path) so lib.utils
remains importable from the viewer.
* Drop the unused _safe_join() helper and its three SafeJoinTest
cases — the views inline a realpath+startswith sanitiser (CodeQL
needs the check in the same function as the sink), and the helper
was only being exercised in isolation. Add an equivalent
symlink-escape test against anthias_assets so the actual code path
used by the views is covered.
* Refresh the anthias_django/settings.py docstring + Django doc URLs
from /3.2/ → /4.2/ to match the pinned Django version.
15 view tests pass (was 17 — lost 3 SafeJoinTest + gained 1 symlink
test against the real view).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: refresh architecture diagram for uvicorn migration
Drop the anthias-nginx and anthias-websocket nodes (and their edges)
from docs/d2/anthias-diagram-overview.d2 — the user now talks
directly to anthias-server (uvicorn handling HTTP + /ws), Celery
fans out asset-update events through the Redis-backed Channels
layer, and the viewer fetches media from anthias-server over HTTP.
Regenerate the SVG with d2 v0.7.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address Copilot SSL + CSRF / WS-origin feedback
* Dual uvicorn listeners when SSL is enabled (Copilot #1, #2). HTTP on
$HTTP_PORT (default 8080) for inter-container traffic — viewer +
webview hit anthias-server over plain HTTP on the Docker network and
cannot validate uvicorn's self-signed cert. HTTPS on $HTTPS_PORT
(default 8443) for external clients. bin/enable_ssl.sh now appends
443:8443 to the compose ports list (instead of using `!override` to
swap 80:8080 for 443:8080), so port 80 stays available for backward
compatibility and the Docker-network HTTP port keeps working.
* Drop CSRF_TRUSTED_ORIGINS = ['http://*', 'https://*'] (Copilot #3).
Verified via Django shell: those leading wildcards are ignored by
Django 4.2 (only subdomain wildcards like https://*.example.com are
honoured), so the setting was a no-op. Same-origin POSTs still pass
through Django's built-in Origin/Host check.
* Re-add channels.security.websocket.AllowedHostsOriginValidator to
the WebSocket router (Copilot #5). Currently a no-op under
ALLOWED_HOSTS=['*'], but tightening ALLOWED_HOSTS later will now
also tighten /ws.
Smoke test (dev + SSL override):
- HTTP http://localhost:8000/ -> 200
- HTTPS https://localhost:8443/ -> 200
- HTTP http://localhost:8443/ -> 000 (TLS-only, expected)
- internal http://localhost:8080/ -> 200
- 15 view tests still pass.
Note: Copilot #4 (Docker-bridge CIDR is bypassable via the published
port) is documented in views_files.py as defense-in-depth and matches
the original nginx posture; switching to app-layer auth is out of
scope for this PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(ssl): switch from in-uvicorn TLS to a Caddy sidecar
The previous SSL implementation gave anthias-server two uvicorn
listeners (HTTP + HTTPS) so the viewer/webview could keep talking
plain HTTP over the Docker network while external clients got TLS.
That dual-listener dance is non-zero overhead and complicates signal
handling. Switch to the standard reverse-proxy pattern instead.
When SSL is enabled by bin/enable_ssl.sh:
* anthias-server stays a single uvicorn listener on plain HTTP 8080
(no SSL_CERTFILE/SSL_KEYFILE knobs, no dual-port logic).
* A Caddy sidecar (caddy:2-alpine, only present when the override is
installed) terminates TLS on host port 443, redirects 80→443, and
reverse-proxies to anthias-server:8080 — so X-Forwarded-Proto /
X-Forwarded-For are forwarded as-is by Caddy.
* The override removes anthias-server's external port mapping
(`ports: !override []`), so all external traffic must enter through
Caddy and the IP allowlists in views_files.py see the original LAN
client IP rather than the docker-bridge gateway. Inter-container
traffic is unchanged.
* `FORWARDED_ALLOW_IPS=*` is set on anthias-server in the override —
safe because anthias-server is no longer reachable from outside the
Docker network — and `SECURE_PROXY_SSL_HEADER` is added in Django
settings so request.is_secure() returns True for HTTPS callers.
* When SSL is *not* enabled there is zero new container, zero new
config — the base compose file is untouched and Caddy isn't pulled
or run.
bin/disable_ssl.sh now also removes the anthias-caddy container
before deleting the override, so HTTPS-only state is fully reversed.
Smoke-tested with a temporary Caddy override:
- HTTPS via Caddy: 200
- HTTP via Caddy: 301 → https://...
- Direct anthias-server: refused (port mapping dropped by override)
- WebSocket upgrade: 101 Switching Protocols
- request.is_secure() with X-Forwarded-Proto=https: True
- 15 anthias_app view tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(views_files): document IP-allowlist threat model
Spell out exactly when the docker-bridge CIDR check is and isn't a
real perimeter:
* No-SSL default: anthias-server is published as 80:8080, so requests
arrive with REMOTE_ADDR set to the docker bridge gateway (172.x) and
LAN clients aren't actually excluded. Trying to plug the gap with
auth would be security theatre — credentials would travel in
plaintext over the LAN anyway.
* SSL via the Caddy sidecar: Caddy terminates TLS, rewrites
X-Forwarded-For, uvicorn honours it (FORWARDED_ALLOW_IPS=*), and the
check sees the real client IP — so the bypass is closed for any
deployment that actually cares about confidentiality.
This is documentation only; no behavioural change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ssl): add --domain (auto Let's Encrypt) + drop openssl shim
bin/enable_ssl.sh now has three modes instead of two:
* Default (no args) — Caddy issues per-SNI certs lazily from its
built-in local CA via `tls internal { on_demand }`. Drops the
openssl self-signed-cert generation step entirely; Caddy persists
the CA in the anthias-caddy-data volume and rotates leaf certs
itself. Browsers still warn (CA is local) but no openssl/cert
hygiene is needed on the host.
* `--domain example.com [--email you@example.com] [--staging]` —
Caddy auto-issues + renews from Let's Encrypt. Caddy auto-creates
the HTTP→HTTPS redirect for hostname sites. Use `--staging` to point
at the ACME staging endpoint while testing, so the production rate
limits aren't burned.
* `--cert /path/to/cert.pem --key /path/to/key.pem [--domain ...]` —
unchanged: bring your own cert, Caddy serves it as-is with
`auto_https off`.
Verified:
- All three Caddyfiles pass `caddy validate`.
- Default mode end-to-end: HTTPS=200 with cert from "Caddy Local
Authority - ECC Intermediate", per-SNI SANs (DNS:localhost,
IP Address:192.168.99.99 etc.), HTTP→HTTPS=301, /ws upgrade=101,
anthias-server's external port mapping is dropped so direct access
is refused.
Docs (CLAUDE.md, docs/README.md, docs/developer-documentation.md)
updated to describe the Caddy sidecar instead of in-uvicorn TLS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address self-review findings on PR #2757
* Gate SECURE_PROXY_SSL_HEADER on FORWARDED_ALLOW_IPS
(anthias_django/settings.py): without the gate, a client on a
plain-HTTP deploy could send `X-Forwarded-Proto: https` and flip
`request.is_secure()`. Django reads the header from META directly,
independent of uvicorn's --proxy-headers flag, so the previous
unconditional setting was actually exploitable in non-SSL mode
(secure-cookied sessions would drop on the next plain-HTTP request,
redirects would point at https:// URLs that don't exist).
Verified live: non-SSL → SECURE_PROXY_SSL_HEADER is None and
is_secure() with spoofed XFP=https returns False; SSL via Caddy
override → header is set and is_secure() returns True.
* Replace the isfile() pre-check + open() in anthias_assets and
static_with_mime with a try/except FileNotFoundError around open()
(anthias_app/views_files.py). Eliminates a (tiny but real) TOCTOU
window between the stat and the open. IsADirectoryError handled
too, since `realpath('/dir/')` resolves to the directory and open()
would otherwise 500.
* Comment FORWARDED_ALLOW_IPS=* assumption in bin/enable_ssl.sh: the
wildcard is only safe because the override drops anthias-server's
external port mapping, so any future edit that re-adds a host:port
publication has to either tighten the wildcard to Caddy's IP/CIDR
or unset it.
* Replace ANSI-C escape sequences in the Caddyfile generator with
plain multi-line strings. `read -r -d ''` was the first attempt
but it strips trailing newlines, which collapsed `auto_https off`
onto the same line as `}` in cert mode. Multi-line literals with
echo "$VAR" are unambiguous and Caddy validates all three modes
cleanly again.
* Add a docker-volume cleanup hint to bin/disable_ssl.sh: Caddy's
local CA persists in anthias_anthias-caddy-data so an enable →
disable → enable cycle reuses the same CA (intentional — browsers
that trusted it stay trusted), and operators who want a fresh CA
now have the exact `docker volume rm` command in the script's
output.
15 view tests still pass; default + SSL Caddyfiles still validate;
default + SSL endpoints still return 200 / 301 / 101 in smoke tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address Copilot's host/MIME hardening feedback
Two security tightenings on top of the prior SECURE_PROXY_SSL_HEADER
gate (which Copilot flagged on a stale snapshot — that one's already
fixed in
|
||
|
|
3c96b541a1 |
refactor: rename legacy 'screenly' dirs to 'anthias' with auto-migration (#2753)
* refactor: rename legacy 'screenly' dirs to 'anthias' with auto-migration
For legacy reasons the host directories storing the cloned repo, user
assets, and config + DB still carried the old 'screenly' name. Rename
all three to their 'anthias' equivalents, plus the in-container paths,
the screenly.db / screenly.conf filenames, /tmp/screenly.watchdog,
/etc/sudoers.d/screenly_overrides, the ansible role, and the nginx URL
location. Existing installations are migrated automatically:
~/screenly/ -> ~/anthias/
~/screenly_assets/ -> ~/anthias_assets/
~/.screenly/ -> ~/.anthias/
screenly.db -> anthias.db
screenly.conf -> anthias.conf (paths rewritten in the body)
/etc/sudoers.d/screenly_overrides -> /etc/sudoers.d/anthias_overrides
Migration is driven by two new helpers:
- bin/migrate_legacy_paths.sh: idempotent host-side rename. Self-relocates
if invoked from inside the dir being renamed. Rewrites both relative
and absolute path values inside screenly.conf. Leaves dir-level
back-compat symlinks at the old paths and file-level symlinks
(screenly.db, screenly.conf) inside the migrated config dir so
user automation / one-version downgrade still find familiar names.
- bin/migrate_in_container_paths.sh: defensive /data/.screenly and
/data/screenly_assets symlinks invoked from the container start
scripts, in case an older docker-compose.yml is still mounting the
legacy paths during a partial upgrade.
Wired into bin/install.sh (renames ~/screenly before clone_repo, then
runs the in-repo helper after) and bin/upgrade_containers.sh (runs the
helper near the top before regenerating docker-compose.yml).
Out of scope (intentional): the screenly/anthias-* Docker Hub namespace,
the Screenly/Anthias GitHub repo URLs, the screenly_ose Balena fleet,
api.screenlyapp.com / apt.screenlyapp.com legacy URLs, and brand URLs
in docs.
Tests: added tests/test_migrate_legacy_paths.py (4 cases: full migration,
absolute-path conf rewrite, idempotent rerun, fresh-install no-op) and
tests/test_backup_helper.py::RecoverLegacyTarballTest (recover() still
accepts pre-rename .tar.gz backups). Ruff clean. All 6 new tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style: apply ruff format to new test files
CI's `ruff format --check` flagged tests/test_backup_helper.py and
tests/test_migrate_legacy_paths.py. Reformatted; behaviour unchanged,
6/6 migration-related tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: suppress SonarCloud S5042 on write-mode tarfile.open in fixtures
The two new fixture-building calls in tests/test_backup_helper.py use
`tarfile.open(..., 'w:gz')` (write mode), which Sonar's python:S5042
rule flags as "expanding this archive file" without distinguishing
read from write. arcnames are hardcoded test inputs with no
path-traversal surface, so the warning is a false positive here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address Copilot review feedback
- lib/backup_helper.py: harden recover() against tar path traversal
(Zip Slip / CVE-2007-4559). New _safe_tar_member() rejects absolute
paths, '..' components, non-regular-non-directory members
(symlinks/hardlinks/devices), members outside the allowed top-level
dirs, and any post-normalisation path that escapes $HOME. Iterates
members manually instead of bulk extractall(), and passes
filter='data' on Python with PEP-706 extraction filters
(3.11.4+/3.12+) for belt-and-suspenders defence.
- tests/test_backup_helper.py: BackupHelperTest now patches HOME to a
per-test tmpdir so `tearDown` no longer rmtree's a real ~/anthias
checkout when run on a developer workstation. Also added
test_recover_skips_path_traversal_member, which proves a hostile
tarball entry like `../evil.txt` is logged-and-skipped, not written
outside $HOME.
- docs/raspberry-pi5-ssd-install-instructions.md: capitalise "This"
after the period.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: add missing leading slash to repo dir heading
The heading for the cloned repo dir was rendered as
`home/${USER}/anthias/`, while every other heading in the section uses
absolute paths like `/home/${USER}/.anthias/`. Same fix applied to the
legacy-path mention in the note below it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c7ec6ea771 |
chore(build): replace webpack, npm, and jest with bun (#2746)
* chore(deps): manage Python deps via uv dependency-groups Replaces the six service-scoped requirements*.txt files with PEP 735 dependency-groups in pyproject.toml and rebuilds every Docker image as a two-stage build: a uv-builder stage (using the official ghcr.io/astral-sh/uv image, with a pip fallback for armv6) produces /venv via `uv sync --group <svc>`, which the runtime stage copies in. uv.lock becomes authoritative for all services. requirements/requirements.host.txt is kept as a committed, auto-generated artifact (`uv export --group host`) so bin/install.sh and the Ansible role keep working; a python-lint CI step enforces it stays in sync. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(deps): bump Django, cryptography, pyOpenSSL, and 5 others - Django 4.2.29 → 4.2.30 (latest 4.2 LTS) - cryptography 3.3.2 → 46.0.7 (capped by pyOpenSSL 26's `cryptography<47`; cryptography 47 is incompatible with the latest pyOpenSSL) - pyOpenSSL 19.1.0 → 26.0.0 (required by newer cryptography ABI — pyOpenSSL 19 crashed at import against cryptography ≥ ~3.4) - requests 2.32.5 → 2.33.1 (aligned across every group, including docker-image-builder and local) - pyasn1 0.6.2 → 0.6.3 - redis 7.1.0 → 7.4.0 - Cython 3.2.3 → 3.2.4 - sh 1.8 → 2.2.2 (major bump; usages in celery_tasks.py, bin/wait.py, lib/utils.py stick to the stable `sh.<cmd>` + `sh.ErrorReturnCode_N` API — verified still works) - python-vlc 3.0.20123 → 3.0.21203 `mako` and `flatted` were requested but skipped: `mako` was already removed from the project ( |
||
|
|
ee12387b06 |
chore(deps): manage Python deps via uv dependency-groups (#2744)
* chore(deps): manage Python deps via uv dependency-groups
Replaces the six service-scoped requirements*.txt files with
PEP 735 dependency-groups in pyproject.toml and rebuilds every
Docker image as a two-stage build: a uv-builder stage (using the
official ghcr.io/astral-sh/uv image, with a pip fallback for
armv6) produces /venv via `uv sync --group <svc>`, which the
runtime stage copies in. uv.lock becomes authoritative for all
services. requirements/requirements.host.txt is kept as a
committed, auto-generated artifact (`uv export --group host`) so
bin/install.sh and the Ansible role keep working; a python-lint
CI step enforces it stays in sync.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(deps): bump Django, cryptography, pyOpenSSL, and 5 others
- Django 4.2.29 → 4.2.30 (latest 4.2 LTS)
- cryptography 3.3.2 → 46.0.7 (capped by pyOpenSSL 26's `cryptography<47`;
cryptography 47 is incompatible with the latest pyOpenSSL)
- pyOpenSSL 19.1.0 → 26.0.0 (required by newer cryptography ABI —
pyOpenSSL 19 crashed at import against cryptography ≥ ~3.4)
- requests 2.32.5 → 2.33.1 (aligned across every group, including
docker-image-builder and local)
- pyasn1 0.6.2 → 0.6.3
- redis 7.1.0 → 7.4.0
- Cython 3.2.3 → 3.2.4
- sh 1.8 → 2.2.2 (major bump; usages in celery_tasks.py, bin/wait.py,
lib/utils.py stick to the stable `sh.<cmd>` + `sh.ErrorReturnCode_N`
API — verified still works)
- python-vlc 3.0.20123 → 3.0.21203
`mako` and `flatted` were requested but skipped: `mako` was already
removed from the project (
|
||
|
|
5c66a38743 |
feat: add support for Raspberry Pi OS Trixie (#2732)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
29ae072514 |
chore: replace Poetry with uv for managing host dependencies (#2611)
|
||
|
|
4591206466 | docs: add note that Trixie is not yet supported (#2531) | ||
|
|
07572c6b49 |
docs: fix the Debian iso download link (#2494)
Now that Debian 13 has been release, the previous link points to Debian 13 iso which is not compatible with current version of Anthias. |
||
|
|
89e6182871 | chore: migrate to TypeScript (#2359) | ||
|
|
8d12dff1d4 | docs: update docs for installing using the release images | ||
|
|
9a55aa6cbc | docs: add guide on how to get started with the admin site | ||
|
|
87fbddeacc |
chore(ci): replace NPM lint script with lint:check and lint:fix (#2301)
|
||
|
|
51e4511bba | feat: migrate to React (#2265) | ||
|
|
ad26782aba |
Update documentation with further information. (#2240)
* Screenshot of rpi-eeprom-update output. * Update raspberry-pi5-ssd-install-instructions.md Updated information regarding boot order and bootloader versions based on testing. * Update raspberry-pi5-ssd-install-instructions.md Update with further information after testing with Pi5/Balena OS * Update docs/raspberry-pi5-ssd-install-instructions.md Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> * Update docs/raspberry-pi5-ssd-install-instructions.md Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> * Update docs/raspberry-pi5-ssd-install-instructions.md Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> * Update docs/raspberry-pi5-ssd-install-instructions.md Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> * Update docs/raspberry-pi5-ssd-install-instructions.md Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> * Update docs/raspberry-pi5-ssd-install-instructions.md Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> * Update docs/raspberry-pi5-ssd-install-instructions.md Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> * Update docs/raspberry-pi5-ssd-install-instructions.md Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> --------- Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
15cdfa030c | Update installation-options.md | ||
|
|
85f3dfd8e0 | Update installation-options.md | ||
|
|
1473080ab4 |
Update docs/raspberry-pi5-ssd-install-instructions.md
Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
a94c7bcbf6 |
Update docs/raspberry-pi5-ssd-install-instructions.md
Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
9cf8252a41 |
Update docs/raspberry-pi5-ssd-install-instructions.md
Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
c02cf28812 |
Update docs/raspberry-pi5-ssd-install-instructions.md
Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
0220c83f05 |
Update docs/raspberry-pi5-ssd-install-instructions.md
Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
3206b242cf |
Update docs/raspberry-pi5-ssd-install-instructions.md
Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
97811ef6a0 |
Update docs/raspberry-pi5-ssd-install-instructions.md
Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
322b880627 |
Update docs/raspberry-pi5-ssd-install-instructions.md
Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
67ef900a93 |
Update docs/raspberry-pi5-ssd-install-instructions.md
Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
6a734dc89e |
Update docs/raspberry-pi5-ssd-install-instructions.md
Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
7eb348808d |
Update docs/raspberry-pi5-ssd-install-instructions.md
Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
1a36330d07 |
Update docs/raspberry-pi5-ssd-install-instructions.md
Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
a36a0ff7b2 |
Update docs/raspberry-pi5-ssd-install-instructions.md
Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
9fa9d3ff58 |
Update docs/raspberry-pi5-ssd-install-instructions.md
Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
b184a08bdf |
Update docs/raspberry-pi5-ssd-install-instructions.md
Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
03e51df7d5 |
Update docs/raspberry-pi5-ssd-install-instructions.md
Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
1fef3c9483 |
Update docs/raspberry-pi5-ssd-install-instructions.md
Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
7f5c5c2fb0 |
Update docs/raspberry-pi5-ssd-install-instructions.md
Co-authored-by: Nico Miguelino <nicomiguelino2014@gmail.com> |
||
|
|
ba35e771be | Add files via upload | ||
|
|
490051585f |
Replace flake8 with ruff (#2092)
|
||
|
|
65d5b83a36 | docs: update info about Pi 5 compatibility (#2174) | ||
|
|
4bcb620a03 | docs: update docs to include balenaOS support for Pi 5 (#2172) | ||
|
|
de804d4f06 | chore: refactor the image builder script into multiple files (#2161) |