mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-17 05:12:34 +02:00
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
273 lines
5.6 KiB
SCSS
273 lines
5.6 KiB
SCSS
.tp__recording-form {
|
|
display: flex;
|
|
flex-flow: column;
|
|
gap: var(--gap-default);
|
|
padding: var(--padding-default);
|
|
min-width: 18rem;
|
|
|
|
&__program {
|
|
display: flex;
|
|
flex-flow: column;
|
|
gap: var(--gap-small);
|
|
}
|
|
|
|
&__row {
|
|
display: flex;
|
|
flex-flow: row;
|
|
gap: var(--gap-default);
|
|
align-items: center;
|
|
|
|
}
|
|
|
|
.tp__input {
|
|
flex-flow: row !important;
|
|
}
|
|
|
|
&__visibility {
|
|
display: flex;
|
|
flex-flow: row;
|
|
gap: var(--gap-default);
|
|
align-items: center;
|
|
}
|
|
|
|
&__filename {
|
|
display: flex;
|
|
flex-flow: row;
|
|
gap: var(--gap-default);
|
|
align-items: center;
|
|
}
|
|
|
|
&__heading {
|
|
font-weight: bold;
|
|
font-size: var(--font-size-lg);
|
|
}
|
|
|
|
&__error {
|
|
color: var(--error-color);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.tp__label {
|
|
font-weight: bold;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.tp__value {
|
|
color: var(--modest-text-color);
|
|
}
|
|
}
|
|
|
|
.tp__recording-library-view {
|
|
display: flex;
|
|
flex-flow: column;
|
|
gap: var(--gap-default);
|
|
padding: var(--padding-default);
|
|
}
|
|
|
|
.tp__recording-quota {
|
|
display: flex;
|
|
flex-flow: row;
|
|
gap: var(--gap-default);
|
|
font-size: var(--font-size-sm);
|
|
color: var(--modest-text-color);
|
|
|
|
span {
|
|
padding: var(--gap-small) var(--gap-default);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--border-radius);
|
|
}
|
|
}
|
|
|
|
.tp__recording-list__empty {
|
|
padding: var(--padding-default);
|
|
color: var(--modest-text-color);
|
|
text-align: center;
|
|
}
|
|
|
|
.tp__recording-row-actions {
|
|
display: flex;
|
|
flex-flow: row;
|
|
gap: var(--gap-small);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.tp__task-edit-form {
|
|
display: flex;
|
|
flex-flow: column;
|
|
gap: var(--gap-default);
|
|
padding: var(--padding-default);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--border-radius);
|
|
margin: var(--gap-default) 0;
|
|
background-color: var(--surface-color, transparent);
|
|
|
|
&__row {
|
|
display: grid;
|
|
grid-template-columns: 12rem 1fr;
|
|
gap: var(--gap-default);
|
|
align-items: center;
|
|
}
|
|
|
|
&__actions {
|
|
display: flex;
|
|
flex-flow: row;
|
|
gap: var(--gap-small);
|
|
justify-content: flex-end;
|
|
}
|
|
}
|
|
|
|
.tp__rule-form {
|
|
display: flex;
|
|
flex-flow: column;
|
|
gap: var(--gap-default);
|
|
padding: var(--padding-default);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--border-radius);
|
|
margin: var(--gap-default) 0;
|
|
background-color: var(--surface-color, transparent);
|
|
|
|
&__row {
|
|
display: grid;
|
|
grid-template-columns: 12rem 1fr;
|
|
gap: var(--gap-default);
|
|
align-items: center;
|
|
|
|
label {
|
|
font-weight: bold;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
input {
|
|
min-width: 12rem;
|
|
}
|
|
}
|
|
|
|
&__actions {
|
|
display: flex;
|
|
flex-flow: row;
|
|
gap: var(--gap-small);
|
|
justify-content: flex-end;
|
|
margin-top: var(--gap-default);
|
|
}
|
|
}
|
|
|
|
// Rule-delete dialog: the destructive choice (also cancel upcoming
|
|
// recordings) is an explicit opt-in, with the default spelled out below
|
|
// it so neither outcome is a surprise.
|
|
.tp__recording-rule-delete {
|
|
display: flex;
|
|
flex-flow: column;
|
|
gap: var(--gap-default);
|
|
padding: var(--padding-default);
|
|
|
|
&__option {
|
|
display: flex;
|
|
flex-flow: row;
|
|
align-items: center;
|
|
gap: var(--gap-default);
|
|
cursor: pointer;
|
|
}
|
|
|
|
&__hint {
|
|
margin: 0;
|
|
font-size: var(--font-size-sm);
|
|
color: var(--modest-text-color);
|
|
}
|
|
}
|
|
|
|
.tp__recording-rule-enabled {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.tp__recording-rule-row-actions {
|
|
display: flex;
|
|
flex-flow: row;
|
|
gap: var(--gap-small);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.tp__recording-rules-view {
|
|
display: flex;
|
|
flex-flow: column;
|
|
gap: var(--gap-default);
|
|
padding: var(--padding-default);
|
|
}
|
|
|
|
.tp__recording-rules__list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: var(--gap-default) 0;
|
|
display: flex;
|
|
flex-flow: column;
|
|
gap: var(--gap-small);
|
|
}
|
|
|
|
.tp__recording-rules__item {
|
|
display: flex;
|
|
flex-flow: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: var(--gap-default);
|
|
padding: var(--gap-small) var(--gap-default);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--border-radius);
|
|
|
|
span {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: var(--modest-text-color);
|
|
}
|
|
}
|
|
|
|
// Advisory conflict badge in the recording form. Colours mirror the task
|
|
// status families: a conflict is a warning about the same lifecycle the
|
|
// status pill reports on, so it should not read as a new vocabulary.
|
|
.tp__conflict-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
white-space: nowrap;
|
|
padding: var(--padding-micro) var(--padding-mini);
|
|
border-radius: var(--tag-border-radius);
|
|
border: 1px solid var(--border-color);
|
|
color: var(--modest-text-color);
|
|
font-size: var(--font-size-sm);
|
|
|
|
&--pending {
|
|
font-style: italic;
|
|
}
|
|
|
|
&--ok {
|
|
color: var(--success-color);
|
|
background-color: var(--success-background-color);
|
|
border-color: var(--ok-color);
|
|
}
|
|
|
|
&--warn {
|
|
color: var(--warn-color);
|
|
background-color: var(--warn-background-color);
|
|
border-color: var(--warn-color);
|
|
}
|
|
|
|
&--danger {
|
|
color: var(--error-color);
|
|
background-color: var(--error-background-color);
|
|
border-color: var(--error-color);
|
|
}
|
|
}
|
|
|
|
// The library could not be served — a stale token, or the DVR switched
|
|
// off server-side. Styled as an alert rather than as the empty state:
|
|
// the two mean different things and the user acts differently on each.
|
|
.tp__recording-list__unavailable {
|
|
padding: var(--padding-default);
|
|
margin: var(--gap-default);
|
|
text-align: center;
|
|
color: var(--error-color);
|
|
background-color: var(--error-background-color);
|
|
border: 1px solid var(--error-color);
|
|
border-radius: var(--border-radius);
|
|
}
|