Skip to content

Commit f081132

Browse files
committed
feat(api): expand API functionality
* Added ping/pong to WebSocket for connection testing. * Added onboarding check and completion endpoints. * Added thread pool support to `start-core-service` endpoint. * Introduced endpoints for core and optional services, including service descriptions. * Added `get_config` to return the entire config from the root endpoint. * Updated config update logic to use root endpoint. * Ensured `running_processes` file is created on initialization if missing. feat(config): add origin field to config * Added `origin` field to `dumb_config.json`. * Updated schema in `dumb_config_schema.json`. * Enhanced environment handling in `setup.py` to include `ORIGIN`. feat(config): enhance service management * Enables dynamic service creation and management via API. * Adds simplified process for starting core services with dependency resolution. * Provides detailed service descriptions and configurable options. * Enhances config validation and error handling. fix(decypharr): handle missing config file gracefully * Updated `patch_decypharr_config` to return a message when the config file is not found. fix(rclone): prevent overwriting config with multiple instances * Fixed issue where multiple Rclone instances could cause config overwrites. refactor(rclone): streamline setup logic and reduce redundancy * Refactored Rclone setup logic for clearer configuration handling. * Introduced helper functions for loading and writing config files. * Simplified instance logic for handling key types. fix(zurg): add version comparison during setup * Allows switching between Zurg repositories even when `auto_update` is disabled. chore(deps): update backend dependencies * `certifi` → 2025.7.14 * `jsonschema` → 4.25.0 * `rpds-py` → 0.26.0 * `scalar-fastapi` → 1.2.2 * `typing-extensions` → 4.14.1 * `xmltodict` → 0.14.2
1 parent a5124c6 commit f081132

File tree

13 files changed

+854
-467
lines changed

13 files changed

+854
-467
lines changed

.devcontainer/devcontainer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,5 @@
5656
"--shm-size=128m",
5757
"--pull=always"
5858
],
59-
"postCreateCommand": "apt update && apt install -y gcc python3.11-dev libpq-dev && curl -sSL https://install.python-poetry.org | python - && export PATH=\"$HOME/.local/bin:$PATH\" && poetry config virtualenvs.create false && poetry install --no-root --with dev && git config --global --add safe.directory /workspace"
59+
"postCreateCommand": "apt update && apt install -y gcc python3.11-dev libpq-dev && apt remove -y python3-yaml && curl -sSL https://install.python-poetry.org | python - && export PATH=\"$HOME/.local/bin:$PATH\" && poetry config virtualenvs.create false && poetry install --no-root --with dev && git config --global --add safe.directory /workspace"
6060
}

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,6 @@ services:
115115
- TZ=
116116
- PUID=
117117
- PGID=
118-
- DUMB_LOG_LEVEL=INFO
119118
# network_mode: container:gluetun ## Example to attach to gluetun vpn container if realdebrid blocks IP address
120119
ports:
121120
- "3005:3005" ## DUMB Frontend
@@ -179,15 +178,15 @@ format: `<HOST_DIR>:<CONTAINER_DIR>[:PERMISSIONS]`.
179178
| `/config` | rw | This is where the application stores the rclone.conf, and any files needing persistence. CAUTION: rclone.conf is overwritten upon start/restart of the container. Do NOT use an existing rclone.conf file if you have other rclone services |
180179
| `/log` | rw | This is where the application stores its log files |
181180
| `/zurg/RD` | rw | This is where Zurg will store the active configuration and data for RealDebrid. |
182-
| `/riven/data` | rw | This is where Riven will store its data. |
181+
| `/riven/backend/data` | rw | This is where Riven will store its data. |
183182
| `/postgres_data` | rw | This is where PostgreSQL will store its data. |
184183
| `/pgadmin/data` | rw | This is where pgAdmin 4 will store its data. |
185184
| `/plex_debrid/config` | rw | This is where plex_debrid will store its data. |
186185
| `/cli_debrid/data` | rw | This is where cli_debrid will store its data. |
187186
| `/phalanx_db/data` | rw | This is where phalanx_db will store its data. |
188187
| `/decypharr` | rw | This is where decypharr will store its data. |
189188
| `/plex` | rw | This is where Plex Media Server will store its data. |
190-
| `/mnt/debrid` | rw | This is where the symlinks and rclone mounts will be stored |
189+
| `/mnt/debrid` | rw | This is where the symlinks and rclone mounts will be stored |
191190

192191
## 📝 TODO
193192

api/api_state.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ def _load_status_from_file(self):
2020
self.logger.debug(
2121
f"Status file {self.status_file_path} not found. Initializing empty status."
2222
)
23+
with open(self.status_file_path, "w") as f:
24+
f.write("{}")
2325
return {}
2426
except Exception as e:
2527
self.logger.error(f"Error loading status file: {e}")

api/routers/config.py

Lines changed: 103 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from fastapi import APIRouter, HTTPException, Depends
2-
from typing import Union
1+
from fastapi import APIRouter, HTTPException, Depends, Query
2+
from typing import Union, Optional, Dict, Any
33
from pydantic import BaseModel
44
from utils.dependencies import get_logger, resolve_path
55
from utils.config_loader import CONFIG_MANAGER, find_service_config
@@ -9,9 +9,9 @@
99
import os, json, configparser, xmltodict
1010

1111

12-
class UpdateServiceConfigRequest(BaseModel):
13-
process_name: str
14-
updates: dict
12+
class ConfigUpdateRequest(BaseModel):
13+
process_name: Optional[str] = None
14+
updates: Dict[str, Any]
1515
persist: bool = False
1616

1717

@@ -359,87 +359,111 @@ def find_schema(schema, path_parts):
359359
return schema_section
360360

361361

362-
@config_router.post("/update-dmb-config")
363-
async def update_dumb_config(
364-
request: UpdateServiceConfigRequest, logger=Depends(get_logger)
362+
@config_router.get("/")
363+
async def get_config(
364+
process_name: Optional[str] = Query(
365+
None, description="If set, return only that service’s config"
366+
),
367+
logger=Depends(get_logger),
365368
):
366-
process_name = request.process_name
367-
updates = request.updates
368-
persist = request.persist
369-
370-
logger.info(f"Received update request for {process_name} with persist={persist}")
371-
372-
service_config, service_path = find_service_config(
373-
CONFIG_MANAGER.config, process_name
374-
)
369+
if process_name:
370+
service_cfg, _ = find_service_config(CONFIG_MANAGER.config, process_name)
371+
if not service_cfg:
372+
logger.error(f"Service not found: {process_name}")
373+
raise HTTPException(status_code=404, detail="Service not found")
374+
return service_cfg
375375

376-
if not service_config:
377-
logger.error(f"Service not found: {process_name}")
378-
raise HTTPException(status_code=404, detail="Service not found.")
376+
return CONFIG_MANAGER.config
379377

380-
path_parts = service_path.split(".")
381-
logger.debug(f"Looking up schema for path for {process_name}: {service_path}")
382378

383-
instance_schema = find_schema(
384-
CONFIG_MANAGER.schema.get("properties", {}), path_parts
385-
)
379+
@config_router.post("/")
380+
async def update_config(
381+
request: ConfigUpdateRequest,
382+
logger=Depends(get_logger),
383+
):
384+
if request.process_name:
385+
process_name = request.process_name
386+
updates = request.updates
387+
persist = request.persist
386388

387-
if not instance_schema:
388-
logger.error(f"Schema not found for process: {process_name}")
389-
raise HTTPException(
390-
status_code=400,
391-
detail=f"Schema not found for process: {process_name}",
389+
logger.info(
390+
f"Received update request for service '{process_name}' (persist={persist})"
392391
)
393392

394-
logger.debug(f"Schema found for {process_name}")
395-
396-
try:
397-
validate(instance=updates, schema=instance_schema)
398-
logger.debug("Validation of updates successful.")
399-
except ValidationError as e:
400-
error_path = (
401-
" -> ".join(map(str, e.absolute_path)) if e.absolute_path else "root"
402-
)
403-
raise HTTPException(
404-
status_code=400,
405-
detail=f"Validation error in updates at '{error_path}': {e.message}",
393+
service_config, service_path = find_service_config(
394+
CONFIG_MANAGER.config, process_name
406395
)
396+
if not service_config:
397+
logger.error(f"Service not found: {process_name}")
398+
raise HTTPException(status_code=404, detail="Service not found.")
407399

408-
try:
409-
temp_config = {**service_config, **updates}
410-
validate(instance=temp_config, schema=instance_schema)
411-
logger.debug("Validation of merged configuration successful.")
412-
except ValidationError as e:
413-
error_path = (
414-
" -> ".join(map(str, e.absolute_path)) if e.absolute_path else "root"
415-
)
416-
raise HTTPException(
417-
status_code=400,
418-
detail=f"Validation error in merged configuration at '{error_path}': {e.message}",
400+
path_parts = service_path.split(".")
401+
instance_schema = find_schema(
402+
CONFIG_MANAGER.schema.get("properties", {}), path_parts
419403
)
404+
if not instance_schema:
405+
logger.error(f"Schema not found for service: {process_name}")
406+
raise HTTPException(
407+
status_code=400,
408+
detail=f"Schema not found for service: {process_name}",
409+
)
410+
try:
411+
validate(instance=updates, schema=instance_schema)
412+
except ValidationError as e:
413+
loc = " -> ".join(map(str, e.absolute_path)) or "root"
414+
raise HTTPException(
415+
status_code=400,
416+
detail=f"Validation error in updates at '{loc}': {e.message}",
417+
)
418+
419+
try:
420+
merged = {**service_config, **updates}
421+
validate(instance=merged, schema=instance_schema)
422+
except ValidationError as e:
423+
loc = " -> ".join(map(str, e.absolute_path)) or "root"
424+
raise HTTPException(
425+
status_code=400,
426+
detail=f"Validation error in merged config at '{loc}': {e.message}",
427+
)
420428

421-
try:
422429
for key, value in updates.items():
423430
if key in service_config:
424431
service_config[key] = value
425432
else:
426-
logger.error(f"Invalid key {key} in updates.")
433+
logger.error(f"Invalid configuration key for {process_name}: {key}")
427434
raise HTTPException(
428435
status_code=400, detail=f"Invalid configuration key: {key}"
429436
)
430437

431438
if persist:
432-
logger.info(f"Saving configuration for {process_name}")
439+
logger.info(f"Persisting updated config for service '{process_name}'")
433440
CONFIG_MANAGER.save_config(process_name=process_name)
434441

435-
return {"status": "Config updated successfully", "persisted": persist}
442+
return {
443+
"status": "service config updated",
444+
"process_name": process_name,
445+
"persisted": persist,
446+
}
436447

437-
except Exception as e:
438-
logger.error(f"Failed to update configuration: {e}")
448+
updates = request.updates
449+
if not updates:
439450
raise HTTPException(
440-
status_code=500, detail=f"Failed to update configuration: {str(e)}"
451+
status_code=400, detail="No updates provided for global config."
441452
)
442453

454+
logger.info("Performing global config update (deep merge)")
455+
456+
for key, value in updates.items():
457+
existing = CONFIG_MANAGER.config.get(key)
458+
if isinstance(value, dict) and isinstance(existing, dict):
459+
existing.update(value)
460+
else:
461+
CONFIG_MANAGER.config[key] = value
462+
463+
CONFIG_MANAGER.save_config()
464+
465+
return {"status": "global config updated", "keys": list(updates.keys())}
466+
443467

444468
@config_router.post("/service-config")
445469
async def handle_service_config(
@@ -541,3 +565,22 @@ async def get_service_ui_links(logger=Depends(get_logger)):
541565
except Exception as e:
542566
logger.error(f"Failed to fetch services: {e}")
543567
raise HTTPException(status_code=500, detail="Failed to fetch services")
568+
569+
570+
@config_router.get("/onboarding-status")
571+
async def onboarding_status():
572+
cfg = CONFIG_MANAGER.config
573+
return {
574+
"needs_onboarding": not cfg.get("dumb", {}).get("onboarding_completed", False)
575+
}
576+
577+
578+
@config_router.post("/onboarding-completed")
579+
async def onboarding_completed(logger=Depends(get_logger)):
580+
cfg = CONFIG_MANAGER.config
581+
if "dumb" not in cfg:
582+
cfg["dumb"] = {}
583+
cfg["dumb"]["onboarding_completed"] = True
584+
logger.info("Onboarding completed successfully.")
585+
CONFIG_MANAGER.save_config()
586+
return {"status": "Onboarding completed successfully"}

0 commit comments

Comments
 (0)