From 7107815a5c569e52f4da529f1a062cfc6bd82715 Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 24 Jun 2026 14:38:47 +0000 Subject: [PATCH] refactor(settings): drop jellyfin machine service + dead machine path fields Slice 1 of jellyfin-service-registry. Jellyfin is configured exclusively via the service registry now; the machine-level media_root/path_prefix fields were dead duplicates of the global config. - services/settings_store.py: DEFAULT_SERVICES no longer includes "jellyfin" (now ["monitoring", "files"]). Removed machine-level media_root/path_prefix from _default_local_machine, _row_to_machine, _normalize_machine_payload, _seed_local_machine, get_machine_config, and upsert_machine. _default_local_machine no longer reads global config, so the get_settings import is dropped. - routers/settings.py: removed media_root/path_prefix from MonitoringMachineInput (dead API input; store already ignored them). The global config remote_media_root/path_prefix properties + path_utils.py are unchanged (files.py and media_index still use them for Jellyfin->SSH path resolution). ruff clean; 239 backend tests pass. --- .pi-map.index.md | 2 +- .pi-map.md | 8 ++++---- backend/.pi-map.md | 2 +- backend/src/.pi-map.index.md | 2 +- backend/src/.pi-map.md | 4 ++-- .../media_library_viewer_api/.pi-map.index.md | 2 +- .../src/media_library_viewer_api/.pi-map.md | 4 ++-- .../routers/.pi-map.index.md | 2 +- .../routers/.pi-map.md | 6 +++--- .../routers/settings.py | 2 -- .../services/.pi-map.index.md | 2 +- .../services/.pi-map.md | 6 +++--- .../services/settings_store.py | 18 +----------------- 13 files changed, 21 insertions(+), 39 deletions(-) diff --git a/.pi-map.index.md b/.pi-map.index.md index 554209b..d45be33 100644 --- a/.pi-map.index.md +++ b/.pi-map.index.md @@ -16,7 +16,7 @@ dir: . Trust boundary: index routes, map orients, source decides. ## role -Root project configuration and documentation hub for "Manage," a media and server operations tool with a FastAPI + React architecture and optional observability stack. +Root project directory for "Manage," a media and server operations tool providing documentation, container orchestration, and environment configuration for a FastAPI + React application with optional observability stack. ## parent - ## children diff --git a/.pi-map.md b/.pi-map.md index 25c2f5a..d4031e8 100644 --- a/.pi-map.md +++ b/.pi-map.md @@ -18,11 +18,11 @@ index: ./.pi-map.index.md Trust boundary: index routes, map orients, source decides. ## role -Root project configuration and documentation hub for "Manage," a media and server operations tool with a FastAPI + React architecture and optional observability stack. +Root project directory for "Manage," a media and server operations tool providing documentation, container orchestration, and environment configuration for a FastAPI + React application with optional observability stack. ## files - .dockerignore | Specifies files and directories to exclude from Docker build context to reduce image size and improve build performance | dep: Docker - .env.example | Provides a template of environment variables for configuring application hosts, backend settings, OIDC authentication, SMTP, Grafana, and alerting across a Docker Compose deployment. -- .gitignore | Specifies patterns for Git to ignore across a Python/Streamlit project with frontend components and internal tooling | dep: git, python, streamlit, node/npm +- .gitignore | Configures Git to ignore Python artifacts, virtual environments, secrets, editor files, frontend builds, and tool-specific metadata from version control. - AGENTS.md | Provides project-specific guidance for AI agents working on a media library viewer application with FastAPI backend and Vite React frontend | dep: FastAPI, Vite, React, Docker Compose, uvicorn, pytest, Ruff, TypeScript, Python 3.11 - CHANGELOG.md | Documents notable changes, breaking changes, and migration steps for the Manage project across versions. - CONTRIBUTING.md | Provides guidelines for setting up a development environment, coding standards, validation steps, and contribution requirements for a Streamlit-based media library viewer application. | dep: python, venv, pip, streamlit, py_compile @@ -34,9 +34,9 @@ Root project configuration and documentation hub for "Manage," a media and serve - docker-compose.yml | Defines a Docker Compose production stack for a backend and frontend application with OIDC authentication, Traefik reverse proxy routing, and Prometheus metrics exposure. | dep: Traefik, OIDC provider, Docker, SMTP server, external observability stack (Prometheus/Grafana/Loki/Alertmanager) - token-usage-output.txt | Displays a detailed token usage and cost analysis report for an AI coding session, including breakdowns by category, tool usage, cache efficiency, subagent costs, and pricing comparisons. ## arch -Docker Compose orchestrated multi-service architecture featuring a Python/FastAPI backend, Vite/React frontend, Traefik reverse proxy, and an optional Prometheus/Grafana/Loki monitoring stack. +Containerized full-stack architecture using Docker Compose for orchestration, Traefik as reverse proxy, separate dev/production configurations, OIDC authentication, and an optional observability layer (Prometheus, Grafana, Loki, Alertmanager). ## tags -docker, grafana, application, compose, fastapi, prometheus, project, alertmanager +docker, grafana, application, compose, fastapi, prometheus, alertmanager, loki ## symbols - ## workflows diff --git a/backend/.pi-map.md b/backend/.pi-map.md index 0d80720..001e593 100644 --- a/backend/.pi-map.md +++ b/backend/.pi-map.md @@ -10,7 +10,7 @@ FastAPI backend service providing REST API endpoints for Jellyfin media browsing - README.md | Documentation describing the setup, configuration, Docker deployment, and API endpoints of a FastAPI backend for Jellyfin media browsing, SSH file inspection, and server monitoring. | dep: FastAPI, uvicorn, pydantic-settings, Jellyfin, Jellyseerr, SSH, Docker Compose, Alertmanager, Prometheus, Grafana, Authentik/OIDC - pyproject.toml | Project configuration file defining dependencies, build system, linting, and testing settings for a FastAPI media library viewer backend. | dep: FastAPI, uvicorn, pydantic-settings, paramiko, requests, python-dotenv, pandas, PyJWT, prometheus-client, python-json-logger, cryptography, hatchling, ruff, pytest, httpx ## arch -Containerized Python 3.11 service using FastAPI framework with uvicorn ASGI server, following standard pyproject.toml-based project configuration with defined linting and testing pipelines. +Containerized Python 3.11 service using FastAPI with uvicorn ASGI server, configured via pyproject.toml with linting/testing pipelines and Docker-based deployment. ## tags uvicorn, fastapi, python, backend, pyproject, settings, docker, api ## symbols diff --git a/backend/src/.pi-map.index.md b/backend/src/.pi-map.index.md index ea0ca7a..36aea8c 100644 --- a/backend/src/.pi-map.index.md +++ b/backend/src/.pi-map.index.md @@ -2,7 +2,7 @@ dir: backend/src ## role -Root source directory of the backend application containing server configuration, business logic, API routes, and data access layers. +Root source directory serving as the entry point for the backend application. ## parent index: backend/.pi-map.index.md map: backend/.pi-map.md diff --git a/backend/src/.pi-map.md b/backend/src/.pi-map.md index edf1069..f7838e0 100644 --- a/backend/src/.pi-map.md +++ b/backend/src/.pi-map.md @@ -4,10 +4,10 @@ dir: backend/src index: backend/src/.pi-map.index.md ## role -Root source directory of the backend application containing server configuration, business logic, API routes, and data access layers. +Root source directory serving as the entry point for the backend application. ## files ## arch -Unable to determine specific architecture/patterns without file listing; please provide directory contents for analysis. +Likely follows a modular/framework-dependent architecture (e.g., MVC, layered, or hexagonal); contains core application setup, routing, configuration, and business logic modules. ## tags - ## symbols diff --git a/backend/src/media_library_viewer_api/.pi-map.index.md b/backend/src/media_library_viewer_api/.pi-map.index.md index 43ffde1..7a04886 100644 --- a/backend/src/media_library_viewer_api/.pi-map.index.md +++ b/backend/src/media_library_viewer_api/.pi-map.index.md @@ -2,7 +2,7 @@ dir: backend/src/media_library_viewer_api ## role -FastAPI backend providing authenticated media library viewing APIs with Jellyfin/Jellyseerr integration, remote SSH job execution, and observability features. +FastAPI backend providing authenticated API endpoints and remote SSH job execution for viewing and managing media library metadata from Jellyfin/Jellyseerr services. ## parent index: backend/src/.pi-map.index.md map: backend/src/.pi-map.md diff --git a/backend/src/media_library_viewer_api/.pi-map.md b/backend/src/media_library_viewer_api/.pi-map.md index cbb41bc..2cf9a18 100644 --- a/backend/src/media_library_viewer_api/.pi-map.md +++ b/backend/src/media_library_viewer_api/.pi-map.md @@ -4,7 +4,7 @@ dir: backend/src/media_library_viewer_api index: backend/src/media_library_viewer_api/.pi-map.index.md ## role -FastAPI backend providing authenticated media library viewing APIs with Jellyfin/Jellyseerr integration, remote SSH job execution, and observability features. +FastAPI backend providing authenticated API endpoints and remote SSH job execution for viewing and managing media library metadata from Jellyfin/Jellyseerr services. ## files - __init__.py | Swaps the position of two tmux panes within a window or between windows | dep: tmux, sh - auth.py | Implements OIDC/JWT and API key authentication for a FastAPI backend with middleware-based route protection. | exp: func:_normalize_issuer_url(issuer_url: str) → str, call:issuer_url.rstrip, func:get_oidc_metadata(issuer_url: str) → dict[str, Any], call:_normalize_issuer_url, call:urljoin, call:requests.get, call:response.raise_for_status, call:response.json, call:isinstance, raise:RuntimeError, func:get_jwk_client(jwks_url: str) → PyJWKClient, call:PyJWKClient, func:_split_audience(audience: str) → list[str], call:item.strip, call:audience.split, func:validate_auth_settings(settings: Settings) → None, raise:RuntimeError, func:validate_bearer_jwt(authorization: str | None, settings) → dict[str, Any], call:get_settings, call:validate_auth_settings, call:authorization.partition, call:scheme.lower, call:token.strip, call:_normalize_issuer_url, call:get_oidc_metadata, call:settings.oidc_jwks_url.strip, call:str, call:metadata.get, call:get_jwk_client, call:jwk_client.get_signing_key_from_jwt, call:_split_audience, call:jwt.decode, call:list, call:len, call:int, raise:PermissionError, raise:RuntimeError, func:require_jwt_auth(request: Request, call_next), call:get_settings, call:path.startswith, call:call_next, call:validate_bearer_jwt, call:request.headers.get, call:logger.warning, call:JSONResponse, call:str, call:logger.exception, call:claims.get, call:isinstance, func:get_api_key() → str, call:get_settings_store, call:store.get_settings, call:settings.get, call:secrets.token_urlsafe, call:store.update_setting, func:require_api_key(authorization) → str, call:get_api_key, call:secrets.compare_digest, raise:HTTPException | dep: logging, secrets, functools, typing, urllib.parse, jwt, requests, fastapi, fastapi.responses, jwt.exceptions, media_library_viewer_api.config, media_library_viewer_api.dependencies @@ -18,7 +18,7 @@ FastAPI backend providing authenticated media library viewing APIs with Jellyfin - utils.py | Provides UI-framework-independent formatting helpers and ffprobe output summarizers for video, audio, and subtitle streams. | exp: func:ticks_to_minutes(ticks: int | None) → int | None, call:round, func:human_size(num: int | float | None) → str, call:float, call:int, func:timestamp_to_local(ts: float | None) → str, call:datetime.fromtimestamp(ts).strftime, func:is_known_video_file(path: str | None) → bool, call:PurePosixPath(path).suffix.lower, func:format_duration(seconds: str | int | float | None) → str, call:float, call:str, call:int, func:format_bitrate(bit_rate: str | int | float | None) → str, call:float, call:str, func:_tags(stream: dict[str, Any]) → dict[str, Any], call:stream.get, func:_disposition(stream: dict[str, Any], key: str) → str, call:(stream.get("disposition") or {}).get, call:stream.get, func:_side_data_types(stream: dict[str, Any]) → str, call:stream.get, call:item.get, call:values.append, call:", ".join, func:ffprobe_format_summary(ffprobe: dict[str, Any]) → dict[str, str], call:ffprobe.get, call:fmt.get, call:format_duration, call:human_size, call:float, call:format_bitrate, call:str, func:summarize_video_streams(ffprobe: dict[str, Any]) → list[dict[str, Any]], call:ffprobe.get, call:stream.get, call:_tags, call:rows.append, call:format_bitrate, call:_side_data_types, call:tags.get, call:_disposition, func:summarize_audio_streams(ffprobe: dict[str, Any]) → list[dict[str, Any]], call:ffprobe.get, call:stream.get, call:_tags, call:rows.append, call:format_bitrate, call:tags.get, call:_disposition, func:summarize_subtitle_streams(ffprobe: dict[str, Any]) → list[dict[str, Any]], call:ffprobe.get, call:stream.get, call:_tags, call:rows.append, call:tags.get, call:_disposition, func:summarize_streams(ffprobe: dict[str, Any]) → list[dict[str, Any]], call:ffprobe.get, call:rows.append, call:format_bitrate, call:stream.get("tags", {}).get | dep: datetime, pathlib, typing - version.py | Provides version retrieval and formatting utilities for a backend service, falling back through environment variables, package metadata, and default values. | exp: func:get_backend_version() → str, call:os.getenv("APP_VERSION", "").strip, call:package_version, func:get_backend_build_info() → str, call:os.getenv("APP_BUILD_INFO", "").strip, call:os.getenv("GIT_COMMIT", "").strip, call:os.getenv("BUILD_COMMIT", "").strip, func:format_version_label(version: str, build_info: str) → str, call:version.strip, call:build_info.strip, func:get_version_info() → dict[str, str], call:get_backend_version, call:get_backend_build_info, call:format_version_label | dep: os, importlib.metadata ## arch -Layered FastAPI architecture using dependency injection, pydantic-settings configuration, OIDC/API-key middleware authentication, template-based remote execution, and Prometheus/structured logging observability. +Layered FastAPI architecture using dependency injection (providers/clients), Pydantic settings configuration, OIDC/API-key middleware authentication, and template-based remote SSH execution with structured observability (Prometheus/JSON logging). ## tags call:, settings, call:get, request, get, client, call:str, id ## symbols diff --git a/backend/src/media_library_viewer_api/routers/.pi-map.index.md b/backend/src/media_library_viewer_api/routers/.pi-map.index.md index a0d6b97..9113f5a 100644 --- a/backend/src/media_library_viewer_api/routers/.pi-map.index.md +++ b/backend/src/media_library_viewer_api/routers/.pi-map.index.md @@ -2,7 +2,7 @@ dir: backend/src/media_library_viewer_api/routers ## role -FastAPI router package that exposes all REST API endpoints for the media library viewer backend, organized by domain (backups, dashboard, files, jobs, media, monitoring, services, settings, tasks, users, widgets). +FastAPI router package that exposes all REST API endpoints for the media library viewer backend, organized by domain (files, media, jobs, backups, dashboard, monitoring, services, settings, tasks, users, widgets). ## parent index: backend/src/media_library_viewer_api/.pi-map.index.md map: backend/src/media_library_viewer_api/.pi-map.md diff --git a/backend/src/media_library_viewer_api/routers/.pi-map.md b/backend/src/media_library_viewer_api/routers/.pi-map.md index 8f5ef1d..81af666 100644 --- a/backend/src/media_library_viewer_api/routers/.pi-map.md +++ b/backend/src/media_library_viewer_api/routers/.pi-map.md @@ -4,7 +4,7 @@ dir: backend/src/media_library_viewer_api/routers index: backend/src/media_library_viewer_api/routers/.pi-map.index.md ## role -FastAPI router package that exposes all REST API endpoints for the media library viewer backend, organized by domain (backups, dashboard, files, jobs, media, monitoring, services, settings, tasks, users, widgets). +FastAPI router package that exposes all REST API endpoints for the media library viewer backend, organized by domain (files, media, jobs, backups, dashboard, monitoring, services, settings, tasks, users, widgets). ## files - __init__.py | Marks the directory as a Python package for routers. - backups.py | FastAPI router that provides REST endpoints for reporting, tracking, and alerting on backup jobs and runs. | exp: func:_get_or_create_job(store: SettingsStore, report: BackupReportRequest) → dict[str, Any], call:store.get_backup_job_by_name, call:store.upsert_backup_job, call:store.get_backup_job, func:post_backup_report(report: BackupReportRequest, store, _auth) → BackupRunResponse, call:_get_or_create_job, call:store.list_backup_runs, call:int, call:report.started_at.timestamp, call:abs, call:BackupRunResponse, call:report.ended_at.timestamp, call:store.create_backup_run, call:record_backup_run, call:generate_alerts_for_run, call:store.create_backup_alert, call:store.resolve_backup_alerts_for_job, call:run.pop, func:post_backup_start(report: BackupReportRequest, store, _auth) → BackupRunResponse, call:_get_or_create_job, call:int, call:report.started_at.timestamp, call:store.create_backup_run, call:record_backup_run, call:run.pop, call:BackupRunResponse, func:get_backup_jobs(store) → list[dict[str, Any]], call:store.list_backup_jobs, func:get_backup_job(job_id: str, store) → dict[str, Any], call:store.get_backup_job, call:store.list_backup_runs, raise:HTTPException, func:get_backup_runs(job_id, status, limit, store) → list[BackupRunResponse], call:store.list_backup_runs, call:BackupRunResponse, func:get_backup_run(run_id: str, store) → BackupRunResponse, call:store.get_backup_run, call:BackupRunResponse, raise:HTTPException, func:get_backup_alerts(job_id, acknowledged, severity, store) → list[BackupAlertResponse], call:store.list_backup_alerts, call:BackupAlertResponse, func:acknowledge_backup_alert(alert_id: str, store) → BackupAlertResponse, call:store.acknowledge_backup_alert, call:BackupAlertResponse, raise:HTTPException | dep: typing, fastapi, ..auth, ..models.backups, ..observability, ..services.backup_alert_engine, ..services.settings_store @@ -14,13 +14,13 @@ FastAPI router package that exposes all REST API endpoints for the media library - media.py | FastAPI router that manages media index lifecycle (status, build, stop, query) with subprocess worker orchestration and cooperative/force cancellation. | exp: func:get_media_index() → MediaIndex, call:MediaIndex, func:_set_build_metadata(index: MediaIndex, state: dict[str, Any]) → None, call:state.items, call:index.set_metadata, func:_staging_db_path(index: MediaIndex) → Path, call:index.db_path.with_name, func:_pid_is_alive(pid: int | None) → bool, call:os.kill, func:_clean_stale_build_state(index: MediaIndex) → Any, call:index.status, call:_pid_is_alive, call:logger.warning, call:_set_build_metadata, func:_serialize_status(status: Any) → dict[str, Any], func:_worker_command(final_db_path: Path, staging_db_path: Path) → list[str], call:str, func:_start_worker(index: MediaIndex) → subprocess.Popen[bytes], call:_staging_db_path, call:staging_path.unlink, call:subprocess.Popen, call:_worker_command, call:os.environ.copy, func:get_index_status(index) → dict[str, Any], call:_clean_stale_build_state, call:logger.info, call:_serialize_status, func:post_build_index(client, user_id, index) → dict[str, Any], call:_clean_stale_build_state, call:_pid_is_alive, call:logger.warning, call:client.libraries, call:logger.info, call:len, call:_start_worker, call:_set_build_metadata, call:index.status, call:record_media_index_build, call:_serialize_status, raise:HTTPException, func:stop_build(index) → dict[str, Any], call:_clean_stale_build_state, call:logger.warning, call:logger.info, call:_set_build_metadata, call:index.status, call:_serialize_status, raise:HTTPException, func:force_stop_build(index) → dict[str, Any], call:_clean_stale_build_state, call:logger.warning, call:_pid_is_alive, call:_set_build_metadata, call:index.status, call:_serialize_status, call:logger.info, call:os.killpg, call:time.time, call:time.sleep, call:record_media_index_build, raise:HTTPException, func:query_media(libraries, types, search, hdr_filter, sort_key, sort_order, limit, offset, client, user_id, index) → dict[str, Any], call:lid.strip, call:libraries.split, call:client.libraries, call:t.strip, call:types.split, call:logger.info, call:len, call:",".join, call:index.query | dep: logging, os, signal, subprocess, sys, threading, time, pathlib, typing, fastapi, media_library_viewer_api.clients.jellyfin, media_library_viewer_api.dependencies, media_library_viewer_api.observability, media_library_viewer_api.services.media_index, media_library_viewer_api.workers.media_index_worker - monitoring.py | FastAPI router providing endpoints to check the health/status of Alertmanager, Grafana, and Prometheus services and expose Prometheus scrape targets. | exp: func:_resolve_service_record(store: SettingsStore, service_type: str, service_id) → ServiceRecord | None, call:store.get_service, call:row.get, call:build_service_record, call:store.list_services, func:_base_url(service: ServiceRecord) → str, call:str(service.config.get("base_url") or "").rstrip, call:service.config.get, func:_timeout(service: ServiceRecord, default: int) → int, call:int, call:service.config.get, func:_auth_headers(service: ServiceRecord) → dict[str, str], call:str, call:service.secrets.get, func:_status_response(service: ServiceRecord | None, version, error) → dict[str, Any], func:_summary_from_alerts(alerts: list[dict[str, Any]]) → dict[str, Any], call:summarize_alerts, func:get_machines(store) → list[dict[str, Any]], call:store.list_machines, call:m.get, func:get_prometheus_targets(store) → list[dict[str, Any]], call:build_node_exporter_targets, call:logger.info, call:len, func:get_alertmanager_alerts(service_id, store) → dict[str, Any], call:_resolve_service_record, call:requests.get, call:_base_url, call:_auth_headers, call:_timeout, call:response.raise_for_status, call:response.json, call:logger.exception, call:data.get, call:_summary_from_alerts, call:logger.info, func:get_alertmanager_status(service_id, store) → dict[str, Any], call:_resolve_service_record, call:requests.get, call:_base_url, call:_auth_headers, call:_timeout, call:response.raise_for_status, call:response.json, call:logger.exception, call:data.get("versionInfo", {}).get, call:status.get, call:p.get, call:cluster.get, func:get_grafana_status(service_id, store) → dict[str, Any], call:_resolve_service_record, call:_status_response, call:requests.get, call:_base_url, call:_auth_headers, call:_timeout, call:response.raise_for_status, call:response.json, call:logger.exception, call:data.get, func:get_prometheus_status(service_id, store) → dict[str, Any], call:_resolve_service_record, call:_status_response, call:_base_url, call:_timeout, call:_auth_headers, call:requests.get, call:health.raise_for_status, call:build_info.raise_for_status, call:build_info.json().get("data", {}).get, call:logger.exception, func:receive_alertmanager_webhook(payload) → dict[str, str], call:payload.get, call:logger.info, call:len | dep: logging, typing, requests, fastapi, media_library_viewer_api.dependencies, media_library_viewer_api.services.settings_store, media_library_viewer_api.services.targets, media_library_viewer_api.widgets.sources, media_library_viewer_api.integrations.alertmanager - services.py | Provides REST API endpoints for managing service instances in a service registry, including listing service types and CRUD operations for instances while ensuring plaintext secrets are never exposed. | exp: func:_to_type_info(service_type: str) → ServiceTypeInfo, call:require_service_definition, call:ServiceTypeInfo, call:SecretFieldInfo, call:WidgetKindInfo, func:_to_instance(row: dict[str, Any]) → ServiceInstance, call:get_service_definition, call:set, call:row.get, call:bool, call:ServiceInstance, func:_validate_input(body: ServiceInstanceInput) → None, call:get_service_definition, call:validate_config, call:set, raise:HTTPException, func:list_types() → list[ServiceTypeInfo], call:_to_type_info, call:sorted, func:list_instances(service_type, store) → list[ServiceInstance], call:store.list_services, call:_to_instance, func:create_instance(body: ServiceInstanceInput, store) → ServiceInstance, call:_validate_input, call:store.upsert_service, call:_to_instance, func:update_instance(service_id: str, body: ServiceInstanceInput, store) → ServiceInstance, call:store.get_service, call:_validate_input, call:store.upsert_service, call:_to_instance, raise:HTTPException, func:delete_instance(service_id: str, store) → dict[str, str], call:store.get_service, call:store.delete_service, raise:HTTPException | dep: logging, typing, fastapi, media_library_viewer_api.dependencies, media_library_viewer_api.integrations.base, media_library_viewer_api.integrations.registry, media_library_viewer_api.models.services, media_library_viewer_api.services.settings_store -- settings.py | FastAPI router providing REST endpoints for managing machine configurations, SSH keys, and local database resets. | exp: class:MonitoringMachineInput, class:SSHKeyInput, class:SSHKeyGenerateInput, class:ResetLocalDatabaseInput, func:get_machines(store) → list[dict[str, Any]], call:store.list_machines, func:_resolve_ssh_client(machine: MonitoringMachineInput, store: SettingsStore) → tuple[RemoteSSHClient, str, int], call:machine.host.strip, call:machine.username.strip, call:int, call:store.get_ssh_key, call:str, call:ssh_key.get, call:get_settings, call:RemoteSSHClient, raise:HTTPException, func:_raise_ssh_validation_error(host: str, port: int, exc: Exception) → None, call:str, call:message.lower, raise:HTTPException, func:_validate_saved_machine_ssh(machine: MonitoringMachineInput, store: SettingsStore) → None, call:str(machine.mode or "").strip().lower, call:_resolve_ssh_client, call:client.connect, call:_raise_ssh_validation_error, call:client.close, func:test_machine_ssh(machine: MonitoringMachineInput, store) → dict[str, Any], call:str(machine.mode or "").strip().lower, call:_resolve_ssh_client, call:get_settings, call:has_known_host, call:client.connect, call:message.lower, call:client.close, raise:HTTPException, func:post_machine(machine: MonitoringMachineInput, store) → dict[str, Any], call:store.upsert_machine, call:machine.model_dump, call:MonitoringMachineInput.model_validate, call:_validate_saved_machine_ssh, func:put_machine(machine_id: str, machine: MonitoringMachineInput, store) → dict[str, Any], call:store.get_machine, call:store.upsert_machine, call:machine.model_dump, call:MonitoringMachineInput.model_validate, call:_validate_saved_machine_ssh, raise:HTTPException, func:delete_machine(machine_id: str, store) → dict[str, str], call:store.get_machine, call:store.delete_machine, raise:HTTPException, func:generate_ssh_key(payload: SSHKeyGenerateInput) → dict[str, Any], call:paramiko.RSAKey.generate, call:StringIO, call:key.write_private_key, call:private_buffer.getvalue, call:key.get_name, call:key.get_base64, call:":".join, call:key.get_fingerprint, func:get_ssh_keys(store) → list[dict[str, Any]], call:store.list_ssh_keys, func:post_ssh_key(key: SSHKeyInput, store) → dict[str, Any], call:store.upsert_ssh_key, call:key.model_dump, func:put_ssh_key(key_id: str, key: SSHKeyInput, store) → dict[str, Any], call:store.get_ssh_key, call:store.upsert_ssh_key, call:key.model_dump, raise:HTTPException, func:delete_ssh_key(key_id: str, store) → dict[str, str], call:store.get_ssh_key, call:store.delete_ssh_key, raise:HTTPException, func:reset_local_database(payload: ResetLocalDatabaseInput, store) → dict[str, Any], call:payload.confirm_phrase.strip().upper, call:remove_sqlite_database, call:MediaIndex, call:bool, raise:HTTPException | dep: logging, io, typing, paramiko, fastapi, pydantic, media_library_viewer_api.clients.ssh, media_library_viewer_api.config, media_library_viewer_api.dependencies, media_library_viewer_api.services.db_maintenance, media_library_viewer_api.services.known_hosts, media_library_viewer_api.services.media_index, media_library_viewer_api.services.settings_store +- settings.py | FastAPI router for managing machine definitions, SSH keys, SSH connection validation, and local database resets. | exp: class:MonitoringMachineInput, class:SSHKeyInput, class:SSHKeyGenerateInput, class:ResetLocalDatabaseInput, func:get_machines(store) → list[dict[str, Any]], call:store.list_machines, func:_resolve_ssh_client(machine: MonitoringMachineInput, store: SettingsStore) → tuple[RemoteSSHClient, str, int], call:machine.host.strip, call:machine.username.strip, call:int, call:store.get_ssh_key, call:str, call:ssh_key.get, call:get_settings, call:RemoteSSHClient, raise:HTTPException, func:_raise_ssh_validation_error(host: str, port: int, exc: Exception) → None, call:str, call:message.lower, raise:HTTPException, func:_validate_saved_machine_ssh(machine: MonitoringMachineInput, store: SettingsStore) → None, call:str(machine.mode or "").strip().lower, call:_resolve_ssh_client, call:client.connect, call:_raise_ssh_validation_error, call:client.close, func:test_machine_ssh(machine: MonitoringMachineInput, store) → dict[str, Any], call:str(machine.mode or "").strip().lower, call:_resolve_ssh_client, call:get_settings, call:has_known_host, call:client.connect, call:message.lower, call:client.close, raise:HTTPException, func:post_machine(machine: MonitoringMachineInput, store) → dict[str, Any], call:store.upsert_machine, call:machine.model_dump, call:MonitoringMachineInput.model_validate, call:_validate_saved_machine_ssh, func:put_machine(machine_id: str, machine: MonitoringMachineInput, store) → dict[str, Any], call:store.get_machine, call:store.upsert_machine, call:machine.model_dump, call:MonitoringMachineInput.model_validate, call:_validate_saved_machine_ssh, raise:HTTPException, func:delete_machine(machine_id: str, store) → dict[str, str], call:store.get_machine, call:store.delete_machine, raise:HTTPException, func:generate_ssh_key(payload: SSHKeyGenerateInput) → dict[str, Any], call:paramiko.RSAKey.generate, call:StringIO, call:key.write_private_key, call:private_buffer.getvalue, call:key.get_name, call:key.get_base64, call:":".join, call:key.get_fingerprint, func:get_ssh_keys(store) → list[dict[str, Any]], call:store.list_ssh_keys, func:post_ssh_key(key: SSHKeyInput, store) → dict[str, Any], call:store.upsert_ssh_key, call:key.model_dump, func:put_ssh_key(key_id: str, key: SSHKeyInput, store) → dict[str, Any], call:store.get_ssh_key, call:store.upsert_ssh_key, call:key.model_dump, raise:HTTPException, func:delete_ssh_key(key_id: str, store) → dict[str, str], call:store.get_ssh_key, call:store.delete_ssh_key, raise:HTTPException, func:reset_local_database(payload: ResetLocalDatabaseInput, store) → dict[str, Any], call:payload.confirm_phrase.strip().upper, call:remove_sqlite_database, call:MediaIndex, call:bool, raise:HTTPException | dep: logging, io, typing, paramiko, fastapi, pydantic, media_library_viewer_api.clients.ssh, media_library_viewer_api.config, media_library_viewer_api.dependencies, media_library_viewer_api.services.db_maintenance, media_library_viewer_api.services.known_hosts, media_library_viewer_api.services.media_index, media_library_viewer_api.services.settings_store - tasks.py | FastAPI router providing CRUD endpoints and execution for saved server tasks with SSH service resolution | exp: class:TaskInput, class:RunTaskRequest, func:_service_label(service: dict[str, Any] | None) → str, call:str, call:service.get, func:_resolve_service_for_task(store: SettingsStore, task: dict[str, Any], service_id: str | None) → dict[str, Any] | None, call:store.get_service, call:str(task.get("default_service_id") or "").strip, call:task.get, call:store.list_services, call:svc.get, func:_service_row_to_record(service_row: dict[str, Any]) → ServiceRecord, call:build_service_record, call:get_settings_store, func:list_tasks(store) → list[dict[str, Any]], call:store.list_tasks, func:create_task(task: TaskInput, store) → dict[str, Any], call:store.upsert_task, call:task.model_dump, func:update_task(task_id: str, task: TaskInput, store) → dict[str, Any], call:store.get_task, call:store.upsert_task, call:task.model_dump, raise:HTTPException, func:delete_task(task_id: str, store) → dict[str, str], call:store.get_task, call:store.delete_task, raise:HTTPException, func:list_task_runs(task_id: str, limit, store) → dict[str, Any], call:store.get_task, call:store.list_service_task_runs, call:len, raise:HTTPException, func:run_task(request: RunTaskRequest, service_id, store) → dict[str, Any], call:store.get_task, call:task.get, call:_resolve_service_for_task, call:service_row.get, call:_service_row_to_record, call:run_saved_task, call:_service_label, raise:HTTPException | dep: logging, typing, fastapi, pydantic, media_library_viewer_api.dependencies, media_library_viewer_api.services.settings_store, media_library_viewer_api.services.task_runner, media_library_viewer_api.widgets.sources - users.py | Re-exports all public names from the users_impl module to provide a unified public API for user-related functionality. | dep: .users_impl, users_impl - users_impl.py | FastAPI router that fetches and merges Jellyfin users with optional Jellyseerr enrichment, plus endpoints for queueing user emails via a background mail worker. | exp: func:_safe_int(value: Any) → int, call:int, func:_permission_labels(permissions: int) → list[str], func:_role_label(permissions: int) → str, func:_account_type(user_type: Any) → str, call:_USER_TYPES.get, call:_safe_int, func:_merge_users(jellyfin_users: list[dict[str, Any]], jellyseerr_jellyfin_users: list[dict[str, Any]] | None, jellyseerr_users: list[dict[str, Any]] | None, jellyseerr_client: JellyseerrClient | None) → dict[str, Any], call:str(value or "").strip().lower, call:bool, call:_looks_like_email, call:str(value).strip, call:", ".join, call:_normalize, call:item.get, call:_lookup_keys, call:user.get, call:linked_by_jellyfin_id.get, call:(jf_link or {}).get, call:seerr_by_key.get, call:_pick_source_and_value, call:(seerr_user or {}).get, call:_first_value, call:jellyseerr_client.absolute_url, call:_safe_int, call:_role_label, call:_source_summary, call:items.append, call:_account_type, call:_permission_labels, call:logger.info, call:len, func:_normalize(value: Any) → str, call:str(value or "").strip().lower, func:_looks_like_email(value: Any) → bool, call:str(value or "").strip, call:bool, func:_pick_source_and_value(candidates: list[tuple[str, Any]]) → tuple[str, str], call:_looks_like_email, call:str(value).strip, func:_first_value(candidates: list[tuple[str, Any]]) → tuple[str, str], call:str(value or "").strip, func:_source_summary(name_source: str, email_source: str, avatar_source: str, access_source: str) → str, call:", ".join, func:_lookup_keys(item: dict[str, Any]) → list[str], call:_normalize, call:item.get, func:get_users(jellyfin, jellyseerr) → dict[str, Any], call:jellyfin.users, call:logger.info, call:len, call:jellyseerr.jellyfin_users, call:logger.exception, call:jellyseerr.users, call:_merge_users, call:bool, func:get_user_message_status() → dict[str, Any], call:mail_queue.status, func:post_user_message(recipient_ids, subject, html_body, text_body, attachments, jellyfin, jellyseerr) → dict[str, Any], call:json.loads, call:isinstance, call:str(item).strip, call:subject.strip, call:get_users, call:item.get, call:directory.get, call:users_by_id.get, call:skipped.append, call:str(item.get("email") or "").strip, call:recipients.append, call:recipient_labels.append, call:get_settings, call:validate_smtp_settings, call:mail_queue.status, call:upload.read, call:attachment_payloads.append, call:EmailAttachment, call:mail_queue.enqueue, call:str(getattr(settings, "smtp_from_address", "") or "").strip, call:getattr, call:str(getattr(settings, "smtp_username", "") or "").strip, call:logger.info, call:len, raise:HTTPException | dep: json, logging, typing, fastapi, media_library_viewer_api.clients.jellyfin, media_library_viewer_api.clients.jellyseerr, media_library_viewer_api.config, media_library_viewer_api.dependencies, media_library_viewer_api.services.mailer - widgets.py | Provides a FastAPI REST API for managing dashboard widget instances, including CRUD operations, built-in widget discovery, and data fetching through service or built-in adapters. | exp: func:_validate_widget_input(body: WidgetInstanceInput, store: SettingsStore) → None, call:store.get_service, call:get_service_definition, call:definition.widget_kind, call:validate_config, call:is_builtin_kind, call:validate_builtin_config, raise:HTTPException, func:list_builtin_kinds() → list[BuiltinWidgetKindInfo], call:BuiltinWidgetKindInfo, call:BUILTIN_WIDGET_KINDS.values, func:list_instances(store) → list[dict[str, Any]], call:WidgetInstance(**widget).model_dump, call:store.list_widgets, func:create_instance(body: WidgetInstanceInput, store) → dict[str, Any], call:_validate_widget_input, call:store.upsert_widget, call:body.model_dump, call:WidgetInstance(**widget).model_dump, func:update_instance(widget_id: str, body: WidgetInstanceInput, store) → dict[str, Any], call:store.get_widget, call:_validate_widget_input, call:store.upsert_widget, call:body.model_dump, call:WidgetInstance(**widget).model_dump, raise:HTTPException, func:delete_instance(widget_id: str, store) → dict[str, str], call:store.get_widget, call:store.delete_widget, raise:HTTPException, func:fetch_data(widget_id: str, store) → dict[str, Any], call:store.get_widget, call:widget.get, call:store.get_service, call:WidgetDataResponse( widget_id=widget_id, error=f"Service {service_id} not found", fetched_at=int(time.time()), ).model_dump, call:int, call:time.time, call:service_row.get, call:WidgetDataResponse( widget_id=widget_id, error="Service is disabled", fetched_at=int(time.time()), ).model_dump, call:get_service_adapter, call:WidgetDataResponse( widget_id=widget_id, error=f"No adapter for service type {service_row['service_type']}", fetched_at=int(time.time()), ).model_dump, call:build_service_record, call:get_builtin_adapter, call:WidgetDataResponse( widget_id=widget_id, error=f"Unknown built-in widget kind: {widget_kind}", fetched_at=int(time.time()), ).model_dump, call:adapter.fetch, call:logger.exception, call:WidgetDataResponse( widget_id=widget_id, data=data if "error" not in data else None, error=data.get("error"), fetched_at=int(time.time()), ).model_dump, call:data.get, raise:HTTPException | dep: logging, time, typing, fastapi, media_library_viewer_api.dependencies, media_library_viewer_api.integrations.base, media_library_viewer_api.integrations.registry, media_library_viewer_api.models.widgets, media_library_viewer_api.services.settings_store, media_library_viewer_api.widgets.builtin, media_library_viewer_api.widgets.sources ## arch -Modular APIRouter-per-domain pattern where each module defines a FastAPI router with endpoints for its concern, aggregated by the parent application; routers delegate business logic to services/workers and interact with remote systems via SSH and external APIs. +Modular router-per-domain pattern where each file defines an isolated FastAPI APIRouter; routers are registered by the parent application and share common dependencies for SSH orchestration, database access, and service resolution. ## tags call:, service, raise:httpexception, get, backup, media_library_viewer_api, ssh, call:logger.info ## symbols diff --git a/backend/src/media_library_viewer_api/routers/settings.py b/backend/src/media_library_viewer_api/routers/settings.py index 5ba0c1f..90d932b 100644 --- a/backend/src/media_library_viewer_api/routers/settings.py +++ b/backend/src/media_library_viewer_api/routers/settings.py @@ -40,8 +40,6 @@ class MonitoringMachineInput(BaseModel): ssh_private_key: str = "" ssh_private_key_passphrase: str = "" password: str = "" - media_root: str = "" - path_prefix: str = "" notes: str = "" diff --git a/backend/src/media_library_viewer_api/services/.pi-map.index.md b/backend/src/media_library_viewer_api/services/.pi-map.index.md index 0dd5c0e..813d12a 100644 --- a/backend/src/media_library_viewer_api/services/.pi-map.index.md +++ b/backend/src/media_library_viewer_api/services/.pi-map.index.md @@ -2,7 +2,7 @@ dir: backend/src/media_library_viewer_api/services ## role -Backend service layer providing business logic for media library management, including media indexing, backup monitoring, email notifications, SSH task execution, settings persistence, and security. +Backend service layer providing business logic for media library management, backup monitoring, email notifications, SSH task execution, encryption, and persistent settings storage. ## parent index: backend/src/media_library_viewer_api/.pi-map.index.md map: backend/src/media_library_viewer_api/.pi-map.md diff --git a/backend/src/media_library_viewer_api/services/.pi-map.md b/backend/src/media_library_viewer_api/services/.pi-map.md index 74b86df..81587c8 100644 --- a/backend/src/media_library_viewer_api/services/.pi-map.md +++ b/backend/src/media_library_viewer_api/services/.pi-map.md @@ -4,7 +4,7 @@ dir: backend/src/media_library_viewer_api/services index: backend/src/media_library_viewer_api/services/.pi-map.index.md ## role -Backend service layer providing business logic for media library management, including media indexing, backup monitoring, email notifications, SSH task execution, settings persistence, and security. +Backend service layer providing business logic for media library management, backup monitoring, email notifications, SSH task execution, encryption, and persistent settings storage. ## files - __init__.py | Swaps the position of two tmux panes within a window or between windows | dep: tmux, sh - backup_alert_engine.py | Generates alerts for backup job runs based on failure status, size/duration anomalies compared to historical medians, and missed schedules. | exp: func:generate_alerts_for_run(run: dict[str, Any], previous_runs: list[dict[str, Any]], job: dict[str, Any] | None) → list[dict[str, Any]], call:alerts.append, call:run.get, call:r.get, call:len, call:statistics.median, func:check_missed_schedules(jobs: list[dict[str, Any]], get_latest_run: callable, existing_alerts: list[dict[str, Any]]) → list[dict[str, Any]], call:int, call:time.time, call:job.get, call:get_latest_run, call:alerts.append, call:any | dep: statistics, typing, time @@ -17,11 +17,11 @@ Backend service layer providing business logic for media library management, inc - media_index.py | Re-exports all symbols from the media_index_impl module to provide a public API interface for media indexing services | dep: media_library_viewer_api.services.media_index_impl - media_index_impl.py | SQLite-backed media inventory service that indexes Jellyfin media libraries with queryable, sortable, and filterable data for UI-agnostic consumption. | exp: class:MediaIndexBuildCancelled, class:MediaIndexStatus, class:MediaIndex, method:__init__(self, db_path), call:Path, call:self.db_path.parent.mkdir, method:connect(self) → sqlite3.Connection, call:sqlite3.connect, call:conn.execute, method:init_schema(self) → None, call:self.connect, call:conn.executescript, method:set_metadata(self, key: str, value: str | int | float) → None, call:self.init_schema, call:self.connect, call:conn.execute, call:str, method:replace_items(self, rows: Iterable[dict[str, Any]]) → int, call:self.init_schema, call:list, call:",".join, call:len, call:self.connect, call:conn.execute, call:conn.executemany, call:','.join, call:row.get, call:str, call:int, call:time.time, method:status(self) → MediaIndexStatus, call:self.db_path.exists, call:MediaIndexStatus, call:self.connect, call:int, call:conn.execute("SELECT COUNT(*) FROM media_items").fetchone, call:conn.execute("SELECT key, value FROM index_metadata").fetchall, call:meta.get, call:str(updated_at_raw).isdigit, call:time.strftime, call:time.localtime, call:float, call:str(meta.get(key, str(default))).strip().lower, call:_bool, call:_float, call:_int, method:query(self, library_id, library_ids, media_types, search, hdr_filter, sort_key, sort_order, limit, offset) → tuple[list[dict[str, Any]], int], call:self.init_schema, call:where.append, call:",".join, call:len, call:params.extend, call:params.append, call:search.lower, call:" AND ".join, call:SORT_COLUMNS.get, call:self.connect, call:int, call:conn.execute("SELECT COUNT(*) FROM media_items" + where_sql, params).fetchone, call:conn.execute( "SELECT * FROM media_items" + where_sql + order_sql + " LIMIT ? OFFSET ?", [*params, int(limit), int(offset)], ).fetchall, call:display_media_row, call:dict, func:_estimate_remaining_seconds(elapsed_seconds: float, progress: float | None) → float | None, call:max, call:min, func:build_media_index(client: JellyfinClient, user_id: str, libraries: list[dict[str, Any]], index, page_size, media_root, fallback_prefix, progress_callback, should_cancel) → int, call:MediaIndex, call:time.perf_counter, call:should_cancel, call:progress_callback, call:_estimate_remaining_seconds, call:len, call:ensure_not_cancelled, call:logger.info, call:emit, call:enumerate, call:library.get, call:client.items, call:response.get, call:int, call:max, call:normalized_rows.extend, call:resolve_remote_media_path, call:row.get, call:normalize_media_item, call:logger.debug, call:index.replace_items, call:index.set_metadata, raise:MediaIndexBuildCancelled, func:ensure_not_cancelled() → None, call:should_cancel, raise:MediaIndexBuildCancelled, func:emit(stage: str, message: str) → None, call:time.perf_counter, call:progress_callback, call:_estimate_remaining_seconds, call:len | dep: logging, sqlite3, time, dataclasses, pathlib, typing, media_library_viewer_api.clients.jellyfin, media_library_viewer_api.domain.media, media_library_viewer_api.path_utils, jellyfin_client, media_domain, path_utils - secrets.py | Provides symmetric authenticated encryption for service secrets using Fernet with a mandatory environment variable master key. | exp: class:EncryptionKeyError, func:get_encryption_key() → bytes, call:os.environ.get, call:raw.strip().encode, call:Fernet, raise:EncryptionKeyError, func:reset_encryption_key_cache() → None, call:get_encryption_key.cache_clear, func:_fernet() → Fernet, call:Fernet, call:get_encryption_key, func:encrypt_value(plaintext: str) → str, call:_fernet().encrypt(plaintext.encode()).decode, call:plaintext.encode, func:decrypt_value(ciphertext: str) → str, call:_fernet().decrypt(ciphertext.encode()).decode, call:ciphertext.encode, raise:EncryptionKeyError, func:encrypt_secrets(values: dict[str, str]) → dict[str, str], call:_fernet, call:fernet.encrypt(value.encode()).decode, call:value.encode, call:values.items, func:decrypt_secrets(blob: dict[str, str]) → dict[str, str], call:_fernet, call:blob.items, call:fernet.decrypt(ciphertext.encode()).decode, call:ciphertext.encode, raise:EncryptionKeyError, func:generate_development_key() → str, call:Fernet.generate_key().decode, func:validate_encryption_key() → None, call:get_encryption_key | dep: os, functools, cryptography.fernet, functools.lru_cache -- settings_store.py | Manages persistent application settings, machine definitions, SSH keys, dashboard widgets, saved tasks, backup jobs, and service configurations in a SQLite database with schema migrations and WAL mode. | exp: class:SettingsStore, method:__init__(self, db_path), call:Path, call:self.db_path.parent.mkdir, method:connect(self) → sqlite3.Connection, call:sqlite3.connect, call:conn.execute, method:init_schema(self) → None, call:self.connect, call:conn.execute("PRAGMA table_info(ssh_keys)").fetchall, call:conn.execute("PRAGMA table_info(saved_tasks)").fetchall, call:conn.execute("PRAGMA table_info(dashboard_widgets)").fetchall, method:_row_to_machine(self, row: sqlite3.Row) → dict[str, Any], call:json.loads, call:self._normalize_services, call:data.get, call:bool, call:int, method:_normalize_machine_payload(self, payload: dict[str, Any], machine_id) → dict[str, Any], call:self.get_machine, call:str(payload.get("id") or machine_id or uuid.uuid4().hex[:12]).strip, call:payload.get, call:uuid.uuid4, call:str(payload.get("mode") or (current or {}).get("mode") or "local").strip().lower, call:(current or {}).get, call:bool, call:str(payload.get("name") or (current or {}).get("name") or "").strip, call:self._normalize_services, call:str( payload.get(field) if payload.get(field) is not None else (current or {}).get(field, default) or default ).strip, call:_current_str, call:int, method:_seed_local_machine(self) → None, call:_default_local_machine, call:int, call:time.time, call:machine.get, call:self.connect, call:conn.execute, call:json.dumps, method:_seed_dashboard_widgets(self) → None, method:ensure_defaults(self) → None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT COUNT(*) FROM monitoring_machines").fetchone, call:int, call:self._seed_local_machine, method:list_machines(self) → list[dict[str, Any]], call:self.init_schema, call:self.connect, call:conn.execute( "SELECT * FROM monitoring_machines ORDER BY CASE WHEN id = ? THEN 0 ELSE 1 END, name COLLATE NOCASE", (LOCAL_MACHINE_ID,), ).fetchall, call:self._row_to_machine, method:get_machine(self, machine_id: str | None) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM monitoring_machines WHERE id = ?", (machine_id,)).fetchone, call:self._row_to_machine, method:get_machine_config(self, machine_id: str | None) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM monitoring_machines WHERE id = ?", (machine_id,)).fetchone, call:json.loads, call:bool, call:self._normalize_services, call:data.get, call:int, method:list_machines_for_service(self, service: str) → list[dict[str, Any]], call:self.list_machines, call:machine.get, method:get_machine_for_service(self, service: str, machine_id) → dict[str, Any] | None, call:self.get_machine, call:machine.get, call:self.list_machines_for_service, method:upsert_machine(self, payload: dict[str, Any], machine_id) → dict[str, Any], call:self.init_schema, call:self._normalize_machine_payload, call:int, call:time.time, call:machine.get, call:self.connect, call:conn.execute( "SELECT created_at FROM monitoring_machines WHERE id = ?", (machine["id"],), ).fetchone, call:json.dumps, call:self.get_machine, method:delete_machine(self, machine_id: str) → None, call:self.init_schema, call:self.connect, call:conn.execute, method:_row_to_ssh_key(self, row: sqlite3.Row, usage_count) → dict[str, Any], call:self._private_key_summary, call:str, call:bool, method:_normalize_ssh_key_payload(self, payload: dict[str, Any], key_id) → dict[str, Any], call:self.get_ssh_key, call:str(payload.get("id") or key_id or uuid.uuid4().hex[:12]).strip, call:payload.get, call:uuid.uuid4, call:str(payload.get("name") or (current or {}).get("name") or key_id).strip, call:(current or {}).get, call:str( payload.get("notes") if payload.get("notes") is not None else (current or {}).get("notes", "") or "" ).strip, call:self._private_key_summary, call:str( payload.get("public_key") if payload.get("public_key") is not None else (current or {}).get("public_key", "") or summary["public_key"] or "" ).strip, call:str( payload.get("fingerprint") if payload.get("fingerprint") is not None else (current or {}).get("fingerprint", "") or summary["fingerprint"] or "" ).strip, method:list_ssh_keys(self) → list[dict[str, Any]], call:self.init_schema, call:self.list_machines, call:str(machine.get("ssh_key_id") or "").strip, call:machine.get, call:usage_counts.get, call:self.connect, call:conn.execute("SELECT * FROM ssh_keys ORDER BY name COLLATE NOCASE").fetchall, call:self._row_to_ssh_key, method:get_ssh_key(self, key_id: str | None) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM ssh_keys WHERE id = ?", (key_id,)).fetchone, call:self._private_key_summary, call:str, method:upsert_ssh_key(self, payload: dict[str, Any], key_id) → dict[str, Any], call:self.init_schema, call:self._normalize_ssh_key_payload, call:int, call:time.time, call:self.connect, call:conn.execute("SELECT created_at FROM ssh_keys WHERE id = ?", (key["id"],)).fetchone, call:self.get_ssh_key, method:delete_ssh_key(self, key_id: str) → None, call:self.init_schema, call:self.connect, call:conn.execute, method:_row_to_task(self, row: sqlite3.Row) → dict[str, Any], call:bool, method:_normalize_task_payload(self, payload: dict[str, Any], task_id) → dict[str, Any], call:self.get_task, call:str(payload.get("id") or task_id or uuid.uuid4().hex[:12]).strip, call:payload.get, call:uuid.uuid4, call:str(payload.get("name") or (current or {}).get("name") or task_id).strip, call:(current or {}).get, call:str(payload.get("task_type") or (current or {}).get("task_type") or "shell").strip().lower, call:bool, call:str( payload.get("default_service_id") if payload.get("default_service_id") is not None else (current or {}).get("default_service_id", "") or "" ).strip, call:str( payload.get("notes") if payload.get("notes") is not None else (current or {}).get("notes", "") or "" ).strip, method:list_tasks(self) → list[dict[str, Any]], call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM saved_tasks ORDER BY name COLLATE NOCASE").fetchall, call:self._row_to_task, method:get_task(self, task_id: str | None) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM saved_tasks WHERE id = ?", (task_id,)).fetchone, call:self._row_to_task, method:upsert_task(self, payload: dict[str, Any], task_id) → dict[str, Any], call:self.init_schema, call:self._normalize_task_payload, call:int, call:time.time, call:self.connect, call:conn.execute("SELECT created_at FROM saved_tasks WHERE id = ?", (task["id"],)).fetchone, call:self.get_task, method:delete_task(self, task_id: str) → None, call:self.init_schema, call:self.connect, call:conn.execute, method:_row_to_shortcut(self, row: sqlite3.Row) → dict[str, Any], call:json.loads, call:bool, call:target.get, method:_normalize_shortcut_payload(self, payload: dict[str, Any], shortcut_id) → dict[str, Any], call:self.get_shortcut, call:str(payload.get("id") or shortcut_id or uuid.uuid4().hex[:12]).strip, call:payload.get, call:uuid.uuid4, call:str(payload.get("shortcut_type") or (current or {}).get("shortcut_type") or "website").strip().lower, call:(current or {}).get, call:str(payload.get("label") or (current or {}).get("label") or "").strip, call:bool, call:str( payload.get(field) if payload.get(field) is not None else (current or {}).get(field, default) or default ).strip, call:_field, method:list_shortcuts(self) → list[dict[str, Any]], call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM dashboard_shortcuts ORDER BY label COLLATE NOCASE").fetchall, call:self._row_to_shortcut, method:get_shortcut(self, shortcut_id: str | None) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM dashboard_shortcuts WHERE id = ?", (shortcut_id,)).fetchone, call:self._row_to_shortcut, method:upsert_shortcut(self, payload: dict[str, Any], shortcut_id) → dict[str, Any], call:self.init_schema, call:self._normalize_shortcut_payload, call:int, call:time.time, call:self.connect, call:conn.execute( "SELECT created_at FROM dashboard_shortcuts WHERE id = ?", (shortcut["id"],), ).fetchone, call:json.dumps, call:self.get_shortcut, method:delete_shortcut(self, shortcut_id: str) → None, call:self.init_schema, call:self.connect, call:conn.execute, method:_row_to_job(self, row: sqlite3.Row) → dict[str, Any], method:_normalize_backup_job_payload(self, payload: dict[str, Any], job_id) → dict[str, Any], call:self.get_backup_job, call:str(payload.get("id") or job_id or uuid.uuid4().hex[:12]).strip, call:payload.get, call:uuid.uuid4, call:str(payload.get("name") or (current or {}).get("name") or job_id).strip, call:(current or {}).get, call:str( payload.get("source") if payload.get("source") is not None else (current or {}).get("source", "") or "" ).strip, call:str( payload.get("target") if payload.get("target") is not None else (current or {}).get("target", "") or "" ).strip, call:int, method:get_backup_job_by_name(self, name: str) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM backup_jobs WHERE name = ?", (name,)).fetchone, call:self._row_to_job, method:upsert_backup_job(self, payload: dict[str, Any]) → dict[str, Any], call:self.init_schema, call:self._normalize_backup_job_payload, call:int, call:time.time, call:self.connect, call:conn.execute("SELECT created_at FROM backup_jobs WHERE id = ?", (job["id"],)).fetchone, call:self.get_backup_job, method:get_backup_job(self, job_id: str | None) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM backup_jobs WHERE id = ?", (job_id,)).fetchone, call:self._row_to_job, method:list_backup_jobs(self) → list[dict[str, Any]], call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM backup_jobs ORDER BY created_at DESC").fetchall, call:self._row_to_job, method:_row_to_run(self, row: sqlite3.Row) → dict[str, Any], call:json.loads, method:create_backup_run(self, payload: dict[str, Any]) → dict[str, Any], call:self.init_schema, call:str(payload.get("id") or uuid.uuid4().hex[:12]).strip, call:payload.get, call:uuid.uuid4, call:int, call:time.time, call:json.dumps, call:self.connect, call:conn.execute, call:self.get_backup_run, method:get_backup_run(self, run_id: str | None) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM backup_runs WHERE id = ?", (run_id,)).fetchone, call:self._row_to_run, method:list_backup_runs(self, job_id, status, limit) → list[dict[str, Any]], call:self.init_schema, call:clauses.append, call:params.append, call:' AND '.join, call:max, call:min, call:int, call:self.connect, call:conn.execute(sql, params).fetchall, call:self._row_to_run, method:get_latest_backup_run(self, job_id: str) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute( "SELECT * FROM backup_runs WHERE job_id = ? ORDER BY created_at DESC LIMIT 1", (job_id,), ).fetchone, call:self._row_to_run, method:_row_to_alert(self, row: sqlite3.Row) → dict[str, Any], call:bool, method:create_backup_alert(self, payload: dict[str, Any]) → dict[str, Any], call:self.init_schema, call:str(payload.get("id") or uuid.uuid4().hex[:12]).strip, call:payload.get, call:uuid.uuid4, call:int, call:time.time, call:self.connect, call:conn.execute, call:self.get_backup_alert, method:get_backup_alert(self, alert_id: str | None) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM backup_alerts WHERE id = ?", (alert_id,)).fetchone, call:self._row_to_alert, method:list_backup_alerts(self, job_id, acknowledged, severity) → list[dict[str, Any]], call:self.init_schema, call:clauses.append, call:params.append, call:' AND '.join, call:self.connect, call:conn.execute(sql, params).fetchall, call:self._row_to_alert, method:acknowledge_backup_alert(self, alert_id: str) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute, call:self.get_backup_alert, method:resolve_backup_alerts_for_job(self, job_id: str, alert_type: str) → int, call:self.init_schema, call:int, call:time.time, call:self.connect, call:conn.execute, method:prune_backup_alerts(self, cutoff_ts: int) → int, call:self.init_schema, call:self.connect, call:conn.execute, call:int, method:get_settings(self) → dict[str, Any], call:self.init_schema, call:self.connect, call:conn.execute("SELECT key, value FROM app_settings").fetchall, method:get_setting(self, key: str, default) → Any, call:self.init_schema, call:self.connect, call:conn.execute("SELECT value FROM app_settings WHERE key = ?", (key,)).fetchone, method:update_setting(self, key: str, value: str) → None, call:self.init_schema, call:int, call:time.time, call:self.connect, call:conn.execute, method:_row_to_widget(self, row: sqlite3.Row) → dict[str, Any], call:row.keys, call:json.loads, call:bool, call:int, method:_normalize_widget_payload(self, payload: dict[str, Any], widget_id) → dict[str, Any], call:self.get_widget, call:str(payload.get("id") or widget_id or uuid.uuid4().hex[:12]).strip, call:payload.get, call:uuid.uuid4, call:str(payload.get("service_id") or (current or {}).get("service_id") or "").strip, call:(current or {}).get, call:str(payload.get("widget_kind") or (current or {}).get("widget_kind", "")).strip, call:str(payload.get("title") or (current or {}).get("title", "") or "").strip, call:isinstance, call:_validate_config_keys, call:bool, call:int, method:list_widgets(self) → list[dict[str, Any]], call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM dashboard_widgets ORDER BY sort_order ASC, created_at ASC").fetchall, call:self._row_to_widget, method:get_widget(self, widget_id: str | None) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM dashboard_widgets WHERE id = ?", (widget_id,)).fetchone, call:self._row_to_widget, method:upsert_widget(self, payload: dict[str, Any], widget_id) → dict[str, Any], call:self.init_schema, call:self._normalize_widget_payload, call:int, call:time.time, call:self.connect, call:conn.execute( "SELECT created_at FROM dashboard_widgets WHERE id = ?", (widget["id"],), ).fetchone, call:json.dumps, call:self.get_widget, method:delete_widget(self, widget_id: str) → None, call:self.init_schema, call:self.connect, call:conn.execute, method:_row_to_service(self, row: sqlite3.Row) → dict[str, Any], call:json.loads, call:bool, method:list_services(self, service_type) → list[dict[str, Any]], call:self.init_schema, call:self.connect, call:conn.execute( "SELECT * FROM services WHERE service_type = ? ORDER BY name ASC", (service_type,), ).fetchall, call:conn.execute("SELECT * FROM services ORDER BY name ASC").fetchall, call:self._row_to_service, method:get_service(self, service_id: str) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM services WHERE id = ?", (service_id,)).fetchone, call:self._row_to_service, method:_normalize_service_payload(self, payload: dict[str, Any], service_id) → dict[str, Any], call:self.get_service, call:str(payload.get("id") or service_id or uuid.uuid4().hex[:12]).strip, call:payload.get, call:uuid.uuid4, call:str(payload.get("service_type") or (current or {}).get("service_type", "")).strip, call:(current or {}).get, call:str(payload.get("name") or (current or {}).get("name", "") or "").strip, call:isinstance, call:bool, method:upsert_service(self, payload: dict[str, Any], secret_values, service_id) → dict[str, Any], call:self.init_schema, call:self._normalize_service_payload, call:int, call:time.time, call:self.get_service, call:dict, call:secret_values.items, call:secrets_blob.pop, call:encrypt_value, call:self.connect, call:conn.execute, call:json.dumps, method:delete_service(self, service_id: str) → None, call:self.init_schema, call:self.connect, call:conn.execute("PRAGMA table_info(dashboard_widgets)").fetchall, method:record_service_task_run(self, payload: dict[str, Any]) → dict[str, Any], call:self.init_schema, call:str, call:payload.get, call:uuid.uuid4, call:int, call:time.time, call:self.connect, call:conn.execute, method:list_service_task_runs(self, service_id, task_id, limit) → list[dict[str, Any]], call:self.init_schema, call:clauses.append, call:params.append, call:" AND ".join, call:int, call:self.connect, call:conn.execute( f"SELECT * FROM service_task_runs {where} ORDER BY created_at DESC LIMIT ?", params, ).fetchall, func:_default_local_machine() → dict[str, Any], call:get_settings, call:list, func:get_settings_store() → SettingsStore, call:SettingsStore | dep: json, sqlite3, time, uuid, io, pathlib, typing, paramiko, media_library_viewer_api.config, media_library_viewer_api.models.widgets +- settings_store.py | Manages persistent application settings (machines, SSH keys, tasks, services, backups, dashboard widgets) via a SQLite database with schema migrations and CRUD operations. | exp: class:SettingsStore, method:__init__(self, db_path), call:Path, call:self.db_path.parent.mkdir, method:connect(self) → sqlite3.Connection, call:sqlite3.connect, call:conn.execute, method:init_schema(self) → None, call:self.connect, call:conn.execute("PRAGMA table_info(ssh_keys)").fetchall, call:conn.execute("PRAGMA table_info(saved_tasks)").fetchall, call:conn.execute("PRAGMA table_info(dashboard_widgets)").fetchall, method:_row_to_machine(self, row: sqlite3.Row) → dict[str, Any], call:json.loads, call:self._normalize_services, call:data.get, call:bool, call:int, method:_normalize_machine_payload(self, payload: dict[str, Any], machine_id) → dict[str, Any], call:self.get_machine, call:str(payload.get("id") or machine_id or uuid.uuid4().hex[:12]).strip, call:payload.get, call:uuid.uuid4, call:str(payload.get("mode") or (current or {}).get("mode") or "local").strip().lower, call:(current or {}).get, call:bool, call:str(payload.get("name") or (current or {}).get("name") or "").strip, call:self._normalize_services, call:str( payload.get(field) if payload.get(field) is not None else (current or {}).get(field, default) or default ).strip, call:_current_str, call:int, method:_seed_local_machine(self) → None, call:_default_local_machine, call:int, call:time.time, call:machine.get, call:self.connect, call:conn.execute, call:json.dumps, method:_seed_dashboard_widgets(self) → None, method:ensure_defaults(self) → None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT COUNT(*) FROM monitoring_machines").fetchone, call:int, call:self._seed_local_machine, method:list_machines(self) → list[dict[str, Any]], call:self.init_schema, call:self.connect, call:conn.execute( "SELECT * FROM monitoring_machines ORDER BY CASE WHEN id = ? THEN 0 ELSE 1 END, name COLLATE NOCASE", (LOCAL_MACHINE_ID,), ).fetchall, call:self._row_to_machine, method:get_machine(self, machine_id: str | None) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM monitoring_machines WHERE id = ?", (machine_id,)).fetchone, call:self._row_to_machine, method:get_machine_config(self, machine_id: str | None) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM monitoring_machines WHERE id = ?", (machine_id,)).fetchone, call:json.loads, call:bool, call:self._normalize_services, call:data.get, call:int, method:list_machines_for_service(self, service: str) → list[dict[str, Any]], call:self.list_machines, call:machine.get, method:get_machine_for_service(self, service: str, machine_id) → dict[str, Any] | None, call:self.get_machine, call:machine.get, call:self.list_machines_for_service, method:upsert_machine(self, payload: dict[str, Any], machine_id) → dict[str, Any], call:self.init_schema, call:self._normalize_machine_payload, call:int, call:time.time, call:machine.get, call:self.connect, call:conn.execute( "SELECT created_at FROM monitoring_machines WHERE id = ?", (machine["id"],), ).fetchone, call:json.dumps, call:self.get_machine, method:delete_machine(self, machine_id: str) → None, call:self.init_schema, call:self.connect, call:conn.execute, method:_row_to_ssh_key(self, row: sqlite3.Row, usage_count) → dict[str, Any], call:self._private_key_summary, call:str, call:bool, method:_normalize_ssh_key_payload(self, payload: dict[str, Any], key_id) → dict[str, Any], call:self.get_ssh_key, call:str(payload.get("id") or key_id or uuid.uuid4().hex[:12]).strip, call:payload.get, call:uuid.uuid4, call:str(payload.get("name") or (current or {}).get("name") or key_id).strip, call:(current or {}).get, call:str( payload.get("notes") if payload.get("notes") is not None else (current or {}).get("notes", "") or "" ).strip, call:self._private_key_summary, call:str( payload.get("public_key") if payload.get("public_key") is not None else (current or {}).get("public_key", "") or summary["public_key"] or "" ).strip, call:str( payload.get("fingerprint") if payload.get("fingerprint") is not None else (current or {}).get("fingerprint", "") or summary["fingerprint"] or "" ).strip, method:list_ssh_keys(self) → list[dict[str, Any]], call:self.init_schema, call:self.list_machines, call:str(machine.get("ssh_key_id") or "").strip, call:machine.get, call:usage_counts.get, call:self.connect, call:conn.execute("SELECT * FROM ssh_keys ORDER BY name COLLATE NOCASE").fetchall, call:self._row_to_ssh_key, method:get_ssh_key(self, key_id: str | None) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM ssh_keys WHERE id = ?", (key_id,)).fetchone, call:self._private_key_summary, call:str, method:upsert_ssh_key(self, payload: dict[str, Any], key_id) → dict[str, Any], call:self.init_schema, call:self._normalize_ssh_key_payload, call:int, call:time.time, call:self.connect, call:conn.execute("SELECT created_at FROM ssh_keys WHERE id = ?", (key["id"],)).fetchone, call:self.get_ssh_key, method:delete_ssh_key(self, key_id: str) → None, call:self.init_schema, call:self.connect, call:conn.execute, method:_row_to_task(self, row: sqlite3.Row) → dict[str, Any], call:bool, method:_normalize_task_payload(self, payload: dict[str, Any], task_id) → dict[str, Any], call:self.get_task, call:str(payload.get("id") or task_id or uuid.uuid4().hex[:12]).strip, call:payload.get, call:uuid.uuid4, call:str(payload.get("name") or (current or {}).get("name") or task_id).strip, call:(current or {}).get, call:str(payload.get("task_type") or (current or {}).get("task_type") or "shell").strip().lower, call:bool, call:str( payload.get("default_service_id") if payload.get("default_service_id") is not None else (current or {}).get("default_service_id", "") or "" ).strip, call:str( payload.get("notes") if payload.get("notes") is not None else (current or {}).get("notes", "") or "" ).strip, method:list_tasks(self) → list[dict[str, Any]], call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM saved_tasks ORDER BY name COLLATE NOCASE").fetchall, call:self._row_to_task, method:get_task(self, task_id: str | None) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM saved_tasks WHERE id = ?", (task_id,)).fetchone, call:self._row_to_task, method:upsert_task(self, payload: dict[str, Any], task_id) → dict[str, Any], call:self.init_schema, call:self._normalize_task_payload, call:int, call:time.time, call:self.connect, call:conn.execute("SELECT created_at FROM saved_tasks WHERE id = ?", (task["id"],)).fetchone, call:self.get_task, method:delete_task(self, task_id: str) → None, call:self.init_schema, call:self.connect, call:conn.execute, method:_row_to_shortcut(self, row: sqlite3.Row) → dict[str, Any], call:json.loads, call:bool, call:target.get, method:_normalize_shortcut_payload(self, payload: dict[str, Any], shortcut_id) → dict[str, Any], call:self.get_shortcut, call:str(payload.get("id") or shortcut_id or uuid.uuid4().hex[:12]).strip, call:payload.get, call:uuid.uuid4, call:str(payload.get("shortcut_type") or (current or {}).get("shortcut_type") or "website").strip().lower, call:(current or {}).get, call:str(payload.get("label") or (current or {}).get("label") or "").strip, call:bool, call:str( payload.get(field) if payload.get(field) is not None else (current or {}).get(field, default) or default ).strip, call:_field, method:list_shortcuts(self) → list[dict[str, Any]], call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM dashboard_shortcuts ORDER BY label COLLATE NOCASE").fetchall, call:self._row_to_shortcut, method:get_shortcut(self, shortcut_id: str | None) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM dashboard_shortcuts WHERE id = ?", (shortcut_id,)).fetchone, call:self._row_to_shortcut, method:upsert_shortcut(self, payload: dict[str, Any], shortcut_id) → dict[str, Any], call:self.init_schema, call:self._normalize_shortcut_payload, call:int, call:time.time, call:self.connect, call:conn.execute( "SELECT created_at FROM dashboard_shortcuts WHERE id = ?", (shortcut["id"],), ).fetchone, call:json.dumps, call:self.get_shortcut, method:delete_shortcut(self, shortcut_id: str) → None, call:self.init_schema, call:self.connect, call:conn.execute, method:_row_to_job(self, row: sqlite3.Row) → dict[str, Any], method:_normalize_backup_job_payload(self, payload: dict[str, Any], job_id) → dict[str, Any], call:self.get_backup_job, call:str(payload.get("id") or job_id or uuid.uuid4().hex[:12]).strip, call:payload.get, call:uuid.uuid4, call:str(payload.get("name") or (current or {}).get("name") or job_id).strip, call:(current or {}).get, call:str( payload.get("source") if payload.get("source") is not None else (current or {}).get("source", "") or "" ).strip, call:str( payload.get("target") if payload.get("target") is not None else (current or {}).get("target", "") or "" ).strip, call:int, method:get_backup_job_by_name(self, name: str) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM backup_jobs WHERE name = ?", (name,)).fetchone, call:self._row_to_job, method:upsert_backup_job(self, payload: dict[str, Any]) → dict[str, Any], call:self.init_schema, call:self._normalize_backup_job_payload, call:int, call:time.time, call:self.connect, call:conn.execute("SELECT created_at FROM backup_jobs WHERE id = ?", (job["id"],)).fetchone, call:self.get_backup_job, method:get_backup_job(self, job_id: str | None) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM backup_jobs WHERE id = ?", (job_id,)).fetchone, call:self._row_to_job, method:list_backup_jobs(self) → list[dict[str, Any]], call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM backup_jobs ORDER BY created_at DESC").fetchall, call:self._row_to_job, method:_row_to_run(self, row: sqlite3.Row) → dict[str, Any], call:json.loads, method:create_backup_run(self, payload: dict[str, Any]) → dict[str, Any], call:self.init_schema, call:str(payload.get("id") or uuid.uuid4().hex[:12]).strip, call:payload.get, call:uuid.uuid4, call:int, call:time.time, call:json.dumps, call:self.connect, call:conn.execute, call:self.get_backup_run, method:get_backup_run(self, run_id: str | None) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM backup_runs WHERE id = ?", (run_id,)).fetchone, call:self._row_to_run, method:list_backup_runs(self, job_id, status, limit) → list[dict[str, Any]], call:self.init_schema, call:clauses.append, call:params.append, call:' AND '.join, call:max, call:min, call:int, call:self.connect, call:conn.execute(sql, params).fetchall, call:self._row_to_run, method:get_latest_backup_run(self, job_id: str) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute( "SELECT * FROM backup_runs WHERE job_id = ? ORDER BY created_at DESC LIMIT 1", (job_id,), ).fetchone, call:self._row_to_run, method:_row_to_alert(self, row: sqlite3.Row) → dict[str, Any], call:bool, method:create_backup_alert(self, payload: dict[str, Any]) → dict[str, Any], call:self.init_schema, call:str(payload.get("id") or uuid.uuid4().hex[:12]).strip, call:payload.get, call:uuid.uuid4, call:int, call:time.time, call:self.connect, call:conn.execute, call:self.get_backup_alert, method:get_backup_alert(self, alert_id: str | None) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM backup_alerts WHERE id = ?", (alert_id,)).fetchone, call:self._row_to_alert, method:list_backup_alerts(self, job_id, acknowledged, severity) → list[dict[str, Any]], call:self.init_schema, call:clauses.append, call:params.append, call:' AND '.join, call:self.connect, call:conn.execute(sql, params).fetchall, call:self._row_to_alert, method:acknowledge_backup_alert(self, alert_id: str) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute, call:self.get_backup_alert, method:resolve_backup_alerts_for_job(self, job_id: str, alert_type: str) → int, call:self.init_schema, call:int, call:time.time, call:self.connect, call:conn.execute, method:prune_backup_alerts(self, cutoff_ts: int) → int, call:self.init_schema, call:self.connect, call:conn.execute, call:int, method:get_settings(self) → dict[str, Any], call:self.init_schema, call:self.connect, call:conn.execute("SELECT key, value FROM app_settings").fetchall, method:get_setting(self, key: str, default) → Any, call:self.init_schema, call:self.connect, call:conn.execute("SELECT value FROM app_settings WHERE key = ?", (key,)).fetchone, method:update_setting(self, key: str, value: str) → None, call:self.init_schema, call:int, call:time.time, call:self.connect, call:conn.execute, method:_row_to_widget(self, row: sqlite3.Row) → dict[str, Any], call:row.keys, call:json.loads, call:bool, call:int, method:_normalize_widget_payload(self, payload: dict[str, Any], widget_id) → dict[str, Any], call:self.get_widget, call:str(payload.get("id") or widget_id or uuid.uuid4().hex[:12]).strip, call:payload.get, call:uuid.uuid4, call:str(payload.get("service_id") or (current or {}).get("service_id") or "").strip, call:(current or {}).get, call:str(payload.get("widget_kind") or (current or {}).get("widget_kind", "")).strip, call:str(payload.get("title") or (current or {}).get("title", "") or "").strip, call:isinstance, call:_validate_config_keys, call:bool, call:int, method:list_widgets(self) → list[dict[str, Any]], call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM dashboard_widgets ORDER BY sort_order ASC, created_at ASC").fetchall, call:self._row_to_widget, method:get_widget(self, widget_id: str | None) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM dashboard_widgets WHERE id = ?", (widget_id,)).fetchone, call:self._row_to_widget, method:upsert_widget(self, payload: dict[str, Any], widget_id) → dict[str, Any], call:self.init_schema, call:self._normalize_widget_payload, call:int, call:time.time, call:self.connect, call:conn.execute( "SELECT created_at FROM dashboard_widgets WHERE id = ?", (widget["id"],), ).fetchone, call:json.dumps, call:self.get_widget, method:delete_widget(self, widget_id: str) → None, call:self.init_schema, call:self.connect, call:conn.execute, method:_row_to_service(self, row: sqlite3.Row) → dict[str, Any], call:json.loads, call:bool, method:list_services(self, service_type) → list[dict[str, Any]], call:self.init_schema, call:self.connect, call:conn.execute( "SELECT * FROM services WHERE service_type = ? ORDER BY name ASC", (service_type,), ).fetchall, call:conn.execute("SELECT * FROM services ORDER BY name ASC").fetchall, call:self._row_to_service, method:get_service(self, service_id: str) → dict[str, Any] | None, call:self.init_schema, call:self.connect, call:conn.execute("SELECT * FROM services WHERE id = ?", (service_id,)).fetchone, call:self._row_to_service, method:_normalize_service_payload(self, payload: dict[str, Any], service_id) → dict[str, Any], call:self.get_service, call:str(payload.get("id") or service_id or uuid.uuid4().hex[:12]).strip, call:payload.get, call:uuid.uuid4, call:str(payload.get("service_type") or (current or {}).get("service_type", "")).strip, call:(current or {}).get, call:str(payload.get("name") or (current or {}).get("name", "") or "").strip, call:isinstance, call:bool, method:upsert_service(self, payload: dict[str, Any], secret_values, service_id) → dict[str, Any], call:self.init_schema, call:self._normalize_service_payload, call:int, call:time.time, call:self.get_service, call:dict, call:secret_values.items, call:secrets_blob.pop, call:encrypt_value, call:self.connect, call:conn.execute, call:json.dumps, method:delete_service(self, service_id: str) → None, call:self.init_schema, call:self.connect, call:conn.execute("PRAGMA table_info(dashboard_widgets)").fetchall, method:record_service_task_run(self, payload: dict[str, Any]) → dict[str, Any], call:self.init_schema, call:str, call:payload.get, call:uuid.uuid4, call:int, call:time.time, call:self.connect, call:conn.execute, method:list_service_task_runs(self, service_id, task_id, limit) → list[dict[str, Any]], call:self.init_schema, call:clauses.append, call:params.append, call:" AND ".join, call:int, call:self.connect, call:conn.execute( f"SELECT * FROM service_task_runs {where} ORDER BY created_at DESC LIMIT ?", params, ).fetchall, func:_default_local_machine() → dict[str, Any], call:list, func:get_settings_store() → SettingsStore, call:SettingsStore | dep: json, sqlite3, time, uuid, io, pathlib, typing, paramiko, media_library_viewer_api.models.widgets - targets.py | Builds a Prometheus HTTP service discovery target list of remote Node Exporter endpoints from configured SSH machines. | exp: func:_scrape_address(machine: dict[str, Any]) → str | None, call:machine.get, call:str(machine.get("node_exporter_scrape_host") or "").strip, call:str(machine.get("host") or "").strip, call:int, func:build_node_exporter_targets(store: SettingsStore) → list[dict[str, Any]], call:store.list_machines, call:machine.get, call:str(machine.get("mode") or "local").strip().lower, call:_scrape_address, call:targets.append | dep: logging, typing, media_library_viewer_api.services.settings_store - task_runner.py | Provides a unified execution path for running saved tasks over SSH against ssh_tasks service instances, including client building, command rendering, execution, and run logging. | exp: class:TaskRunResult, func:build_ssh_client(store: SettingsStore, service: "ServiceRecord") → RemoteSSHClient, call:str(config.get("host") or "").strip, call:config.get, call:str(config.get("username") or "").strip, call:get_settings, call:str(config.get("ssh_key_id") or "").strip, call:store.get_ssh_key, call:ssh_key.get, call:service.secrets.get, call:RemoteSSHClient, call:int, raise:ValueError, func:_render_command(task: dict[str, Any]) → str, call:str(task.get("task_type") or "shell").lower, call:task.get, call:shlex.quote, raise:ValueError, func:run_saved_task(store: SettingsStore, task: dict[str, Any], service: "ServiceRecord", timeout) → TaskRunResult, call:int, call:service.config.get, call:build_ssh_client, call:_render_command, call:time.perf_counter, call:client.run, call:_record, call:str, call:logger.exception, call:task.get, call:TaskRunResult, func:_record(store: SettingsStore, task: dict[str, Any], service: "ServiceRecord", status: str, exit_status, duration_ms, stdout_tail, stderr_tail, error) → None, call:store.record_service_task_run, call:str, call:task.get | dep: logging, shlex, time, dataclasses, typing, media_library_viewer_api.clients.ssh, media_library_viewer_api.config, media_library_viewer_api.services.settings_store, media_library_viewer_api.widgets.sources, RemoteSSHClient, get_settings, SettingsStore, ServiceRecord ## arch -Modular service-oriented design with background daemon threads, SQLite persistence with WAL mode, implementation-module re-export pattern for public APIs, and in-process queues for asynchronous work. +Modular service-oriented design with implementation-hiding re-export patterns, background worker threads for async tasks, SQLite-backed persistence, and SSH/SMTP integration abstractions. ## tags call:str, call:conn.execute, call:self.connect, call:self., schema, call:self.init, call:int, call: ## symbols diff --git a/backend/src/media_library_viewer_api/services/settings_store.py b/backend/src/media_library_viewer_api/services/settings_store.py index 1be5876..e2021b2 100644 --- a/backend/src/media_library_viewer_api/services/settings_store.py +++ b/backend/src/media_library_viewer_api/services/settings_store.py @@ -17,16 +17,14 @@ from typing import Any import paramiko -from media_library_viewer_api.config import get_settings from media_library_viewer_api.models.widgets import _validate_config_keys DEFAULT_SETTINGS_PATH = Path(".cache/media_library_viewer/settings.sqlite") LOCAL_MACHINE_ID = "local" -DEFAULT_SERVICES = ["monitoring", "files", "jellyfin"] +DEFAULT_SERVICES = ["monitoring", "files"] def _default_local_machine() -> dict[str, Any]: - settings = get_settings() return { "id": LOCAL_MACHINE_ID, "name": "This machine", @@ -42,8 +40,6 @@ def _default_local_machine() -> dict[str, Any]: "ssh_private_key": "", "ssh_private_key_passphrase": "", "password": "", - "media_root": settings.media_root, - "path_prefix": settings.path_prefix, "node_exporter_enabled": False, "node_exporter_port": 9100, "node_exporter_scrape_host": "", @@ -285,8 +281,6 @@ class SettingsStore: "ssh_private_key_set": bool(data.get("ssh_private_key")), "ssh_private_key_passphrase_set": bool(data.get("ssh_private_key_passphrase")), "password_set": bool(data.get("password")), - "media_root": data.get("media_root", ""), - "path_prefix": data.get("path_prefix", ""), "node_exporter_enabled": bool(data.get("node_exporter_enabled", False)), "node_exporter_port": int(data.get("node_exporter_port", 9100) or 9100), "node_exporter_scrape_host": data.get("node_exporter_scrape_host", ""), @@ -334,8 +328,6 @@ class SettingsStore: if password in (None, ""): password = (current or {}).get("password", "") password = str(password or "") - media_root = _current_str("media_root") - path_prefix = _current_str("path_prefix") node_exporter_enabled = bool( payload.get("node_exporter_enabled") if payload.get("node_exporter_enabled") is not None @@ -365,8 +357,6 @@ class SettingsStore: "ssh_private_key": ssh_private_key, "ssh_private_key_passphrase": ssh_private_key_passphrase, "password": password, - "media_root": media_root, - "path_prefix": path_prefix, "node_exporter_enabled": node_exporter_enabled, "node_exporter_port": node_exporter_port, "node_exporter_scrape_host": node_exporter_scrape_host, @@ -388,8 +378,6 @@ class SettingsStore: "ssh_private_key": "", "ssh_private_key_passphrase": "", "password": "", - "media_root": machine["media_root"], - "path_prefix": machine["path_prefix"], "node_exporter_enabled": machine["node_exporter_enabled"], "node_exporter_port": machine["node_exporter_port"], "node_exporter_scrape_host": machine["node_exporter_scrape_host"], @@ -473,8 +461,6 @@ class SettingsStore: "ssh_private_key": data.get("ssh_private_key", ""), "ssh_private_key_passphrase": data.get("ssh_private_key_passphrase", ""), "password": data.get("password", ""), - "media_root": data.get("media_root", ""), - "path_prefix": data.get("path_prefix", ""), "node_exporter_enabled": bool(data.get("node_exporter_enabled", False)), "node_exporter_port": int(data.get("node_exporter_port", 9100) or 9100), "node_exporter_scrape_host": data.get("node_exporter_scrape_host", ""), @@ -512,8 +498,6 @@ class SettingsStore: "ssh_private_key": machine["ssh_private_key"], "ssh_private_key_passphrase": machine["ssh_private_key_passphrase"], "password": machine["password"], - "media_root": machine["media_root"], - "path_prefix": machine["path_prefix"], "node_exporter_enabled": machine["node_exporter_enabled"], "node_exporter_port": machine["node_exporter_port"], "node_exporter_scrape_host": machine["node_exporter_scrape_host"],