feat(charting): unify configurable time windows
This commit is contained in:
@@ -86,7 +86,7 @@ class PrometheusChartWidgetConfig(WidgetConfigBase):
|
||||
"""A PromQL range query rendered as a multi-series line chart (SC-101..SC-104)."""
|
||||
|
||||
promql: str
|
||||
window: str = "1h" # one of 1h / 6h / 24h / 7d (see WINDOW_PRESETS)
|
||||
window: Literal["5m", "15m", "30m", "1h", "3h", "6h", "12h", "24h", "2d", "7d", "14d", "30d"] = "1h"
|
||||
# Display scaling for the Y axis + tooltip. "none" shows raw values; the
|
||||
# others auto/force a decimal-prefix unit (kB/MB/GB, kbps/Mbps, etc.).
|
||||
unit: Literal[
|
||||
@@ -116,7 +116,7 @@ class PrometheusMeanWidgetConfig(WidgetConfigBase):
|
||||
"""A PromQL range query averaged client-side into a single value (SC-112..SC-114)."""
|
||||
|
||||
promql: str
|
||||
window: str = "1h" # one of 1h / 6h / 24h / 7d (see WINDOW_PRESETS)
|
||||
window: Literal["5m", "15m", "30m", "1h", "3h", "6h", "12h", "24h", "2d", "7d", "14d", "30d"] = "1h"
|
||||
unit: str | None = None
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from media_library_viewer_api.clients.qbittorrent import QbittorrentClient
|
||||
from media_library_viewer_api.integrations.base import (
|
||||
@@ -83,7 +83,7 @@ class QbittorrentWidgetConfig(WidgetConfigBase):
|
||||
class QbittorrentSpeedWidgetConfig(WidgetConfigBase):
|
||||
"""Speed chart config. The source returns raw bytes/sec; the frontend scales."""
|
||||
|
||||
window_seconds: int = Field(default=1_800, ge=60, le=86_400)
|
||||
window_seconds: int | Literal["all"] = 1_800
|
||||
unit: Literal[
|
||||
"none",
|
||||
"bytes",
|
||||
@@ -95,6 +95,16 @@ class QbittorrentSpeedWidgetConfig(WidgetConfigBase):
|
||||
] = "bytes_per_sec"
|
||||
scale: Literal["auto", "k", "m", "g", "t"] = "auto"
|
||||
|
||||
@field_validator("window_seconds")
|
||||
@classmethod
|
||||
def validate_window_seconds(cls, value: int | str) -> int | str:
|
||||
"""Allow all retained samples while bounding explicit numeric windows."""
|
||||
if value == "all":
|
||||
return value
|
||||
if not isinstance(value, int) or not 60 <= value <= 86_400:
|
||||
raise ValueError("window_seconds must be between 60 and 86400, or 'all'")
|
||||
return value
|
||||
|
||||
|
||||
DEFINITION = ServiceDefinition(
|
||||
service_type="qbittorrent",
|
||||
|
||||
@@ -54,7 +54,8 @@ class SchedulerSample(BaseModel):
|
||||
|
||||
class SchedulerSamplesResponse(BaseModel):
|
||||
service_id: str
|
||||
window_seconds: int
|
||||
window_seconds: int | None
|
||||
all_values: bool = False
|
||||
samples: list[SchedulerSample]
|
||||
|
||||
|
||||
|
||||
@@ -103,14 +103,22 @@ def run_scheduler_action(
|
||||
def get_scheduler_samples(
|
||||
service_id: str,
|
||||
window_seconds: int = Query(default=1_800, ge=60, le=86_400),
|
||||
all_values: bool = Query(default=False),
|
||||
store: SettingsStore = Depends(get_settings_store),
|
||||
) -> SchedulerSamplesResponse:
|
||||
_require_qbittorrent(service_id, store)
|
||||
since_ts = _safe_int(time.time()) - window_seconds
|
||||
samples = QbittorrentSampleStore().window(service_id, since_ts=since_ts)
|
||||
sample_store = QbittorrentSampleStore()
|
||||
if all_values:
|
||||
samples = sample_store.window(service_id)
|
||||
response_window: int | None = None
|
||||
else:
|
||||
since_ts = _safe_int(time.time()) - window_seconds
|
||||
samples = sample_store.window(service_id, since_ts=since_ts)
|
||||
response_window = window_seconds
|
||||
return SchedulerSamplesResponse(
|
||||
service_id=service_id,
|
||||
window_seconds=window_seconds,
|
||||
window_seconds=response_window,
|
||||
all_values=all_values,
|
||||
samples=samples,
|
||||
)
|
||||
|
||||
|
||||
@@ -21,10 +21,18 @@ from typing import Any
|
||||
#: Window presets (SC-108, SC-112). Users pick one of these rather than typing
|
||||
#: raw ``from``/``to``/``step`` values. Values are window lengths in seconds.
|
||||
WINDOW_PRESETS: dict[str, int] = {
|
||||
"5m": 300,
|
||||
"15m": 900,
|
||||
"30m": 1_800,
|
||||
"1h": 3_600,
|
||||
"3h": 10_800,
|
||||
"6h": 21_600,
|
||||
"12h": 43_200,
|
||||
"24h": 86_400,
|
||||
"2d": 172_800,
|
||||
"7d": 604_800,
|
||||
"14d": 1_209_600,
|
||||
"30d": 2_592_000,
|
||||
}
|
||||
|
||||
#: Sentinel values Prometheus serialises for non-finite floats; map these to
|
||||
@@ -36,9 +44,9 @@ def step_for_window(window_seconds: int, target_points: int = 200) -> int:
|
||||
"""Derive a scrape ``step`` for a window that yields ~``target_points`` samples.
|
||||
|
||||
Clamped to a minimum of 15 seconds so Prometheus does not reject
|
||||
sub-15s resolutions on high-cardinality queries. The spec (SC-104) requires
|
||||
the resulting point count to land in the 100–300 band; with
|
||||
``target_points=200`` every preset yields 200 points.
|
||||
sub-15s resolutions on high-cardinality queries. The 5m and 15m presets
|
||||
therefore return 20 and 60 points respectively; all longer presets stay
|
||||
in the target 100–300 point band.
|
||||
"""
|
||||
return max(15, round(window_seconds / target_points))
|
||||
|
||||
|
||||
@@ -480,12 +480,16 @@ class QbittorrentWidgetSource:
|
||||
return {"error": "qBittorrent widget is missing its service"}
|
||||
|
||||
if widget_kind == "speed":
|
||||
window_seconds = _safe_int(
|
||||
config.get("window_seconds") or service.config.get("sample_retention_seconds") or 1_800
|
||||
)
|
||||
window_seconds = max(60, min(window_seconds, 86_400))
|
||||
since_ts = _safe_int(time.time()) - window_seconds
|
||||
samples = QbittorrentSampleStore().window(service.id, since_ts=since_ts)
|
||||
configured_window = config.get("window_seconds")
|
||||
if configured_window == "all":
|
||||
samples = QbittorrentSampleStore().window(service.id)
|
||||
else:
|
||||
window_seconds = _safe_int(
|
||||
configured_window or service.config.get("sample_retention_seconds") or 1_800
|
||||
)
|
||||
window_seconds = max(60, min(window_seconds, 86_400))
|
||||
since_ts = _safe_int(time.time()) - window_seconds
|
||||
samples = QbittorrentSampleStore().window(service.id, since_ts=since_ts)
|
||||
series = [
|
||||
{
|
||||
"label": "download",
|
||||
|
||||
Reference in New Issue
Block a user