Compare commits
2 Commits
b011d2421b
...
6d46de26c4
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d46de26c4 | |||
| 50c0c9b548 |
@@ -2,7 +2,7 @@
|
||||
dir: backend/src/media_library_viewer_api/clients
|
||||
|
||||
## role
|
||||
Provides external service integration clients (API wrappers and command executors) for media servers, identity providers, torrent clients, and remote/local filesystems.
|
||||
Provides external service integration clients (API wrappers and remote/local command executors) for media library data aggregation and system monitoring.
|
||||
## parent
|
||||
index: backend/src/media_library_viewer_api/.pi-map.index.md
|
||||
map: backend/src/media_library_viewer_api/.pi-map.md
|
||||
|
||||
@@ -4,7 +4,7 @@ dir: backend/src/media_library_viewer_api/clients
|
||||
index: backend/src/media_library_viewer_api/clients/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides external service integration clients (API wrappers and command executors) for media servers, identity providers, torrent clients, and remote/local filesystems.
|
||||
Provides external service integration clients (API wrappers and remote/local command executors) for media library data aggregation and system monitoring.
|
||||
## files
|
||||
- __init__.py | Swaps the position of two tmux panes within a window or between windows | dep: tmux, sh
|
||||
- authentik.py | API client wrapper for Authentik directory service providing paginated user browsing and search via REST API. | exp: class:AuthentikClient, method:__init__(self, base_url: str, api_token: str, timeout), call:base_url.rstrip, call:self.base_url.endswith, call:http_timeout, call:requests.Session, call:self.session.headers.update, raise:ValueError, method:get(self, path: str, **params: Any) → Any, call:params.items, call:logger.debug, call:sorted, call:clean_params.keys, call:self.session.get, call:response.raise_for_status, call:logger.warning, call:response.json, raise:requests.HTTPError, method:users(self, search, page, page_size) → dict[str, Any], call:self.get, call:isinstance, call:logger.warning, call:type, call:payload.get, call:int, call:pagination.get, call:logger.info, call:len | dep: logging, typing, requests, media_library_viewer_api.clients.http_timeout
|
||||
@@ -12,10 +12,10 @@ Provides external service integration clients (API wrappers and command executor
|
||||
- jellyfin.py | Wraps the Jellyfin/Emby HTTP API to provide methods for fetching users, libraries, media items, playback sessions, and image URLs as plain Python dictionaries. | exp: class:JellyfinClient, method:__init__(self, base_url: str, api_key: str, timeout), call:base_url.rstrip, call:self.base_url.endswith, call:http_timeout, call:requests.Session, call:self.session.headers.update, raise:ValueError, method:get(self, path: str, **params: Any) → Any, call:params.items, call:logger.debug, call:sorted, call:clean_params.keys, call:self.session.get, call:response.raise_for_status, call:logger.warning, call:response.json, raise:requests.HTTPError, method:users(self) → list[dict[str, Any]], call:self.get, call:logger.info, call:len, method:resolve_user_id(self, identifier: str | None) → str, call:self.users, call:any, call:str, call:u.get, call:next, call:logger.info, call:logger.warning, raise:RuntimeError, method:libraries(self, user_id: str) → list[dict[str, Any]], call:self.get(f"/Users/{user_id}/Views").get, call:logger.info, call:len, method:items(self, user_id: str, parent_id, start_index, limit, search, include_item_types, recursive, sort_by, sort_order) → dict[str, Any], call:logger.debug, call:self.get, call:str(recursive).lower, method:item_count(self, user_id: str, include_item_types: str, parent_id) → int, call:self.get, call:int, call:response.get, call:logger.debug, method:media_counts(self, user_id: str) → dict[str, int], call:self.item_count, method:library_item_counts(self, user_id: str, libraries: list[dict[str, Any]]) → list[dict[str, Any]], call:lib.get, call:self.item_count, call:results.append, method:sessions(self, active_within_seconds) → list[dict[str, Any]], call:self.get, call:cast, call:isinstance, method:active_sessions(self, active_within_seconds) → list[dict[str, Any]], call:self.sessions, call:session.get, call:logger.info, call:len, method:image_url(self, item_id: str, image_type) → str | dep: logging, typing, requests, media_library_viewer_api.clients.http_timeout
|
||||
- jellyseerr.py | HTTP API client wrapper for Jellyseerr to fetch user data and enrich Jellyfin user lists. | exp: class:JellyseerrClient, method:__init__(self, base_url: str, api_key: str, timeout), call:base_url.rstrip, call:self.base_url.endswith, call:http_timeout, call:requests.Session, call:self.session.headers.update, raise:ValueError, method:get(self, path: str, **params: Any) → Any, call:params.items, call:logger.debug, call:sorted, call:clean_params.keys, call:self.session.get, call:response.raise_for_status, call:logger.warning, call:response.json, raise:requests.HTTPError, method:absolute_url(self, path: str | None) → str, call:path.startswith, method:jellyfin_users(self) → list[dict[str, Any]], call:self.get, call:isinstance, call:logger.info, call:len, call:payload.get, method:users(self, page_size) → list[dict[str, Any]], call:max, call:int, call:self.get, call:isinstance, call:payload.get, call:results.extend, call:page_info.get, call:logger.debug, call:len, call:logger.info | dep: logging, typing, requests, media_library_viewer_api.clients.http_timeout
|
||||
- local.py | Provides a local command execution client that mirrors remote SSH helpers to run POSIX shell commands, list directories, stat paths, and run ffprobe on the API host for built-in local monitoring. | exp: class:CommandResult, class:LocalCommandClient, method:__init__(self, timeout), method:run(self, command: str, timeout) → CommandResult, call:logger.debug, call:subprocess.run, call:CommandResult, call:logger.warning, call:result.stderr.strip, call:result.stdout.strip, method:list_dir(self, path: str) → CommandResult, call:shlex.quote, call:self.run, method:stat_path(self, path: str) → CommandResult, call:shlex.quote, call:self.run, method:ffprobe_json(self, path: str) → dict[str, object], call:shlex.quote, call:self.run, call:json.loads, raise:RuntimeError | dep: json, logging, posixpath, shlex, subprocess, dataclasses
|
||||
- qbittorrent.py | Minimal read-only API client for qBittorrent that handles authenticated session management to fetch sync data. | exp: class:QbittorrentClient, method:__init__(self, base_url: str, username: str, password: str, timeout) → None, call:base_url.rstrip, call:self.base_url.endswith, call:http_timeout, call:requests.Session, raise:ValueError, method:_login(self) → None, call:self._session.post, call:resp.raise_for_status, call:resp.text.strip, call:bool, call:resp.cookies.get, call:logger.info, raise:RuntimeError, method:_get(self, path: str, **params: Any) → dict[str, Any], call:self._login, call:self._session.get, call:logger.debug, call:resp.raise_for_status, call:resp.json, method:maindata(self) → dict[str, Any], call:self._get | dep: logging, typing, requests, media_library_viewer_api.clients.http_timeout
|
||||
- qbittorrent.py | Minimal read-only qBittorrent Web API client that manages authenticated session cookies to fetch torrent data. | exp: class:QbittorrentClient, method:__init__(self, base_url: str, username: str, password: str, timeout) → None, call:base_url.rstrip, call:self.base_url.endswith, call:http_timeout, call:requests.Session, raise:ValueError, method:_login(self) → None, call:self._session.post, call:resp.raise_for_status, call:resp.text.strip, call:bool, call:resp.cookies.get, call:logger.info, raise:RuntimeError, method:_get(self, path: str, **params: Any) → dict[str, Any], call:self._login, call:self._session.get, call:logger.debug, call:resp.raise_for_status, call:resp.json, method:maindata(self) → dict[str, Any], call:self._get | dep: logging, typing, requests, media_library_viewer_api.clients.http_timeout
|
||||
- ssh.py | Provides an SSH client wrapper for remote filesystem inspection and media analysis using paramiko, with POSIX shell command execution and host key management. | exp: class:CommandResult, class:RemoteSSHClient, method:__init__(self, host: str, username: str, port, key_filename, private_key, private_key_passphrase, password, known_hosts_path, timeout), raise:ValueError, method:connect(self) → paramiko.SSHClient, call:paramiko.SSHClient, call:client.load_system_host_keys, call:Path, call:bool, call:has_known_host, call:known_hosts_file.is_file, call:client.load_host_keys, call:client.set_missing_host_key_policy, call:paramiko.RejectPolicy, call:paramiko.AutoAddPolicy, call:self._load_private_key, call:client.connect, call:str(exc).lower, call:known_hosts_file.parent.mkdir, call:client.save_host_keys, raise:RuntimeError, method:close(self) → None, call:self._client.close, method:run(self, command: str, timeout) → CommandResult, call:self.connect, call:shlex.quote, call:logger.debug, call:client.exec_command, call:stdout.channel.recv_exit_status, call:CommandResult, call:stdout.read().decode, call:stderr.read().decode, call:logger.warning, call:result.stderr.strip, call:result.stdout.strip, method:list_dir(self, path: str) → CommandResult, call:shlex.quote, call:self.run, call:logger.info, method:stat_path(self, path: str) → CommandResult, call:shlex.quote, call:self.run, call:logger.info, method:ffprobe_json(self, path: str) → dict[str, Any], call:shlex.quote, call:self.run, call:logger.info, call:json.loads, raise:RuntimeError | dep: json, logging, posixpath, shlex, dataclasses, io, pathlib, typing, paramiko, media_library_viewer_api.services.known_hosts
|
||||
## arch
|
||||
Adapter/wrapper pattern with each client encapsulating service-specific communication (HTTP REST, SSH, local shell) behind a uniform interface returning plain Python data structures.
|
||||
Client-wrapper pattern with per-service classes encapsulating authentication, pagination, and response normalization; consistent dictionary-based data contracts across clients.
|
||||
## tags
|
||||
call:logger.info, error, call:logger.debug, call:self.get, client, init, timeout, call:logger.warning
|
||||
## symbols
|
||||
|
||||
@@ -68,7 +68,7 @@ class QbittorrentClient:
|
||||
# than as a bare HTTPError.
|
||||
if resp.status_code in (502, 503, 504):
|
||||
raise RuntimeError(
|
||||
f"qBittorrent login failed: reverse proxy returned HTTP {resp.status_code} "
|
||||
f"qBittorrent is unreachable: reverse proxy returned HTTP {resp.status_code} "
|
||||
f"for {resp.url}. qBittorrent may be down, starting up, or unable to "
|
||||
"answer within the proxy's forwarding timeout."
|
||||
)
|
||||
@@ -85,9 +85,9 @@ class QbittorrentClient:
|
||||
if body == "Fails.":
|
||||
raise RuntimeError(f"qBittorrent login failed (HTTP {resp.status_code}): invalid username or password")
|
||||
raise RuntimeError(
|
||||
f"qBittorrent login failed (HTTP {resp.status_code}, body={body!r}). "
|
||||
"Expected the text 'Ok.' from /api/v2/auth/login — this usually means "
|
||||
"base_url does not reach the qBittorrent Web API (check the URL, path, "
|
||||
f"Unexpected response from qBittorrent login endpoint (HTTP {resp.status_code}, "
|
||||
f"body={body!r}). Expected the text 'Ok.' from /api/v2/auth/login — this usually "
|
||||
"means base_url does not reach the qBittorrent Web API (check the URL, path, "
|
||||
"and any reverse proxy in front of qBittorrent)."
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: backend/src/media_library_viewer_api/integrations
|
||||
|
||||
## role
|
||||
Defines external service integrations (e.g., Jellyfin, Prometheus, Alertmanager) with configuration models, widget schemas, and data-fetching logic for the media library viewer API.
|
||||
Provides a plugin-based integration layer connecting the media library viewer API to various external services (Alertmanager, Authentik, Jellyfin, Prometheus, qBittorrent, etc.) via a centralized registry.
|
||||
## parent
|
||||
index: backend/src/media_library_viewer_api/.pi-map.index.md
|
||||
map: backend/src/media_library_viewer_api/.pi-map.md
|
||||
|
||||
@@ -4,23 +4,23 @@ dir: backend/src/media_library_viewer_api/integrations
|
||||
index: backend/src/media_library_viewer_api/integrations/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Defines external service integrations (e.g., Jellyfin, Prometheus, Alertmanager) with configuration models, widget schemas, and data-fetching logic for the media library viewer API.
|
||||
Provides a plugin-based integration layer connecting the media library viewer API to various external services (Alertmanager, Authentik, Jellyfin, Prometheus, qBittorrent, etc.) via a centralized registry.
|
||||
## files
|
||||
- __init__.py | Defines a closed registry module for service integrations.
|
||||
- alertmanager.py | Defines the Alertmanager service integration configuration, widget definitions, and alert summarization logic for a media library viewer API. | exp: class:AlertmanagerConfig, class:AlertmanagerAlertsWidgetConfig, func:summarize_alerts(alerts: list[dict[str, Any]], severity_filter) → dict[str, Any], call:alert.get, call:labels.get, call:by_severity.get, call:open_alerts.append, call:annotations.get, call:open_alerts.sort, call:len | dep: typing, media_library_viewer_api.integrations.base
|
||||
- authentik.py | Defines the service configuration model and definition for integrating Authentik as a user directory and identity provider. | exp: class:AuthentikConfig | dep: media_library_viewer_api.integrations.base
|
||||
- alertmanager.py | Defines a service integration for Prometheus Alertmanager, providing configuration models, connection testing, alert summarization, and widget definitions for displaying active alerts. | exp: class:AlertmanagerConfig, class:AlertmanagerAlertsWidgetConfig, func:summarize_alerts(alerts: list[dict[str, Any]], severity_filter) → dict[str, Any], call:alert.get, call:labels.get, call:by_severity.get, call:open_alerts.append, call:annotations.get, call:open_alerts.sort, call:len, func:test_connection(config: dict[str, Any], secrets: dict[str, str], store: SettingsStore) → TestResult, call:str(config.get("base_url") or "").rstrip, call:config.get, call:int, call:secrets.get, call:requests.get, call:resp.raise_for_status, call:resp.json, call:payload.get("versionInfo", {}).get, call:TestResult, call:translate_connection_error | dep: typing, requests, media_library_viewer_api.integrations.base, media_library_viewer_api.services.settings_store
|
||||
- authentik.py | Defines the Authentik service integration for user-directory access, including connection config, API token secret management, and a connection test. | exp: class:AuthentikConfig, func:test_connection(config: dict[str, Any], secrets: dict[str, str], store: SettingsStore) → TestResult, call:str(config.get("base_url") or "").rstrip, call:config.get, call:secrets.get, call:float, call:AuthentikClient, call:client.users, call:result.get, call:isinstance, call:TestResult, call:translate_connection_error | dep: typing, media_library_viewer_api.clients.authentik, media_library_viewer_api.integrations.base, media_library_viewer_api.services.settings_store, media_library_viewer_api.clients.authentik.AuthentikClient
|
||||
- backups.py | Defines a Backups service type with configuration and summary widget for monitoring backup jobs, run history, and alerting. | exp: class:BackupsConfig, class:BackupsSummaryWidgetConfig | dep: media_library_viewer_api.integrations.base
|
||||
- base.py | Provides foundational base classes and dataclasses for defining external service integrations, including config validation and widget schema generation. | exp: class:ServiceConfigBase, class:WidgetConfigBase, class:SecretField, class:WidgetKind, class:ServiceDefinition, method:widget_kind(self, kind: str) → WidgetKind | None, func:_validate_service_base_url(value: Any) → str, call:isinstance, call:value.strip, call:text.lower, call:lowered.startswith, raise:ValueError, func:widget_kind(kind: str, name: str, description: str, model_cls: type[WidgetConfigBase], default_config, refresh_interval_ms) → WidgetKind, call:model_cls.model_json_schema, call:schema.pop, call:WidgetKind, call:dict, func:validate_config(model_cls: type[BaseModel], config: dict[str, Any] | None) → dict[str, Any], call:model_cls.model_validate, call:instance.model_dump | dep: dataclasses, typing, pydantic
|
||||
- jellyfin.py | Defines the Jellyfin media server service configuration, secret fields, and widget definitions for activity and now-playing sessions. | exp: class:JellyfinConfig, class:JellyfinActivityWidgetConfig, class:JellyfinNowPlayingWidgetConfig | dep: media_library_viewer_api.integrations.base
|
||||
- nextcloud.py | Defines the Nextcloud service configuration model and service definition for integration into the media library viewer API. | exp: class:NextcloudConfig | dep: media_library_viewer_api.integrations.base
|
||||
- prometheus.py | Defines the Prometheus service configuration and widget types (metric, chart, gauge, mean) for querying and visualizing PromQL data. | exp: class:PrometheusConfig, class:PrometheusMetricWidgetConfig, class:PrometheusChartWidgetConfig, class:PrometheusGaugeWidgetConfig, class:PrometheusMeanWidgetConfig | dep: media_library_viewer_api.integrations.base
|
||||
- qbittorrent.py | Declares the qBittorrent service definition including config models, secret fields, and three widget kinds (totals, active, speed). | exp: class:QbittorrentConfig, class:QbittorrentWidgetConfig | dep: media_library_viewer_api.integrations.base
|
||||
- base.py | Provides base classes and utility functions for defining external service integrations, including config schemas, secrets, widgets, and connection error translation. | exp: class:ServiceConfigBase, class:WidgetConfigBase, class:SecretField, class:WidgetKind, class:TestResult, class:ServiceDefinition, method:widget_kind(self, kind: str) → WidgetKind | None, func:_validate_service_base_url(value: Any) → str, call:isinstance, call:value.strip, call:text.lower, call:lowered.startswith, raise:ValueError, func:widget_kind(kind: str, name: str, description: str, model_cls: type[WidgetConfigBase], default_config, refresh_interval_ms) → WidgetKind, call:model_cls.model_json_schema, call:schema.pop, call:WidgetKind, call:dict, func:validate_config(model_cls: type[BaseModel], config: dict[str, Any] | None) → dict[str, Any], call:model_cls.model_validate, call:instance.model_dump, func:translate_connection_error(exc: Exception, context) → TestResult, call:str, call:message.lower, call:isinstance, call:TestResult | dep: asyncio, dataclasses, typing, requests, pydantic, media_library_viewer_api.services.settings_store
|
||||
- jellyfin.py | Defines the Jellyfin media server service integration, including connection testing, configuration models, and widget definitions for activity monitoring. | exp: class:JellyfinConfig, class:JellyfinActivityWidgetConfig, class:JellyfinNowPlayingWidgetConfig, func:test_connection(config: dict[str, Any], secrets: dict[str, str], store: SettingsStore) → TestResult, call:str, call:config.get, call:secrets.get, call:int, call:JellyfinClient, call:client.users, call:TestResult, call:len, call:translate_connection_error | dep: typing, media_library_viewer_api.clients.jellyfin, media_library_viewer_api.integrations.base, media_library_viewer_api.services.settings_store, media_library_viewer_api.clients.jellyfin.JellyfinClient, media_library_viewer_api.services.settings_store.SettingsStore
|
||||
- nextcloud.py | Defines a Nextcloud service integration with connection testing and configuration for a media library viewer API. | exp: class:NextcloudConfig, func:test_connection(config: dict[str, Any], secrets: dict[str, str], store: SettingsStore) → TestResult, call:str(config.get("base_url") or "").rstrip, call:config.get, call:requests.get, call:resp.raise_for_status, call:resp.json, call:payload.get, call:TestResult, call:translate_connection_error | dep: typing, requests, media_library_viewer_api.integrations.base, media_library_viewer_api.services.settings_store
|
||||
- prometheus.py | Defines a Prometheus service integration that queries metrics through a Grafana gateway and provides configuration for metric, chart, gauge, and mean widgets. | exp: class:PrometheusConfig, class:PrometheusMetricWidgetConfig, class:PrometheusChartWidgetConfig, class:PrometheusGaugeWidgetConfig, class:PrometheusMeanWidgetConfig, func:test_connection(config: dict[str, Any], secrets: dict[str, str], store: SettingsStore) → TestResult, call:str(config.get("grafana_url") or "").rstrip, call:config.get, call:secrets.get, call:int, call:TestResult, call:requests.post, call:resp.raise_for_status, call:translate_connection_error | dep: typing, requests, media_library_viewer_api.integrations.base, media_library_viewer_api.services.settings_store
|
||||
- qbittorrent.py | Defines the qBittorrent service integration including config models, connection testing, and widget definitions for displaying torrent activity and speeds. | exp: class:QbittorrentConfig, class:QbittorrentWidgetConfig, func:test_connection(config: dict[str, Any], secrets: dict[str, str], store: SettingsStore) → TestResult, call:config.get, call:secrets.get, call:int, call:QbittorrentClient, call:client.maindata, call:data.get("server_state", {}).get, call:TestResult, call:str(exc).lower, call:translate_connection_error | dep: typing, media_library_viewer_api.clients.qbittorrent, media_library_viewer_api.integrations.base, media_library_viewer_api.services.settings_store, media_library_viewer_api.clients.qbittorrent.QbittorrentClient, media_library_viewer_api.services.settings_store.SettingsStore
|
||||
- registry.py | Maintains a closed registry of service definitions and provides lookup functions to query available services, their types, and widget kinds. | exp: func:list_service_types() → list[str], call:sorted, func:get_service_definition(service_type: str) → ServiceDefinition | None, call:SERVICE_DEFINITIONS.get, func:get_widget_kind(service_type: str, widget_kind: str) → WidgetKind | None, call:get_service_definition, call:definition.widget_kind, func:require_service_definition(service_type: str) → ServiceDefinition, call:get_service_definition, raise:ValueError | dep: media_library_viewer_api.integrations.alertmanager, media_library_viewer_api.integrations.authentik, media_library_viewer_api.integrations.backups, media_library_viewer_api.integrations.base, media_library_viewer_api.integrations.jellyfin, media_library_viewer_api.integrations.nextcloud, media_library_viewer_api.integrations.prometheus, media_library_viewer_api.integrations.qbittorrent, media_library_viewer_api.integrations.ssh_tasks
|
||||
- ssh_tasks.py | Defines a service configuration for an SSH task runner that executes reusable saved tasks over SSH and records run history. | exp: class:SshTasksConfig, class:SshTaskOutputWidgetConfig | dep: media_library_viewer_api.integrations.base
|
||||
- ssh_tasks.py | Defines a service plugin that runs reusable saved tasks over SSH by managing connection configuration, secrets, and connection testing. | exp: class:SshTasksConfig, class:SshTaskOutputWidgetConfig, func:test_connection(config: dict[str, Any], secrets: dict[str, str], store: SettingsStore) → TestResult, call:str(config.get("host") or "").strip, call:config.get, call:int, call:ServiceRecord, call:build_ssh_client, call:client.connect, call:str(exc).lower, call:TestResult, call:translate_connection_error, call:client.close | dep: typing, media_library_viewer_api.integrations.base, media_library_viewer_api.services.settings_store, media_library_viewer_api.services.task_runner, media_library_viewer_api.widgets.sources
|
||||
## arch
|
||||
Plugin-style registry pattern with a shared base class hierarchy; each service module independently defines config dataclasses, widget types, and summarization logic, all registered in a central closed registry for discovery and lookup.
|
||||
Registry pattern with a shared base class defining config schemas, secrets, widgets, and connection-testing contracts; each integration is a self-contained module registered in a closed registry for runtime discovery.
|
||||
## tags
|
||||
config, widget, service, integrations, base, media_library_viewer_api, prometheus, definition
|
||||
config, connection, widget, media_library_viewer_api, service, error, integrations, call:str
|
||||
## symbols
|
||||
- AlertmanagerConfig
|
||||
- AlertmanagerAlertsWidgetConfig
|
||||
|
||||
@@ -42,8 +42,10 @@ def test_connection(
|
||||
return TestResult(ok=True, detail="Connected to qBittorrent.", evidence=version)
|
||||
except RuntimeError as exc:
|
||||
lowered = str(exc).lower()
|
||||
if "login failed" in lowered:
|
||||
if "invalid username or password" in lowered:
|
||||
return TestResult(ok=False, detail="Authentication failed — qBittorrent rejected the credentials.")
|
||||
# Gateway timeout, wrong URL/path, empty body, etc. — surface the real
|
||||
# reason instead of masking every login error as an auth failure.
|
||||
return translate_connection_error(exc, context="qBittorrent")
|
||||
except Exception as exc:
|
||||
return translate_connection_error(exc, context="qBittorrent")
|
||||
|
||||
@@ -76,12 +76,28 @@ class TestQbittorrentTestConnection:
|
||||
|
||||
def test_login_failed_translates_to_auth_message(self) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client.maindata.side_effect = RuntimeError("qBittorrent login failed: Fails.")
|
||||
# Mirrors the real _login auth-failure message for a "Fails." body.
|
||||
mock_client.maindata.side_effect = RuntimeError(
|
||||
"qBittorrent login failed (HTTP 200): invalid username or password"
|
||||
)
|
||||
with patch("media_library_viewer_api.integrations.qbittorrent.QbittorrentClient", return_value=mock_client):
|
||||
result = qbit_test({"base_url": "http://qb:8080"}, {"username": "u", "password": "p"}, MagicMock())
|
||||
assert result.ok is False
|
||||
assert "Authentication failed" in result.detail
|
||||
|
||||
def test_gateway_error_does_not_masquerade_as_auth_failure(self) -> None:
|
||||
mock_client = MagicMock()
|
||||
# A 504 from the reverse proxy must NOT be reported as "Authentication
|
||||
# failed" — that misled users into re-entering correct credentials.
|
||||
mock_client.maindata.side_effect = RuntimeError(
|
||||
"qBittorrent is unreachable: reverse proxy returned HTTP 504 for http://qb:8080/api/v2/auth/login."
|
||||
)
|
||||
with patch("media_library_viewer_api.integrations.qbittorrent.QbittorrentClient", return_value=mock_client):
|
||||
result = qbit_test({"base_url": "http://qb:8080"}, {"username": "u", "password": "p"}, MagicMock())
|
||||
assert result.ok is False
|
||||
assert "Authentication failed" not in result.detail
|
||||
assert "504" in result.detail
|
||||
|
||||
def test_connection_error_translates(self) -> None:
|
||||
mock_client = MagicMock()
|
||||
mock_client.maindata.side_effect = requests.ConnectionError("Connection refused")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: frontend/src/components
|
||||
|
||||
## role
|
||||
Reusable UI component library providing presentation-layer building blocks (tables, cards, charts, dialogs, widgets) for the frontend application.
|
||||
Shared UI component library providing reusable React display, table, dialog, card, chart, and widget components for the frontend application.
|
||||
## parent
|
||||
index: frontend/src/.pi-map.index.md
|
||||
map: frontend/src/.pi-map.md
|
||||
@@ -28,6 +28,7 @@ map: frontend/src/.pi-map.md
|
||||
- PinnedServiceLink.tsx
|
||||
- SectionCard.tsx
|
||||
- SelectionRailCard.tsx
|
||||
- ServiceTestPanel.tsx
|
||||
- SessionActivityPanel.tsx
|
||||
- TabbedCard.tsx
|
||||
- WidgetConfigDialog.tsx
|
||||
|
||||
@@ -4,7 +4,7 @@ dir: frontend/src/components
|
||||
index: frontend/src/components/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Reusable UI component library providing presentation-layer building blocks (tables, cards, charts, dialogs, widgets) for the frontend application.
|
||||
Shared UI component library providing reusable React display, table, dialog, card, chart, and widget components for the frontend application.
|
||||
## files
|
||||
- BackupAlertsTable.tsx | Renders a responsive table of backup alerts with severity badges and acknowledge actions, switching between desktop table and mobile card layouts. | exp: func:BackupAlertsTable({ alerts, onAcknowledge }: Props), call:useIsMobile, call:onAcknowledge, call:alerts.map, call:severityVariant, call:formatTimestamp | dep: @/components/ui/badge, @/components/ui/button, @/components/ui/table, @/components/ui/mobile-card, ../hooks/useIsMobile, ../types/backups, useIsMobile hook, BackupAlert type
|
||||
- BackupDashboardWidget.tsx | Displays a dashboard widget summarizing backup job statistics including total jobs, 24-hour success rate, active alerts, and last failure timestamp. | exp: func:BackupDashboardWidget(), call:useBackupDashboard, call:new Date(data.last_failed_at * 1000).toLocaleString | dep: @/components/ui/badge, @/components/ui/card, ../hooks/useBackups
|
||||
@@ -14,18 +14,19 @@ Reusable UI component library providing presentation-layer building blocks (tabl
|
||||
- DialogFooter.tsx | Renders a dialog footer component with cancel, optional secondary action, and confirm buttons, mapping legacy MUI color/variant props to shadcn Button variants. | exp: func:DialogFooter({ onCancel, cancelLabel = "Cancel", onConfirm, confirmLabel, confirmBusyLabel, confirmDisabled, confirmColor = "primary", confirmVariant = "contained", confirmStartIcon, secondaryAction, }: DialogFooterProps), call:resolveConfirmVariant | dep: react, @/components/ui/button
|
||||
- HoverEditButton.tsx | Renders a hover-reveal edit button for desktop and always-visible edit button for mobile, preserving legacy CSS class hooks. | exp: func:HoverEditButton({ onClick, label = "Edit", mobile = "always", }: HoverEditButtonProps), call:e.stopPropagation, call:onClick | dep: lucide-react, @/components/ui/button
|
||||
- LibraryOverview.tsx | Renders a two-column responsive grid displaying movie and TV library counts using shadcn/ui Card components | exp: func:LibraryOverview({ libraries }: Props), call:libraries.filter, call:movieLibs.map, call:lib.total.toLocaleString, call:lib.movies.toLocaleString, call:tvLibs.map, call:lib.series.toLocaleString | dep: @/components/ui/card, ../types
|
||||
- LineSeriesChart.tsx | Renders multiple time-series as a shared recharts line chart with merged data and formatted timestamps. | exp: SeriesPoint, ChartSeries, func:LineSeriesChart({ series, height = 300, }: LineSeriesChartProps), call:mergeSeries, call:formatTime, call:Number, call:series.map | dep: recharts
|
||||
- LineSeriesChart.tsx | Renders multiple time-series as a shared recharts line-chart with merged timestamps. | exp: SeriesPoint, ChartSeries, func:LineSeriesChart({ series, height = 300, }: LineSeriesChartProps), call:mergeSeries, call:formatTime, call:Number, call:series.map | dep: recharts
|
||||
- MetricCard.tsx | Renders a compact metric display card with label, value, and optional subtext using Tailwind CSS styling. | exp: func:MetricCard({ label, value, subtext }: Props) | dep: @/components/ui/card
|
||||
- NowPlaying.tsx | Renders a now-playing panel by wrapping SessionActivityPanel with a specific empty message for user activity sessions. | exp: func:NowPlaying({ sessions, onSelectSession }: Props) | dep: ../types, ./SessionActivityPanel
|
||||
- PinnedServiceLink.tsx | Renders a navigable card-shaped button for pinned service shortcuts on dashboards and provides a helper to construct service target paths. | exp: PinnedServiceLinkProps, func:PinnedServiceLink({ label, target, icon: Icon = Boxes, className, }: PinnedServiceLinkProps), call:useNavigate, call:navigate, call:cn, func:serviceLinkTarget(serviceType: string, serviceId: string, tab: string) → string | dep: react-router-dom, lucide-react, @/lib/utils
|
||||
- SectionCard.tsx | A reusable card component that renders a titled section with optional description and action, built on shadcn/ui Card primitives for comfortable density layout. | exp: func:SectionCard({ title, description, action, children, }: SectionCardProps) | dep: react, @/components/ui/card
|
||||
- SelectionRailCard.tsx | A reusable card component for a selection rail UI with titled header, scrollable body, and optional footer, preserving legacy API compatibility during a migration from MUI. | exp: func:SelectionRailCard({ title, description, children, footer, minHeight = 420, }: SelectionRailCardProps) | dep: react, @/components/ui/card
|
||||
- ServiceTestPanel.tsx | Presentational component rendering a "Test credentials" panel with test button, result display, and a "Save anyway" checkbox. | exp: func:ServiceTestPanel({ result, isPending, saveAnyway, onTest, onSaveAnywayChange, disabled, }: Props), call:onSaveAnywayChange | dep: @/components/ui/alert, @/components/ui/button, ../types
|
||||
- SessionActivityPanel.tsx | Renders a scrollable table displaying live media session activity details with status badges and optional session selection callbacks. | exp: func:SessionActivityPanel({ sessions, emptyMessage = "No live sessions matched to this user.", selectedUserLabel, onSelectSession, }: Props), call:buildStatusSummary, call:sessions.map, call:formatStateLabel, call:onSelectSession, call:sessionStateVariant, call:event.stopPropagation | dep: @/components/ui/badge, @/components/ui/button, @/components/ui/table, ../types
|
||||
- TabbedCard.tsx | Renders a card with a line-style tab bar header and content area, acting as a controlled wrapper around shadcn/ui Tabs for backward-compatible API migration from MUI. | exp: func:TabbedCard({ value, onChange, tabs, children, }: TabbedCardProps), call:onChange, call:String | dep: react, @/components/ui/card, @/components/ui/tabs
|
||||
- WidgetConfigDialog.tsx | Provides a user interface dialog and form for creating, configuring, editing, and managing the sort order of dashboard widgets and widget references. | exp: func:WidgetConfigDialog({ open, onClose, serviceId, dashboardScope, editWidgetId, }: Props), call:useWidgetInstances, call:useServiceInstances, call:useTasks, call:useSaveWidgetInstance, call:useDeleteWidgetInstance, call:useWidgetReferences, call:useCreateWidgetReference, call:useDeleteWidgetReference, call:useDetachWidgetReference, call:useUpdateWidgetReference, call:useState, call:Boolean, call:useEffect, call:instances.find, call:references.find, call:startEdit, call:setDraft, call:SERVICE_REGISTRY[ services.find((s) => s.id === serviceId)?.service_type ?? "" ]?.widgets.find, call:services.find, call:setDraftBaseline, call:onClose, call:saveWidget.mutateAsync, call:reset, call:updateRef.mutateAsync, call:deleteWidget.mutateAsync, call:[...instances].sort, call:references.map, call:[...owned, ...refs].sort, call:useMemo, call:instances.map, call:existingSearch.toLowerCase().trim, call:allWidgets .filter((w) => !onDashboard.has(w.id)) .filter, call:onDashboard.has, call:w.title.toLowerCase().includes, call:w.widget_kind.toLowerCase().includes, call:createRef.mutateAsync, call:deleteRef.mutateAsync, call:detachRef.mutateAsync, call:SERVICE_REGISTRY[ services.find((s) => s.id === draft.serviceId)?.service_type ?? "" ]?.widgets.find, call:useIsMobile, call:String, call:Number, call:combinedWidgets.map, call:bindingLabel, call:moveInstance, call:toggleEnabled, call:handleDetach, call:handleRemoveReference, call:removeInstance, call:setShowExisting, call:setExistingSearch, call:availableWidgets.map, call:handleAddReference, call:Object.values(BUILTIN_WIDGETS).map, call:startAddBuiltIn, call:services .filter((s) => s.enabled) // When scoped to a service Overview, only show widgets for THAT // service instance's type (not all services' widgets). .filter((s) => !serviceId || s.id === serviceId) .flatMap, call:(SERVICE_REGISTRY[s.service_type]?.widgets ?? []).map, call:startAddService, call:handleClose, call:JSON.stringify | dep: react, @/components/ui/dialog, @/components/ui/button, @/components/ui/input, @/components/ui/textarea, @/components/ui/label, @/components/ui/switch, @/components/ui/select, @/components/ui/badge, @/components/ui/alert, lucide-react, ../hooks/useWidgets, ../hooks/useServices, ../hooks/useSettings, ../hooks/useIsMobile, @/components/ui/sheet-form, ../types, ../integrations/registry, @/components/ui/*
|
||||
- WidgetConfigDialog.tsx | Provides a dialog and sheet interface for creating, configuring, reordering, and managing the lifecycle of UI widgets and their dashboard references. | exp: func:WidgetConfigDialog({ open, onClose, serviceId, dashboardScope, editWidgetId, }: Props), call:useWidgetInstances, call:useMemo, call:useServiceInstances, call:useTasks, call:useSaveWidgetInstance, call:useDeleteWidgetInstance, call:useWidgetReferences, call:useCreateWidgetReference, call:useDeleteWidgetReference, call:useDetachWidgetReference, call:useUpdateWidgetReference, call:useState, call:Boolean, call:useEffect, call:instances.find, call:references.find, call:startEdit, call:setDraft, call:SERVICE_REGISTRY[ services.find((s) => s.id === serviceId)?.service_type ?? "" ]?.widgets.find, call:services.find, call:setDraftBaseline, call:onClose, call:saveWidget.mutateAsync, call:reset, call:updateRef.mutateAsync, call:deleteWidget.mutateAsync, call:[...instances].sort, call:references.map, call:[...owned, ...refs].sort, call:instances.map, call:existingSearch.toLowerCase().trim, call:allWidgets .filter((w) => !onDashboard.has(w.id)) .filter, call:onDashboard.has, call:w.title.toLowerCase().includes, call:w.widget_kind.toLowerCase().includes, call:createRef.mutateAsync, call:deleteRef.mutateAsync, call:detachRef.mutateAsync, call:SERVICE_REGISTRY[ services.find((s) => s.id === draft.serviceId)?.service_type ?? "" ]?.widgets.find, call:useIsMobile, call:String, call:Number, call:combinedWidgets.map, call:bindingLabel, call:moveInstance, call:toggleEnabled, call:handleDetach, call:handleRemoveReference, call:removeInstance, call:setShowExisting, call:setExistingSearch, call:availableWidgets.map, call:handleAddReference, call:Object.values(BUILTIN_WIDGETS).map, call:startAddBuiltIn, call:services .filter((s) => s.enabled) // When scoped to a service Overview, only show widgets for THAT // service instance's type (not all services' widgets). .filter((s) => !serviceId || s.id === serviceId) .flatMap, call:(SERVICE_REGISTRY[s.service_type]?.widgets ?? []).map, call:startAddService, call:handleClose, call:JSON.stringify | dep: react, @/components/ui/dialog, @/components/ui/button, @/components/ui/input, @/components/ui/textarea, @/components/ui/label, @/components/ui/switch, @/components/ui/select, @/components/ui/badge, @/components/ui/alert, lucide-react, ../hooks/useWidgets, ../hooks/useServices, ../hooks/useSettings, ../hooks/useIsMobile, @/components/ui/sheet-form, ../types, ../integrations/registry, @/components/ui (dialog, button, input, textarea, label, switch, select, badge, alert, sheet-form)
|
||||
- WidgetInstance.tsx | Renders a widget instance card that dynamically resolves and displays a widget component, with optional edit and copy actions. | exp: func:WidgetInstanceCard({ widget, onEdit, onCopy }: Props), call:useServiceInstances, call:resolveWidget, call:onCopy, call:onEdit | dep: @/components/ui/alert, @/components/ui/button, lucide-react, ../hooks/useServices, ../integrations/registry, ../types, ./SectionCard
|
||||
## arch
|
||||
React functional components with responsive design patterns, built on shadcn/ui and Tailwind CSS with adapter layers for backward-compatible MUI-to-shadcn migration.
|
||||
Presentational React/TypeScript components built on shadcn/ui primitives with responsive (desktop/mobile) layouts, wrapping recharts for visualization and maintaining backward-compatible APIs during migration from MUI.
|
||||
## tags
|
||||
call:use, components, ui, card, widget, table, backup, call:on
|
||||
## symbols
|
||||
|
||||
@@ -72,10 +72,10 @@ export function LineSeriesChart({
|
||||
<Tooltip
|
||||
labelFormatter={(label) => formatTime(Number(label))}
|
||||
contentStyle={{
|
||||
backgroundColor: "hsl(var(--popover))",
|
||||
border: "1px solid hsl(var(--border))",
|
||||
backgroundColor: "var(--color-popover)",
|
||||
border: "1px solid var(--color-border)",
|
||||
borderRadius: "0.5rem",
|
||||
color: "hsl(var(--popover-foreground))",
|
||||
color: "var(--color-popover-foreground)",
|
||||
}}
|
||||
/>
|
||||
{series.map((s, i) => (
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: frontend/src/widgets
|
||||
|
||||
## role
|
||||
Dashboard widget components that render visual data for various integrations (Alertmanager, Backups, Jellyfin, Prometheus, qBittorrent, SSH tasks, static content) within the monitoring application.
|
||||
Provides self-contained dashboard widget components for rendering various data sources (Alertmanager, backups, Jellyfin, Prometheus metrics, qBittorrent, SSH tasks, and static content) in a unified card layout.
|
||||
## parent
|
||||
index: frontend/src/.pi-map.index.md
|
||||
map: frontend/src/.pi-map.md
|
||||
@@ -15,9 +15,9 @@ map: frontend/src/.pi-map.md
|
||||
- BackupsWidget.tsx
|
||||
- JellyfinNowPlayingWidget.tsx
|
||||
- JellyfinWidget.tsx
|
||||
- PrometheusChartWidget.tsx
|
||||
- PrometheusGaugeWidget.tsx
|
||||
- PrometheusMeanWidget.tsx
|
||||
- MetricChartWidget.tsx
|
||||
- MetricGaugeWidget.tsx
|
||||
- MetricMeanWidget.tsx
|
||||
- PrometheusMetricWidget.tsx
|
||||
- QbittorrentActiveTorrentsWidget.tsx
|
||||
- QbittorrentSpeedWidget.tsx
|
||||
|
||||
@@ -4,34 +4,34 @@ dir: frontend/src/widgets
|
||||
index: frontend/src/widgets/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Dashboard widget components that render visual data for various integrations (Alertmanager, Backups, Jellyfin, Prometheus, qBittorrent, SSH tasks, static content) within the monitoring application.
|
||||
Provides self-contained dashboard widget components for rendering various data sources (Alertmanager, backups, Jellyfin, Prometheus metrics, qBittorrent, SSH tasks, and static content) in a unified card layout.
|
||||
## files
|
||||
- AlertmanagerAlertsWidget.tsx | Renders an Alertmanager alerts dashboard widget displaying alert summaries, severity badges, and individual alert details. | exp: func:AlertmanagerAlertsWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:Object.entries(summary.by_severity).map, call:severityVariant, call:alerts.slice(0, 5).map | dep: @/components/ui/alert, @/components/ui/badge, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types
|
||||
- BackupsWidget.tsx | Displays a dashboard widget showing backup job metrics including total jobs, 24-hour success rate, active alerts, and last failure timestamp. | exp: func:BackupsWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:new Date(summary.last_failed_at * 1000).toLocaleString | dep: @/components/ui/alert, @/components/ui/badge, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types/backups, ../types
|
||||
- JellyfinNowPlayingWidget.tsx | Displays a Jellyfin now-playing widget that fetches and renders active media sessions using a custom data hook and session activity panel. | exp: func:JellyfinNowPlayingWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:Array.isArray | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/SessionActivityPanel, ../components/SectionCard, ../hooks/useWidgets, ../types, SessionActivityPanel, SectionCard, useWidgetData, types
|
||||
- JellyfinWidget.tsx | Displays Jellyfin media server activity sessions in a widget with loading, error, and empty states. | exp: func:JellyfinWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:Array.isArray | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/SessionActivityPanel, ../components/SectionCard, ../hooks/useWidgets, ../types
|
||||
- PrometheusChartWidget.tsx | Renders a Prometheus query widget that fetches time-series data and displays it as a line chart, with loading, error, and empty states. | exp: func:PrometheusChartWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/LineSeriesChart, ../components/SectionCard, ../hooks/useWidgets, ../types, LineSeriesChart, SectionCard, useWidgetData hook, WidgetInstance type
|
||||
- PrometheusGaugeWidget.tsx | Renders a Prometheus metric as a radial gauge widget with configurable warning/critical threshold bands. | exp: func:PrometheusGaugeWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:Math.max, call:Math.round, call:Math.min, call:toPercent, call:trackCells.map, call:formatValue | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types, recharts, SectionCard, useWidgetData
|
||||
- PrometheusMeanWidget.tsx | Displays a single Prometheus metric value (mean) with adaptive formatting and unit support in a dashboard widget. | exp: func:PrometheusMeanWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:formatMean | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types, SectionCard, useWidgetData, WidgetInstance
|
||||
- PrometheusMetricWidget.tsx | A React component that fetches and displays Prometheus metric data from a widget, handling loading, error, and data states with formatted output for scalar and vector result types. | exp: func:PrometheusMetricWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:formatPrometheusValue | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types
|
||||
- MetricChartWidget.tsx | Renders a metric chart widget that fetches time-series data and displays it in a line chart with loading, error, and empty states. | exp: func:MetricChartWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/LineSeriesChart, ../components/SectionCard, ../hooks/useWidgets, ../types, LineSeriesChart, SectionCard, useWidgetData, WidgetInstance type
|
||||
- MetricGaugeWidget.tsx | Renders a metric gauge widget using a radial bar chart with threshold-based color bands for visualizing a single numeric value. | exp: func:MetricGaugeWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:Math.max, call:Math.round, call:Math.min, call:toPercent, call:formatValue | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types, recharts, SectionCard, useWidgetData, WidgetInstance types
|
||||
- MetricMeanWidget.tsx | Displays a metric mean value from Prometheus/PromQL query results with conditional loading, error, and empty states. | exp: func:MetricMeanWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:formatMean | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types, SectionCard, useWidgetData, WidgetInstance
|
||||
- PrometheusMetricWidget.tsx | Displays a Prometheus metric widget that fetches and formats time-series data with loading and error states. | exp: func:PrometheusMetricWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:formatMetricResult | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types, SectionCard, useWidgetData, WidgetInstance
|
||||
- QbittorrentActiveTorrentsWidget.tsx | Displays a list of active torrents from a qBittorrent instance with download/upload speeds and status badges in a dashboard widget. | exp: func:QbittorrentActiveTorrentsWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:torrents.map, call:formatSpeed | dep: @/components/ui/alert, @/components/ui/badge, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types, SectionCard, useWidgetData hook, WidgetInstance type
|
||||
- QbittorrentSpeedWidget.tsx | Renders a qBittorrent download/upload speed widget with a line series chart, handling loading, error, and empty states. | exp: func:QbittorrentSpeedWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/LineSeriesChart, ../components/SectionCard, ../hooks/useWidgets, ../types, LineSeriesChart, SectionCard, useWidgetData hook
|
||||
- QbittorrentTotalsWidget.tsx | Displays qBittorrent torrent totals and per-state breakdown using a widget with loading, error, and data states. | exp: func:QbittorrentTotalsWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:Object.keys, call:Object.entries(payload.by_state).map | dep: @/components/ui/alert, @/components/ui/badge, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types
|
||||
- SshTaskWidget.tsx | Displays SSH task execution results with exit status, stdout, and stderr in a polling widget card | exp: func:SshTaskWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types
|
||||
- StaticWidget.tsx | Renders a static text widget that displays fetched text content or a fallback message within a section card. | exp: func:StaticWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData | dep: ../components/SectionCard, ../hooks/useWidgets, ../types, SectionCard, useWidgetData, WidgetInstance
|
||||
- index.ts | Barrel file that re-exports all dashboard widget components from a widgets directory. | dep: AlertmanagerAlertsWidget, BackupsWidget, PrometheusChartWidget, PrometheusGaugeWidget, PrometheusMeanWidget, JellyfinWidget, JellyfinNowPlayingWidget, PrometheusMetricWidget, QbittorrentActiveTorrentsWidget, QbittorrentSpeedWidget, QbittorrentTotalsWidget, SshTaskWidget, StaticWidget
|
||||
- index.ts | Barrel file that re-exports all widget components from a dashboard/widget module. | dep: AlertmanagerAlertsWidget, BackupsWidget, MetricChartWidget, MetricGaugeWidget, MetricMeanWidget, JellyfinWidget, JellyfinNowPlayingWidget, PrometheusMetricWidget, QbittorrentActiveTorrentsWidget, QbittorrentSpeedWidget, QbittorrentTotalsWidget, SshTaskWidget, StaticWidget
|
||||
## arch
|
||||
React functional components using a per-widget pattern with consistent loading/error/empty state handling, custom data hooks, and barrel-file aggregation for modular dashboard composition.
|
||||
Composable React functional components using custom data-fetching hooks, each implementing independent loading/error/empty state handling with chart visualizations, exported via a barrel file for modular dashboard composition.
|
||||
## tags
|
||||
widget, components, data, ui, call:use, prometheus, sectioncard, types
|
||||
widget, components, data, ui, call:use, metric, sectioncard, types
|
||||
## symbols
|
||||
- AlertmanagerAlertsWidget
|
||||
- BackupsWidget
|
||||
- JellyfinNowPlayingWidget
|
||||
- JellyfinWidget
|
||||
- PrometheusChartWidget
|
||||
- PrometheusGaugeWidget
|
||||
- PrometheusMeanWidget
|
||||
- MetricChartWidget
|
||||
- MetricGaugeWidget
|
||||
- MetricMeanWidget
|
||||
- PrometheusMetricWidget
|
||||
## workflows
|
||||
- change widgets behavior
|
||||
|
||||
@@ -60,22 +60,18 @@ export function MetricGaugeWidget({
|
||||
const toPercent = (v: number) => Math.round(((v - min) / range) * 100);
|
||||
const valuePct = Math.max(0, Math.min(100, toPercent(value)));
|
||||
|
||||
// Build track cells: green / amber / red when bands are set, else neutral.
|
||||
const trackCells = hasBands
|
||||
? [
|
||||
{ pct: toPercent(warnAt!), fill: "hsl(var(--chart-1))" }, // green
|
||||
{ pct: toPercent(critAt!), fill: "hsl(var(--chart-4))" }, // amber
|
||||
{ pct: 100, fill: "hsl(var(--destructive))" }, // red
|
||||
]
|
||||
: [{ pct: 100, fill: "hsl(var(--muted))" }];
|
||||
|
||||
// The value arc is colored by status (green < warn, amber < crit, red >= crit)
|
||||
// and drawn over a neutral full-track background. We intentionally do NOT
|
||||
// render separate "track band" rings: recharts draws each data entry as a
|
||||
// concentric ring (not an arc segment), so multiple entries produced nested
|
||||
// rings that looked broken.
|
||||
const valueColor = hasBands
|
||||
? value >= critAt!
|
||||
? "hsl(var(--destructive))"
|
||||
? "var(--color-chart-4)" // red (destructive)
|
||||
: value >= warnAt!
|
||||
? "hsl(var(--chart-4))"
|
||||
: "hsl(var(--chart-1))"
|
||||
: "hsl(var(--primary))";
|
||||
? "var(--color-chart-3)" // amber (warning)
|
||||
: "var(--color-chart-2)" // green (success)
|
||||
: "var(--color-primary)";
|
||||
|
||||
return (
|
||||
<SectionCard title={widget.title} description={description}>
|
||||
@@ -86,19 +82,12 @@ export function MetricGaugeWidget({
|
||||
<AlertDescription>{data.error}</AlertDescription>
|
||||
</Alert>
|
||||
) : gauge ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<div className="relative w-full" style={{ height: 220 }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RadialBarChart
|
||||
innerRadius="65%"
|
||||
innerRadius="72%"
|
||||
outerRadius="100%"
|
||||
data={[
|
||||
...trackCells.map((c) => ({
|
||||
name: "track",
|
||||
pct: c.pct,
|
||||
fill: c.fill,
|
||||
})),
|
||||
{ name: "value", pct: valuePct, fill: valueColor },
|
||||
]}
|
||||
data={[{ name: "value", pct: valuePct, fill: valueColor }]}
|
||||
startAngle={90}
|
||||
endAngle={-270}
|
||||
>
|
||||
@@ -109,18 +98,18 @@ export function MetricGaugeWidget({
|
||||
tick={false}
|
||||
/>
|
||||
<RadialBar
|
||||
background={{ fill: "hsl(var(--muted))" }}
|
||||
background={{ fill: "var(--color-muted)" }}
|
||||
dataKey="pct"
|
||||
cornerRadius={6}
|
||||
cornerRadius={10}
|
||||
/>
|
||||
</RadialBarChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="-mt-12 flex flex-col items-center">
|
||||
<span className="text-2xl font-bold" style={{ color: valueColor }}>
|
||||
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center">
|
||||
<span className="text-3xl font-bold" style={{ color: valueColor }}>
|
||||
{formatValue(value, gauge.unit)}
|
||||
</span>
|
||||
{hasBands ? (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="mt-1 text-xs text-muted-foreground">
|
||||
warn {formatValue(warnAt!, gauge.unit)} · crit{" "}
|
||||
{formatValue(critAt!, gauge.unit)}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user