The transition graph landed as the single source of truth for state
changes, but the frontend never saw it. `action_availability` restated
the rules in TypeScript-shaped Rust: its own pausable set, its own
cancellable set, its own resumable-kind check. Two copies of a state
machine, and the module docstring already recorded that they had drifted
once before -- the queue would pause a task in any state while the
frontend offered pause for three.
`RecordingAllowedActions` moves to `shared` and rides on
`RecordingTaskDto`, computed by `allowed_actions` from the same
predicates the commands themselves use. The frontend now narrows that
set by permission and renders it:
pause: can_manage && allowed.pause
State rules appear once, on the server.
The frontend tests that asserted "Live cannot be paused" and "VOD can be
retried" are gone rather than ported. They were restating the backend's
table through a second implementation, and the backend already proves it
exhaustively over kind x state x command. What is left tests what the
frontend actually decides now: that a command the server would refuse is
never offered, and that manage and delete gate separately.
`the_dto_offers_exactly_what_the_transition_graph_permits` walks all
three kinds across all nine states and compares the DTO against the
graph, so the projection cannot quietly stop matching.
The field is `#[serde(default)]`: a client that has not been rebuilt
sees an empty set and offers no controls, which fails closed.
The resume validator was always constructed with ..ResumeValidator::default(),
so expected_etag and expected_last_modified were permanently None and the
ETag/Last-Modified branches of validate_resume_response were unreachable.
The checks were written and tested, but nothing fed them: a provider that
replaced a file between an interruption and the resume returned a
well-formed 206 at the requested offset, and those bytes were appended to a
partial belonging to the previous resource. The result is a silently
corrupt recording that reports success.
RecordingMetadata now persists the validators captured from the first
response, so they survive a restart. Only a fresh transfer may set them; a
resume keeps the ones its partial was written against.
Weak ETags are discarded rather than stored. RFC 9110 forbids a weak
validator in a range request because it promises only semantic equivalence,
so two responses can share a weak tag and still differ byte for byte. A
response that downgrades a previously strong tag to a weak one now fails
validation instead of passing it by absence.
filter_request_header also rejected only host and connection, so a
configured Range or If-Range became a client default header and collided
with the offset a resumable transfer asks for. The transport-owned set now
covers the range and conditional headers plus Content-Length and
Transfer-Encoding, matched case-insensitively.
The queue was a single JSON document rewritten in full on every mutation.
That cannot survive a torn write, and a change to the record shape had no
upgrade path other than refusing to load — load_from_disk fails closed on
an unknown schema_version, which would have stranded every operator on a
version bump.
Replace it with RecordingRepository: one record per task in a B+Tree, with
every write routed through BPlusTreeRecoveryJournal. A deleted or corrupt
database is now rebuilt from the recovery history instead of being lost,
and a future record-shape change becomes a migration step rather than a
refusal to start.
- backend/repository/src/recording_repository.rs owns the persisted shape
and RecordingRecoverySchema V1. It knows nothing about execution: the DVR
decides what a task means and hands over a set to commit atomically.
- commit() diffs the candidate against what is stored, so the recovery
journal stays proportional to what actually changed rather than to the
size of the queue.
- The partition (queued/scheduled/active/finished) is stored explicitly.
Deriving it from the state would be wrong: `active` is a distinct slot
and two tasks in the same state can sit in different partitions.
- Repository calls run on spawn_blocking; it fsyncs both the journal and
the B+Tree, so it must not run on a runtime worker thread.
RecordingTaskState moves to shared, along with the label() helper and the
TransferStatusDto conversion that were inherent impls in the DVR. The
repository crate cannot depend on the DVR, and the state is part of the
persisted contract either way.
Recovery generations are written under backup_dir, so they survive the loss
of the storage volume.
There is no migration from recordings_state.json: an existing queue is
discarded on upgrade.
The request body was declared twice: CreateRecordingTaskBody in the REST
handler and CreateRecordingTaskRequest in the frontend client. The two had
to agree by hand, and they had already drifted — the frontend typed
visibility as a String and built "private"/"shared" literally at three call
sites, so a typo compiled cleanly and only failed once the server rejected
the body.
Move both the request and its source identifiers into shared as
CreateRecordingRequest / RecordingSourceRequest and have each side use
them. visibility is now the RecordingVisibility enum end to end, so
visibility_to_wire returns a value the server can parse by construction.
Both types deny unknown fields: a client sending a field this build does
not understand has a different idea of what it is asking for, and
recording the wrong thing is worse than refusing. Tests cover the exact
minimal JSON, an injected "url" field, an unknown visibility and an
unknown source field.
Also widen the availability preflight to accept any recording permission.
It answers "is the DVR usable" and carries no recording data, so gating it
on read-or-create alone would have refused a manage-only principal.
A single write permission could not express the policy the DVR needs: a
user who may request a recording is not necessarily one who may cancel
someone else's, and neither implies the right to delete a file. Replace it
with recording.create, recording.manage and recording.delete, and map each
action onto the one it actually needs:
- create private/shared -> recording.create
- edit, cancel, manage rules -> recording.manage
- delete, system retention sweep -> recording.delete
The split renumbers every permission bit above recording.read, so
CURRENT_PERMISSION_SCHEMA_VERSION is bumped to 4 and tokens issued earlier
fail closed at the validator instead of having their bits reinterpreted.
The removed recording.write name now decodes to nothing, so a groups file
that still lists it loses the permission rather than silently gaining one
of the three replacements.
Permission bit values are frozen in a test: they are the wire format, and
reordering the enum would reinterpret every issued token.
New Features
Added recording availability checks before opening recording forms, with clear service-status errors.
Added configurable recording settings for retention, disk limits, quotas, notifications, priority, and filename templates.
Recording configuration is now available in video settings.
Added validation for recording quotas, ranges, and unsigned values.
Bug Fixes
Improved recording progress updates and configuration preservation.
Recording navigation visibility now follows recording permissions.
Style
Improved muted and debug log-console contrast across themes.
Translations
Updated English, Arabic, and Russian recording-related messages.
New Features
Dialogs can now be stacked, allowing multiple dialogs to remain open while keeping only the topmost dialog interactive.
Added improved dialog layering and dismissal behavior for nested dialogs.
Improvements
Updated the Yew framework and related routing and hooks libraries.
Improved dialog handling so background layers remain visible without responding to clicks or results.
New Features
Disk-space alerts can now be enabled and saved from the Web UI.
Selected radio buttons now have clearer background, text, border, and focus styling.
Messaging settings preserve configured disk-alert thresholds, including default values.
Bug Fixes
Disabled disk-space alerts are correctly removed when deselected.
Messaging notification types now accept standard and case-insensitive variant names.
Improved reliability when saving messaging configuration changes.
New Features
Added a /ready readiness endpoint with clear initializing, exhausted, and ready states.
Improved health-banner behavior for shared provider capacity, disabled providers, aliases, and idle fallback capacity.
Bug Fixes
Improved capacity calculations with shared provider grouping, overflow-safe totals, and empty-capacity handling.
Documentation
Documented liveness, readiness, and detailed status endpoints, including Docker and orchestrator usage.
* feat(parser): support tvg-type VOD/Series in M3U and synthesize SeriesInfo
- recognize tvg-type=movie|vod|video as Xtream VOD, series|episode as
Series, and live explicitly, taking precedence over extension inference
- for series rows, treat group-title as the show name and #EXTGRP only as
the default Xtream series category, keeping @Group mappable
- group series rows by category + show name and synthesize
SeriesStreamProperties/SeriesInfo via the existing Xtream series path
- extract SxxEyy from titles; generate deterministic u32 episode IDs;
use numeric tvg-id as series ID when available
- build docker with rust 1.95 (project requires rustc >= 1.95.0)
* fix(xtream): serve embedded seasons/episodes for M3U-synthesized SeriesInfo
M3U-synthesized SeriesInfo items carry their series details in
additional_properties but were not recognized by the stream info endpoint
(item_type is not local, no provider URL), so get_series_info returned an
empty array and IPTV clients showed no seasons/episodes.
Route items with embedded details through the local info-document path so
embedded seasons/episodes are serialized by to_info_document.
* fix(parser): normalize series category in M3U series-map key
feat: complete DVR and improve streaming, security, configuration, and UI
Complete the Digital Video Recorder subsystem and add a broad set of
reliability, security, streaming, configuration, processing, and Web UI
improvements across Tuliprox.
DVR:
* complete live recording and provider-aware VOD download support
* add recording queue, workers, scheduling, and recurring recording rules
* add conflict detection and capacity-aware scheduling
* add pause, resume, retry, edit, cancel, and delete workflows
* add recording quotas and configurable retention policies
* add crash recovery and startup reconciliation
* add durable lifecycle notifications with per-channel retries
* add DVR health monitoring and diagnostic tooling
* add secure access to recordings, thumbnails, and subtitles
* add WebSocket notifications for recording and rule changes
* add Web UI management for recordings, rules, progress, and task state
* add RBAC, configuration, documentation, and i18n support
Streaming and HLS:
* fix shared-stream idle handling and release dead provider streams correctly
* stop tee streams when both client and cache consumers are gone
* cancel provisioning probes when client streams terminate
* fix transient HLS origin work accounting and intermittent 503 responses
* make stream buffer byte limits configurable
* make shared subscriber idle timeout configurable
* make initial HLS manifest wait timeout configurable
* add configurable TS chunk packet count
* add configurable HLS refresh failure backoff
* centralize redirect limits and retry jitter handling
* improve provider DNS refresh behavior and failover tuning
* preserve UTF-8 characters in catchup templates
* improve stream history validation and persistence error handling
Security:
* use constant-time credential comparisons
* harden library and media path handling against traversal and symlink escapes
* only trust forwarded client IP headers from configured trusted proxies
* redact credentials and sensitive URL data from logs
* reject invalid authentication status-code configuration
* deny users with unresolved plans or invalid content filters
* improve authentication error handling across proxy and HLS endpoints
Configuration and reliability:
* prevent invalid api-proxy.yml reloads from terminating the running server
* fully validate API proxy configuration before persisting changes
* log configuration and EPG cleanup failures instead of silently discarding them
* keep the last valid configuration active after failed hot reloads
* align backend and shared media-server validation
* remove duplicated path and normalization logic
* improve DNS-store recovery and Windows rename fallback handling
* reject invalid duration, timestamp, and numeric conversions safely
* fix playlist bouquet save error handling
* fix provider record update detection
* fix cache boundary handling
* improve startup and persistence failure diagnostics
Filtering, search, sorting, and processing:
* add field-scoped playlist explorer search
* centralize shared stream-history search field definitions
* extend the filter DSL with string, set, and numeric operators
* add EPG ID, channel number, and detected quality as filterable fields
* add filter dry-run preview API with match statistics and samples
* report filter syntax errors with line and column information
* add natural numeric-aware sorting
* add quality-aware channel deduplication
* add accent-independent deduplication
* move natural sorting and quality detection helpers into shared code
* persist explorer search-field selection across reloads
User plans and content access:
* add reusable API user plans for capability tiers
* support inherited cluster and connection limits with per-user overrides
* add plan-level and user-level content filters
* enforce content filters across Xtream, M3U, direct playback, resource access,
stream info, short EPG, categories, and XMLTV
* add trial plans with automatic expiry and Trial status
* add plan selection and content filtering to the user editor
* add full plan management to the API configuration Web UI
* migrate the API user database to schema V7 with plan and filter persistence
Web UI and accessibility:
* add live logging console to the stats page
* improve login error handling and prevent duplicate authentication requests
* add keyboard navigation to tabs, menus, tables, and search
* add ARIA roles, labels, validation state, and live-region feedback
* add confirmation dialogs for destructive actions
* add unsaved-change warnings and Ctrl/Cmd+S shortcuts
* add loading, progress, empty, and in-flight states across views
* improve dropdown and single-selection behavior
* add clipboard and credential-copy helpers
* persist table pagination and explorer search preferences
* improve error recovery when UI context providers are unavailable
* remove multiple panic-prone unwrap and browser API paths
* replace remaining hardcoded UI strings with translation keys
Maintenance:
* resolve backend and frontend compiler and Clippy warnings
* update packages and test fixtures
* consolidate duplicated helpers and validation logic
* improve documentation for configuration, filters, plans, DVR, and REST APIs
* add and update tests for migrations, filters, deduplication, sorting,
configuration, streaming, and accessibility behavior
* **Performance**
* Improved memory usage and persistence speed for large playlists and electronic programme guides.
* Large EPG datasets now process more reliably while preserving channel priorities and cleaning up temporary data.
* Reduced allocation overhead during parsing and playlist processing.
* **Bug Fixes**
* Prevented programme descriptions from being truncated during XMLTV imports.
* Improved handling of empty or single-source EPG data.
* **Network**
* Optimized response compression by avoiding compression for small or already-compressed media files.
- **New Features**
- Added configurable QoS snapshot compaction to periodically reclaim storage from expired snapshots.
- Compaction runs daily by default and can be disabled by setting the interval to `0`.
- QoS config hot-reload now recognizes compaction interval changes.
- Older saved snapshot checkpoints remain compatible with the new setting.
- **Documentation**
- Updated configuration examples and reverse-proxy QoS documentation to include `compaction_interval_secs`, its default, and disable behavior.
* **New Features**
* Added a STRM export option to control whether generated folder/file names use media metadata or the processed title.
* STRM naming now prefers the processed title by default; enabling the option switches to metadata-derived names.
* Added a UI chip/tag and editor control for the setting.
* **Documentation**
* Updated STRM configuration docs with the new option, its default, and its effect.
* Added localization text for the setting in supported languages.
* **Tests**
* Added/updated tests to verify filename behavior for videos and series with and without the option.
* **New Features**
* Catch-up streams can now auto-detect whether to serve direct video or an HLS playlist.
* Added archive-aware HLS session/identity handling and improved upstream user-agent propagation for manifests.
* Enhanced M3U timeshift/archiving, including nested handling and native Flussonic archive support; Short EPG can advertise archive availability.
* **Bug Fixes**
* Provider session headers now persist across retries and deferred-open scenarios.
* Legacy HLS routing is now cache-aware; seek detection improvements.
* Stalker errors now better redact sensitive URL details.
* Fix: Preserve VOD client range requests
* **Tests**
* Expanded coverage for probing, archive/timeshift resolution, header overlay rules, and related routing.
* **New Features**
* Added **Force MPEG-TS** / `disable_hls_streaming` for Xtream live streams, rewriting live `.m3u8` requests to `.ts`.
* Added UI controls and option indicators, with localized labels in English, Arabic, and Russian.
* **Documentation**
* Clarified MPEG-TS behavior and added an Xtream example configuration.
* **Bug Fixes**
* Improved live-stream playback routing/extension handling and related permission-failure responses.
* Refined provider allocation and grace-period handling for more consistent connection admission.
* **New Features**
* Added configurable mapping stages for `mapping.yml`: `processing` (default) and `after_epg`.
* `after_epg` mappings now run after EPG enrichment and can react to enriched EPG fields.
* **Bug Fixes**
* Stage-aware playlist mapping: if no mappings match, the source is left unconsumed.
* Mapping directory merges now reject conflicting stages for the same mapping id and report both files clearly.
* **Documentation**
* Updated mapping DSL and target `processing_order` docs to describe stage timing and guarantees.
* **Tests**
* Added coverage for stage-restricted mapping behavior, merge conflicts, and AfterEpg timing.
Bug Fixes
- Input forms now consistently update when switching between configured sources.
- Improved filter matching for captions/titles and more accurate type handling for live/video/series (including series-info variants).
New Features
- Mapping DSL now supports reading @input and @type as metadata sources, while rejecting assignments to them.
- Regex capture results are now more consistently exposed across captures (by index and name).
Tests
- Added WASM browser coverage for input switching and updated coverage for filter/mapping behavior.
Documentation
- Updated mapping DSL documentation and changelog to reflect @input/@type read-only behavior and capture access rules.
Bug Fixes
Fixed playlist counter assignments so values reset correctly during each refresh.
Corrected counter displays to consistently show their configured starting values.
Improved counter comparisons to detect changes in starting values accurately.
New Features
- Added first-class Stalker/Ministra portal support, including live, VOD, series, playback-link refresh, and bulk EPG ingestion.
- Added Stalker configuration and batch/CSV support in the Web UI.
- Added playlist preview and playback handling for Stalker inputs, with support for partial refreshes.
* feat: key shared hls sessions by origin id
* feat: support hls content encoding
* fix: coderabbitai findings
* Fix: Custom headers are lost on provider://
In the manual redirect path, the abstract provider:// URL is compared against the resolved HTTP URL.
This causes the request to be marked as "scrubbed" before the first real redirect even occurs, stripping out headers like X-API-Key.
* fix(strm): name every flat/tmdb version after the folder it lands in
Under `flat: true` the movie folder is deduplicated by TMDB id, but
`final_filename` was derived from each item's own provider title before the
folder-reuse branch. Providers routinely list the same film twice with the tag
written two ways, so the second listing landed in the first listing's folder
under a name that does not start with the folder name.
That is precisely the condition Jellyfin's Emby.Naming VideoListResolver checks
in IsEligibleForMultiVersion: the file name must start with the folder name, and
the remainder must be empty or start with '-', '_', '.', or a [bracketed] token.
Any ineligible file makes GetVideosGroupedByVersion return the folder's videos
ungrouped, so *every* file in it becomes a separate movie.
The first item to claim a flat folder now also fixes the file name for every
item that follows it into that folder. The existing add_quality_to_filename
suffix, or the existing [Version id#N] collision suffix when the qualities tie,
is what keeps the versions apart. Kodi and Emby shared the same defect and are
fixed with them.
* fix(quality): classify resolution from the frame, not its height alone
VideoResolution was bucketed from ffprobe's height only. Widescreen releases keep
the full raster width but encode fewer active lines, so a 2.40:1 film mastered at
1080p (1920x796) was tagged '720p HD', and a 2.40:1 UHD master (3840x1600) was
tagged '1440p QHD'.
Classify both axes and keep the higher tier. Height still decides on its own when
the width is missing, which also keeps anamorphic frames (1440x1080) correct.
* fix(strm): drop the category from flat movie file names
The category suffix was added to flat movie file names 'to avoid collisions',
but with folder reuse now naming files after their folder, the only files that
can collide are versions of the same movie -- same TMDB folder, same quality
string -- and the existing [Version id#N] pass already separates those.
It is not free: Jellyfin and Emby render whatever follows the folder name as the
version label, so the category was showing up in the version picker next to the
quality. Removing it makes the label read as the quality alone.
Items without a TMDB id keep the category, because it is what makes their folder
unique -- but they now carry it in the file name as well. Previously the folder
got '[Category]' while the file did not, so the file name did not start with the
folder name and version detection was broken for exactly those items.
* fix(strm): keep the version label when the writer truncates a long name
The [Version id#N] suffix is the only thing separating two versions that share a
base and a quality string, but it was appended last and the writer truncates the
file stem to 250 chars -- so for a long enough title the suffix was cut off, both
versions resolved to the same path, and the second silently overwrote the first.
The collision detector could not see it either, since it compares untruncated
names.
Trim the shared base to fit instead, so the label always survives. The 250-char
limit is now a named constant used by both the naming and the writing side.
Also hoist the per-item flag lookups out of the naming loop and drop a redundant
HashSet lookup (insert already reports whether the entry was present).
* docs: scope the no-tmdb category note to jellyfin/emby
Shared HLS cache session feature and aligns the runtime, cache, provisioning, recovery, custom-response, and documentation paths with the new shared-session model.
* perf: binary-search EPG programme lookup
Replaces the linear position() scan with a binary search via partition_point for EPG programmes. Reduces complexity to O(log p).
* perf: cache interned PlaylistItemType label
Avoids repetitive hash-map lookups during sorting and filtering. The five fixed labels are now interned once via OnceLock and returned as a cheap Arc clone.
* perf: avoid per-event String allocation in web-UI XMLTV parser
Uses borrowed Cow types instead of allocating a new String for every element name. Tracks the active text state using a small TextTag enum.
* perf: throttle per-chunk download control poll
Throttles the control_signal lock polling to a 200ms interval on multi-GB downloads to prevent CPU thrashing. Immediate actions (pause/cancel) remain guaranteed via control_notify.
* perf: pre-size filtered channel buffers in apply_filter_to_playlist
Pre-allocates the filtered channel vector based on the source group length. Eliminates step-by-step reallocations on large playlists.
* fix: surface playlist cache load failures instead of silent empty fallback
Replaces silent error swallowing with real error logging (distinguishing missing files from corruption) while preserving the empty fallback behavior.
* perf: remove redundant first drain loop in get_remote_content_as_file
Removes a dead, redundant loop. The stream body is now consumed only once, allowing the idle timeout to protect the transfer correctly.
* perf: back off instead of busy-spinning on empty client-stream chunks
Introduces a short Sleep backoff (Poll::Pending) when a provider sends continuous empty keep-alive chunks. Prevents the CPU from spinning hot.
* perf: make library orphan cleanup linear instead of O(n^2)
Reduces the complexity of cleaning up orphaned entries from O(n^2) to linear O(n). Loads the metadata library only once and pre-computes partitions.
* perf: indexed batched merge for series expansion instead of O(n^2) scans
Replaces expensive O(n^2) scans during series expansion with index-based batch processing. Builds temporary lookup indexes and merges groups linearly.
* perf: pre-size group flatten buffers to reduce reallocation churn
Pre-allocates target merge vectors and category indexes to eliminate repeated memory reallocations when flattening large target playlists.
* perf: stream custom provider playlist endpoint instead of materializing it
Switches from full in-memory materialization to lazy conversion and streaming of JSON/binary responses. Massively reduces the memory footprint.
* refactor: centralize InputType batch/family checks behind enum helpers
Moves scattered matches! checks for M3u/Xtream types into centralized enum helpers (is_batch(), etc.) to prevent logic drift.
* refactor: centralize XMLTV tag/attribute literals into named constants
Replaces hardcoded inline strings in the XMLTV parser with named constants (EPG_TAG_TITLE, etc.) as a single source of truth.
* refactor: derive GeoIpUpdateError Display/Error via thiserror
Replaces manual Display and Error implementations with the thiserror macro, allowing new error variants to self-document their formatting.
* refactor: attach per-cluster facts to XtreamCluster instead of scattered matches
Centralizes redundant API actions and collection name mappings directly onto the XtreamCluster struct instead of using scattered match blocks.
* refactor: centralize PlaylistItemType video/series classification
Introduces is_video() / is_series() on the enum to unify duplicate matches! checks across filters, probes, and tracking modules.
* feat: add per-stage summary logs to VOD/series resolve queueing
Adds a concise debug summary per processing stage (total counters and elapsed time) to make hot-path diagnostics easier without raising the log level.
* refactor: generate ItemField get/set from a single direct-field list
Uses a callback macro to synchronously generate both getter and setter blocks for directly-bound fields (Name, Url, etc.) from a single master list.
* refactor: derive input-type routing from a single capability descriptor
Bundles InputType behaviors (backends, connection rules) into a single capabilities() descriptor to prevent bugs when adding new variants.
* refactor: derive output-format routing from a single capability descriptor
Centralizes output format features (filter, EPG, and cache support) into a single TargetCapabilities descriptor instead of spreading logic across no-op match arms.
* Fixed sanitizing url scheme batch and provder
* local date in Stream history
* Fixed EPG programme time display to render in local timezone for improved readability.
* Improved stream history date range filtering to accurately use local date boundaries.
* Enhanced credential sanitization for stream URLs to properly mask sensitive information.
* Fix EPG path selection for mixed Xtream/M3U targets
* Fix EPG selection and complete Xtream series_info for input/custom requests
* Add configurable interner GC tuning
* catchup fixes
* feat(frontend): bookmarkable views via URL hash deep linking
Persist the active view to the URL hash so views are bookmarkable and
survive a page refresh. Restore the view from the hash on load, sync the
hash on navigation, and handle browser back/forward via a hashchange listener.
* feat(dev): add dev container for reproducible development
* feat(frontend): interactive multi-series metric sparklines on stats cards
* feat(frontend): aggregate status health banner with capacity breakdown
* feat(frontend): add guided empty states with hints
* chore(devcontainer): add gh CLI, dev tooling, cache volumes, and tasks
- Install GitHub CLI, git, build-essential, clang, lld, mold, jq
- Add cargo-watch, cargo-llvm-cov, cargo-deny, cargo-machete
- Persist cargo registry, target, and gh config via named volumes
- Fix volume ownership for the dev user in post-create
* chore(vscode): add tasks and launch configs for dev workflow
- tasks.json: backend/frontend dev servers, test, lint, fmt, coverage, deps checks, docs
- launch.json: CodeLLDB configs for backend and workspace tests
* a11y: add accessible labels to inputs and collapsed sidebar buttons
- Input: add aria_label prop, falling back to placeholder when no visible label
- Sidebar: pass translated hint + aria_label to collapsed-mode icon buttons
* feat(frontend): warn on unsaved config changes before page unload
* feat(frontend): handle session expiry with client-side logout
Schedule a client-side logout when the JWT expires, showing a
notification and returning the user to the login screen instead of
silently failing with 401s.
* chore(vscode): update tasks.json
* New Features
Session expiration detection with proactive logout warning
Real-time health status banner showing connectivity and provider status
Live metric sparklines for CPU, memory, and network usage
Deep-linkable views via URL hash with back/forward browser support
Enhanced empty states with guided messages across app sections
Improvements
Faster system metrics sampling (2-second intervals)
Archive and catchup-aware EPG handling for accurate program guides
* New Features
Session expiration detection with proactive logout warning
Real-time health status banner showing connectivity and provider status
Live metric sparklines for CPU, memory, and network usage
Deep-linkable views via URL hash with back/forward browser support
Enhanced empty states with guided messages across app sections
Improvements
Faster system metrics sampling (2-second intervals)
Archive and catchup-aware EPG handling for accurate program guides
* feat(ui): toast progress bar, pause-on-hover, and copy-details for errors
Add a countdown progress bar to auto-dismiss toasts, pause both the timer and bar on hover (resuming with remaining time), and a 'copy details' action on error toasts. Respects prefers-reduced-motion.
* feat(ui): confirm destructive download/recording cancel and remove
Route the download/recording cancel and remove actions through the shared ConfirmDialog (which focuses the safe Cancel button by default), matching the existing confirmation flow for user/target/RBAC deletes.
* feat(ui): runtime-discovered languages with manifest and RTL support
Load available UI languages at runtime from assets/i18n/index.json, add a toolbar language picker (shown when >1 language), persist the choice, and set document dir/lang for RTL languages such as Arabic.
* feat(ui): recoverable error boundary with per-view retry fallback
Add an ErrorBoundary component (context handle + use_error_boundary hook) that shows a recoverable fallback with a retry button instead of blanking a section. Wrap each main view and the API-user playlist individually so a failure in one view stays contained and the rest of the UI keeps working.
* fix(api): use async canonicalize in local file stream handler
Replace the blocking std path.canonicalize() in local_stream_response with tokio::fs::canonicalize().await so the async runtime is not blocked while resolving the local media file path.
* Add Escape-to-close keyboard support for popup menus
Popup menus now close on Escape in addition to outside mouse-down, improving keyboard accessibility.
* Use descriptive alt text for logo and channel logo images
Login and sidebar logo images now use the configured app title, and playlist channel logos use the channel title, improving screen-reader accessibility.
* Add explicit type=button to shared button primitives
IconButton and TextButton now render with type=button to prevent accidental form submission when used inside forms.
* Persist sidebar collapsed state and table page size
The sidebar collapsed/expanded state and the stream-history table page size are now saved to localStorage and restored on reload, matching the already-persisted theme preference.
* Debounce filter editor input parsing
The filter editor textarea no longer re-parses, previews, and emits the filter on every keystroke. The textarea stays responsive via an immediate value while parsing and change notifications are debounced (300ms).
* Show localized message in table empty state
Data/paged tables now render the No content label in their empty state instead of an icon with no text. Sticky headers were already present.
* feat(frontend): improve screen-reader support for sidebar and toasts
* fix(frontend): handle missing window gracefully in sidebar
* feat(frontend): cross-dissolve colors on theme switch
* feat(frontend): add card hover lift, button press feedback, animated chevrons
* Removed code duplicates
- Add tamper-proof provider resolve tokens for STRM links
- Resolve provider:// URLs through a generic provider redirect API
- Allow STRM generation with either Xtream or M3U output
- Reject STRM-only targets because STRM needs a backing stream output