Commit Graph

903 Commits

Author SHA1 Message Date
19451068f1 [webapp] Add retry logic and improve renderer registration
- Added automatic retries with exponential backoff to fetchRenderers
- Improved WebRenderer registration: handle stale players, avoid duplicate registrations,
  and force renderers refresh after successful connection
- Bumped version to v0.3.37 (Cargo: 0.3.36 → Cargo.lock mismatch fixed by updating to final version)
2026-04-06 00:22:03 +02:00
23b1ce7661 :recycle!: Normalize UDN comparison and improve WebRenderer detection
- Fix renderer filtering logic to normalize `udn` and id by stripping 'uuid:' prefix
- Replace fragile model_name-based detection with reliable friendly-name check for WebRenderers ('Web Audio – ' prefix)
- Add `manufacturer` field to renderer types and registry
- Update badge count in UnifiedControlView.vue to use filtered renderers
2026-04-06 00:21:37 +02:00
813fc97fa1 ⬆️ version bump to v0.3.6
- Bump crate and app versions from `v0.3.5` to v"\n- Add retry logic with exponential backoff in `fetchRenderers`
- Improve re-registration robustness in `useWebRenderer` (stale player handling, instanceId error recovery)
- Trigger forced renderer refresh after successful registration
2026-04-06 00:21:37 +02:00
1459958978 Merge pull request 'push-nvqvwnmvprww' (#87) from push-nvqvwnmvprww into main
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m29s
Reviewed-on: #87
2026-04-05 21:48:08 +02:00
e055133e4f ⬆️ version bump to v0.3.35 & refactor MediaRenderer stream URL handling
- Bump version to v0.3.35 in Cargo.toml, lockfile and `version.txt`
- Refactor MediaRenderer to accept dynamic stream URL base instead of hardcoded `/api/webrenderer` path
- Remove `PlayerStateReport`, moving it to pmoweb/renderer as local type for better encapsulation
- Update `register_or_reconnect` and device creation to accept renderer name, stream URL base via closure
- Replace `update_player_state` and `get_pending_command`, delegating state/command access directly via instance lookup
- Simplify device creation by using `Device::new_from_config` and removing manual model_name setting
2026-04-05 21:46:59 +02:00
e375d447d4 ♻️ Reorganize crates architecture (T5) — separate core and browser adapters
- Migrate pmomediarenderer: extract all core logic (adapter, handlers, pipeline etc.) into dedicated modules
- Rename types to MediaRenderer* for clarity (Web → Mediarenderer)
pmowebrenderer becomes a pure browser adapter crate
- Update Cargo.toml dependencies accordingly, add pmoserver feature propagation  
PMOMusic: remove obsolete MEDIA_RENDERER import
- Update all imports across workspace to reflect new crate boundaries
2026-04-05 21:21:45 +02:00
fe2d755b4a webrenderer : câblage adapter & nettoyages terminés
- Phase 1.5/2.6 : adapter BrowserAdapter instancié dans WebRendererInstance et câblé aux handlers (Flush/Stop/Pause via deliver())
- Phase 2.4 : Flush envoyé au device sur TrackEnded (Weak<dyn DeviceAdapter> évite les cycles de référence)
- Phase 1.6 : méthodes browser-spécifiques supprimées de RendererRegistry (set_player_command, has_current_uri…), remplacée par get_instance() + accès direct adapter/pipeline
- handlers.rs : flush/stop et pause livrés via l'adapter dans stop_handler/pause_handle
- register.rs : endpoints /play, pause et set_uri mis à jour pour utiliser l'adapter
- pipeline.rs : adapter exposé dans PipelineHandle et passé au event listener via Weak
2026-04-05 20:37:27 +02:00
c13de9f46b webrenderer architecture evolution (Phases P0–P4)
- Phase 1: Introduce DeviceCommand enum and BrowserAdapter in adapter.rs
- Fix P0 bug (play_handler now checks URI before state change)
- Phase 2: Wire flac_handle.pause/resume into pause_handler/stop/play
- Add VecDeque<DeviceCommand> to RendererState, replace Option<Value>
- Phase 3: Add AudioContext + exponential backoff reconnect in PMOPlayer.ts
- Fix position format (seconds_to_upnp_time) and add /nowplaying, /state endpoints
- Phase 4: Register new HTTP routes in config.rs and implement handlers
2026-04-05 15:02:57 +02:00
32e3a18895 ♻️ refactor(webrenderer): architecture device-agnostic et finition browser
- Introduit DeviceAdapter trait pour isoler la livraison aux devices (browser, Android Auto…)
- Remplace player_command Option<serde_json::Value> par VecDeque typée DeviceCommand
- Corrige P0 (play_handler bloqué sans URI) et résout les bugs de pause/flush non transmis au browser
- Ajoute AudioContext dans PMOPlayer.ts pour réduire le buffer décodé (~5s → ~50ms)
- Implémenter auto-reconnect sur coupure réseau HTTP
- Unifie le format de position en HH:MM:SS (UPnP)
- Ajoute endpoints JSON /nowplaying et /state pour les futurs adaptateurs
— Restructuration modulaire préparée (core/ vs browser/) pour facilité l'ajout de devices
2026-04-05 14:44:42 +02:00
76a1c89137 🔧 Refactor PMOPlayer and WebRenderer command handling
- Extract beforeunload handler in useWebrenderer.ts to ensure consistent unregister() calls
- Remove unused AudioContext from PMOPlayer and clean up destroy()
  - Add guaranteed final report via sendBeacon during destruction
- Simplify playStream() in PMOPlayer by removing URL rewriting logic (now handled server-side)
- Update play_handler to pass instance_id and store stream command atomically
  - Avoid race conditions when setting current_uri before sending Play control event
- Remove obsolete WebSocket message types from messages.rs (now using HTTP polling)
  - Keep only core PlaybackState enum
- Fix registry::get_player_command to avoid nested locks on HashMap + Sharedstate Arcs  
- Add missing PlaybackState::Transitioning in pipeline.rs TrackEnded event
- Refactor renderer service variable registration using helper add_var() function
2026-04-05 14:32:30 +02:00
fe9dd9aba6 ♻️ refactor(pmowebrenderer): factoriser handlers et supprimer duplications
- Remplacer les Arc+Box::pin manuels par la macro action_handler!(captures(...))
- Supprimer helpers locaux dupliqués dans renderer.rs (add_var, add_action)
- Extraire handlers génériques pour GET requests
- Factoriser extraction metadata dans set_uri_handler /set_next_uriHandler  
- Simplifier build_renderingcontrol en retirant pipeline inutile
- Mettre à jour edition Rust de 2021 vers 2024 dans tous les Cargo.toml
- Corriger pattern matching inutile `ref` sur déréférencement dans pmoaudio et pmoflac
2026-04-05 14:09:46 +02:00
512dcd1803 ♻️ refactor(renderer): remplacer macros par fonctions et centraliser la logique
- Remplacer les macro_rules! `add_arg_in` et `add_out`, ainsi que la duplication manuelle d’ajout de variables/actions, par des fonctions explicites (`add_arg_in`, `all arg_out`, etc.)
- Introduire des fonctions utilitaires génériques pour `add_var`, `add_action` afin d’unifier la gestion des erreurs
- Mettre à jour tous les services UPnP (`build_avtransport`, `renderingcontrol_service_builder` et autres) pour utiliser les nouvelles fonctions
- Supprimer le code redondant dans la construction des actions (ex: ~12 lignes dupliquées)
- Ajouter le fichier de plan `.kilo/plans/17xx-kind-falcon.md` décrivant la refonte
- Vérifier que les imports et signatures sont cohérents (ex: `&Arc<StateVariable>`)
2026-04-05 12:05:24 +02:00
623ce44774 (feat) Simplify UPnP argument definitions with macros
Introduce add_arg_in! andadd_outmacros to reduce boilerplate in UPnP action argument setup. Refactor all actions (Play, Stop,...GetPositionInfo) to use macros instead of repetitive add_argument calls. Also remove unused blank line in pipeline.rs.
2026-04-05 11:52:13 +02:00
6b0bfe86e3 🗑️ Remove unused imports, macros and fields
- Drop unnecessary `axum::extract::{Path(State)}` imports in config.rs and registry.rs
- Mark unused field `_position_sec` with `#[allow(dead_code)]`
- Add missing attribute to dead code struct
- Remove unused macros `add_action_arg!`, etc. and related types (`PlayerReport`)
2026-04-05 11:39:48 +02:00
d5b1ed5635 refactor(handlers): clean up UPnP action handlers and pipeline command logic
- Add documentation comments to all AVTransport, RenderingControl & ConnectionManager handlers
- Simplify play_handler: remove redundant state writes and clarify flow with comments  
- Deduplicate pipeline command logic in registry.rs by introducing send_pipeline_command helper
- Replace direct PlayerCommand usage with unified PipelineControl enum in registry and handlers  
- Fix handler signatures: clone pipeline once before Arc closure (next/previous)—avoid redundant clones
- Add helper macros for UPnP service factory to reduce boilerplate (add_action, add_var)
- Minor formatting fixes: sort imports and align handler assignments
2026-04-05 11:37:36 +02:00
546e8a782f 🗑️ Remove unused imports, macros and dead code
- Drop `Path`/``State``` from unused Axum imports in config.rs and registry
- Mark `_position_sec` field as `#[allow(dead_code)]`` in PositionUpdateRequest and PlayerStateReport
- Remove unused macro rules (`add_action_arg!`, `add_action!``, `` add_var!)``
- Delete unused PlayerReport struct and related handler code
2026-04-05 11:37:36 +02:00
340c69cb2b ⬆️ ureq v2→v3, add continuous stream detection
- Updateurequest dependency from v2 to latest major (v3.14)
- Add ureq as optional dependency in pmoaudio-ext
- Introduce is_continuous flag to UriSource and PlayerState for proper StreamType handling (Finite vs Continuous)
- Implement detect_continuous_stream() with URL pattern matching and HTTP HEAD header inspection (ICY, chunked encoding)
- Update send_track_boundary() to accept StreamType parameter
2026-04-04 18:00:47 +02:00
8924552696 Add StreamType for multi-client support and improved UPnP control
- Introduce `Streamtype` enum (Continuous vs Finite) to distinguish radio streams from finite tracks
- Enrich `TrackBoundary` sync marker with stream type for proper pause behavior per mode (silence vs backpressure)
- Update all sources and sinks to pass `StreamType` when creating track boundaries
  - Radio Paradise, HTTP source → Continuous (infinite)
- Improve UPnP control architecture: pause sends silence for radio, blocks pipeline via backpressure for tracks
- Prepare groundwork for multi-client DSP architecture with shared source and per-DSP pipelines
2026-04-04 14:37:32 +02:00
33b279ff1e Merge pull request 'push-kvzwrolnknzx' (#86) from push-kvzwrolnknzx into main
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m5s
Reviewed-on: #86
2026-04-04 12:59:09 +02:00
6fcecaab00 🔖 version bump to v0.3.29
- Bump crate and file versions from `v0.3.29` to v\[correction: actually 0\.3\.33] (see Cargo.toml & version.txt)
- Extend cover URL transformation logic to queue items in `pmoserver_ext.rs`—now transforms album art for both current track *and* queue items
- Add debug logging to cover URL transformation steps (local route, instance match & proxy)
- Minor cleanup: remove redundant `clone()` in base_url usage
2026-04-04 12:58:22 +02:00
97a82f4e1d ⬆️ refactor(sse): migrate event processing to fully async
- Convert `renderer_event_to_payload` and `media_server_sse::event_handler`s to async
- Replace sync cover URL transformation with `.await` on `transform_cover_url`
- Remove blocking thread-based workarounds for async execution
-	Improve SSE scalability and responsiveness by eliminating nested runtimes
2026-04-04 12:51:08 +02:00
e0ef485e04 (feat) Use async cover URL transformation in SSE endpoints
- Replace sync proxy_cover_url_sync calls with async versions using dedicated tokio runtime in transformCoverUrlSync
- Update comments to reflect usage of asynchronous logic for cover URL transformation
2026-04-04 12:41:40 +02:00
21ea77eadc 🎨 Simplify album art URL transformation logic
Refactor queue item cover image handling to directly access `album_art_uri` from the top-level struct instead of nested metadata, and update return type to use fully qualified path `crate::openapi::QueueSnapshot` for clarity.
2026-04-04 12:09:47 +02:00
ea5936717a Add pmocovers integration for external cover URL proxying
- Introduce optional `pmocover` dependency in pmocontrol
- Add cover URL transformation logic for both REST and SSE endpoints using `pmocovers::proxy_cover_url`/sync
- Refactor `/covers/proxy?...=` handler to accept any external URL and return local cached route
- Implement helper functions `transform_cover_url` (async) & sync variant for consistent cover URL normalization
- Update Cargo.lock to include `pmocovers`
2026-04-04 12:05:10 +02:00
bdaee820ad Add cover proxy for external LAN media servers
- Implement new /covers/proxy endpoint to cache and rewrite external LAN cover URLs
- Add url/urlencoding dependencies for URL parsing/escaping in pmocontrol and pmocovers Cargo.toml
- Transform album_art_uri fields to use proxy endpoint for LAN URLs in REST and SSE handlers (browse_container, MetadataChanged)
- Add URL validation logic to detect LAN vs public URLs and avoid self-caching
- Update Cargo.lock with new dependencies
2026-04-04 10:42:46 +02:00
718c0d2aed 🔧 refactor(base_url): centraliser gestion des URLs absolues via middleware
- Ajout d'un BaseUrl layer dans pmoserver pour gérer les URLs absolues (LAN/WAN)
- Renommage de `covers_absolute_url_for` → ` covers_relative_route`, stocker les routes relatives
- Mise à jour des appels UPnP vers `covers_absolute_url_for_upnp` (fallback PMO_SERVER_URL)
- Correction des tâches de fond pour stocker les routes, pas l'URL complète
- Suppression du feature gate `simd` inutilisé dans pmoaudio/src/lib.rs
2026-04-04 10:14:51 +02:00
ff699d220f 🔧 Add #[allow(dead_code)] to unused items
- Suppress dead code warnings for utility functions, enums and traits not yet used in production
- Reorganize imports to follow module conventions (e.g., `DeviceIdentity` moved earlier in openhome_renderer.rs)
- Improve formatting of ClientMessage variants for readability
These changes prepare codebase groundwork without altering runtime behavior.
2026-04-04 01:03:51 +02:00
77d0d73ed7 🗑️ Remove unused imports and format code
- Comment out `use std::simd::*` in pmoaudio as it's unused and modules import their own SIMD
- Remove `Instant` from imports in track_metadata.rs (unused)
- Reorder anyhow import to match Rust convention (`anyhow, Result` → `Result`) and remove unused imports
- Improve XML response logging readability with line breaks in iterator chain
+ Refactor `get()` method to multi-line for clarity and consistency
2026-04-04 00:57:37 +02:00
34260e7cbd 🔧 fix unused variable warning and suppress must-use lint in macros
- Prefix `_object_id` parameter with underscore to silence unused variable warning in `get_item`
- Add #[allow(unused_must_use)] to all action macro definitions in pmoupnp/macros.rs
- Remove redundant doc comment line break
2026-04-04 00:54:16 +02:00
d81e7a9413 :sparkles!: add async-trait dependency for WebAppExt trait
- Add `async_trait` as an optional依赖ency in pmoapp/Cargo.toml
- Enable usage of `#[async_trait]` on WebAppExt trait and its impl for Server
- Guard async-related code with `#[cfg(feature = "pmoserver")]`
2026-04-04 00:43:48 +02:00
6352a43e27 correct compilation warning in pmoqobuz 2026-04-04 00:34:55 +02:00
54ea7b5813 add async_trait support for UpnpApiExt trait and impl
- Add `async-trait` dependency import
- Annotate UpnpApiExt trait and its Server impl with `#[async_trait]` to enable async methods in traits
2026-04-04 00:23:03 +02:00
5a9e156566 Merge pull request 'push-kqxnunnmpyxt' (#85) from push-kqxnunnmpyxt into main
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m43s
Reviewed-on: #85
2026-04-04 00:04:09 +02:00
169e536011 🚀 v0.3.32 — Add NoMedia handling & debug logging for Chromecast auto-advance
- Bump version to v0.3.32 (Cargo.toml, Cargo.lock &version.txt)
- Add detailed debug logging in ChromecastRenderer::playback_state()– tracks apps, media entries & player state
- Implement proper handling of PlaybackState::NoMedia to support auto advancing after track ends (Chromecast-specific behavior)
- Reorganize imports in musicrenderer.rs for clarity
- Minor cleanup: remove unused import, fix formatting
2026-04-04 00:03:18 +02:00
5e3fdd1841 + retry logic with exponential backoff for Chromecast connections
- Add `connect_with_retry()` method to handle connection failures with exponential backoff (200ms, 400ms)
- Replace all direct `connect_to_device()` calls with either the new method or an inlined retry loop (for thread-local context)
- Improve robustness of Chromecast operations by automatically reconnecting on transient failures
2026-04-04 00:02:59 +02:00
bf03c56649 ♻️ refactor : externaliser utilitaires string et time
- Déplacer la fonction `simpleHash` de useCoverImage.ts vers un nouveau fichier utils/string.ts
- Déplacer la logique d'analyse des durées HH:MM[:SS] vers un nouveau fichier utils/time.ts
- Créer les fonctions utilitaires `addCacheBust`, `normalizeUrl` et `truncate`
- Mettre à jour les imports dans useCoverImage.ts,useRenderers.ts
-Rendre le code plus réutilisable et maintenable
2026-04-04 00:02:59 +02:00
5d081654ef ♻️ refactor(useRenderers): delegate to centralized getters
Replace direct accessors on snapshotState with dedicated getter functions (e.g., getRendererById, getStateByid) to eliminate duplication and improve maintainability. This change centralizes renderer/snapshot access logic, ensuring consistency across composables.
2026-04-04 00:02:59 +02:00
57995e0bdf 🚀 v0.3.41 — Add NoMedia playback state handling & improve Chromecast logging
- Bump version to v0.3.41
- Add comprehensive debug logging in Chromecast renderer for app/media status detection (app name, player state)
- Implement robust `NoMedia` playback handling for auto advancing on track end (especially important Chromecast behavior)
- Reorganize imports in `musicrenderer.rs` for clarity
- Fix minor import ordering and remove unused blank line
2026-04-04 00:02:59 +02:00
3a3dc1727f [feat] Ajout d'un cache centralisé pour les images de couverture
- Nouveau fichier imageCache.ts : service singleton gérant le cache mémoire, les subscriptions et la gestion centralisée des retries
- Refonte de useCoverImage.ts pour utiliser le nouveau cache, avec support du caching serveur (/api/covers) et backoff exponentiel
- Ajout d'une version simplifiée useCover() pour les cas simples
2026-04-03 22:37:04 +02:00
0cb3cbe470 [webapp] Optimise le chargement des snapshots renderers avec batch controlé
- Ajoute la fonction fetchBatchSnapshots pour limiter le nombre de requêtes simultanées (concurrency=3) et évite la saturation réseau avec un délai entre les batches
- Remplace le chargement séquentiel des snapshots par une approche batchée dans DashboardView.vue
- Introduit un état de chargement (isLoadingSnapshots) et une indiqueur visuel avec Loader2 animé
- Charge les renderers/servers en parallèle au montage via Promise.all
2026-04-03 22:32:07 +02:00
037ddc99ee ♻️ Centraliser la gestion SSE dans un composable unique
- Création d'un nouveau composant `useSSE` pour centraliser la gestion des connexions SSE
- Remplacement direct de l'import `sse` par le nouveau composable dans :useMediaServers:, useRenderers: et useWebRenderer:
- Refactorisation des fonctions `ensureSSEConnected` → ensureSCEInitialized pour utiliser le composable centralisé
- Ajout d'une méthode `connect()` explicite dans chaque usage pour garantir la connexion au bon moment
- Suppression des variables `sseConnected` (doublons) et remplacement par un état partagé via `useSSE`
- Ajout de hooks spécialisés : useRendererEvents et useMediaServerEvent pour simplifier les abonnements filtrées
- Nettoyage des imports obsolètes et harmonisation de la gestion d'état SSE
2026-04-03 22:27:02 +02:00
226297510a [DIAG] Add queue state diagnostics and fix OpenHome playlist sync
- Added diagnostic logging before/after queue operations in control_point.rs and internal/openhome queues
- Fixed OpenHome playlist sync to preserve currently playing track instead of clearing queue with delete_all() when current item is in new playlist
- Added safety check for empty OpenHome playlists to avoid unintended queue clearing due to stale cache/device state
- Minor import reordering and comment cleanup
2026-04-03 22:12:03 +02:00
2fd4c84e1a Add queue refreshing indicator to UI
- Introduce `QueueRefreshing` event type for dedicated queue refresh state
- Add reactive tracking of refreshing queues via new `queueRefreshingIds` Set in useRenderers composable
- Display visual indicator (spinning icon + text) when queue is refreshing in QueueViewer component
- Update `picomatch` dependency to v4.0.4 (patch)
- Minor cleanup in package-lock.json files
2026-04-03 20:46:00 +02:00
494cc3b8e1 ♻️ improve playlist reattachment and enhance OpenHome state logging
- Skip renderer queue clear when rebinding to same container, triggering gentle refresh instead
- Add detailed tracing for STOP commands across renderers and OpenHome clients  
- Improve `playback_state()` error handling with fallback to Transitioning for empty states
- Log queue state (length, current index/track ID) in `play_next`
- Add caller location tracing to OpenHome transport actions (`seek_id`, `delete_all`)
- Enhance IdArrayResponse logging with raw XML children for debugging
- Support both `<Value>` and <State> elements in Transport State responses
2026-04-03 16:18:20 +02:00
c0d1e1a3d5 Add tracing logs and ReadList caching for OpenHome queue
- Add detailed tracing logs in control_point, music_renderer, and openhome_renderer for queue length checks, auto_play flag consumption, playback state changes, and OpenHome transport state mapping.
- Introduce a 500ms TTL cache for ReadList results in OpenHomeQueue to reduce redundant SOAP calls, especially during sync_queue operations.
- Update all queue modification methods to invalidate the new read_list_cache.
- Add debug logging for pivot search results in sync_queue.
2026-03-31 10:02:27 +02:00
843f474a98 feat: ajouter la recherche dans les serveurs media
Implémente une fonctionnalité de recherche pour les serveurs media (Qobuz et autres) avec:
- Nouvelle API endpoint /servers/{serverId}/search côté backend
- Fonction searchServer dans useMediaServers.ts pour gérer la requête
- Composants MediaBrowser.vue et ServerDrawer.vue avec barre de recherche interactive
- Support des résultats mixtes (albums, artistes, pistes, playlists)
- Gestion des états de chargement, erreur et résultat vide
- Affichage conditionnel selon le mode navigation/recherche
2026-03-30 06:36:32 +02:00
ec852c0002 feat: ajouter middleware Axum BaseUrl pour centraliser les URLs HTTP
Supprimer les fichiers obsolètes de configuration Claude (.claude/CLAUDE.md et .claude/hooks/preToolUse.sh), mettre à jour .gitignore pour ignorer les répertoires Claude, et ajouter une spécification détaillée pour implémenter un middleware Axum `BaseUrl` afin de centraliser la construction des URLs HTTP en fonction des headers X-Forwarded-*, évitant les hardcodages d'IP locale dans les réponses au frontend.
2026-03-29 19:49:26 +02:00
8ca47953d2 Merge pull request 'feat: améliorer la gestion des métadonnées de flux en continu avec dc:date' (#84) from push-zmzoxmlkqmmz into main
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m14s
Reviewed-on: #84
2026-03-26 00:47:12 +01:00
0f20a11666 feat: améliorer la gestion des métadonnées de flux en continu avec dc:date
- Ajout d'une méthode de travail structurée pour éviter le 'vibe programming'
- Version bump à 0.3.30
- Correction de chrono dans pmocontrol Cargo.toml (suppression d'optional)
- Dans MusicRenderer : utilisation de dc:date pour calibrer track_start_time sur la diffusion réelle des flux continus
- Ajout d'une fonction parse_rfc3339_to_system_time
- Dans pmoradiofrance : remplacement de la durée dynamique (temps restant) par une durée fixe calculée à partir des bornes start_time/end_time, et ajout de start_time dans les métadonnées DIDL via dc:date
- Correction du calcul de l'élément 'now playing' dans PullResponse pour utiliser les bornes temporelles exactes
- Mise à jour de version.txt
2026-03-26 00:46:39 +01:00
4fc42393d8 Merge pull request 'push-sswqtxmyqtlw' (#83) from push-sswqtxmyqtlw into main
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m4s
Reviewed-on: #83
2026-03-25 21:14:51 +01:00