Files
Business-Management/features/doc-service/app/routers/plugin.py
T
curo1305 00466a9801 Add generic plugin architecture and watch-directory feature
Introduces a manifest contract so feature containers self-describe their
settings (JSON Schema + access rules). Backend and frontend gain generic
plugin proxy and dynamic Extensions UI with zero feature-specific code.

Doc-service is the first plugin consumer: exposes /plugin/manifest and
/plugin/settings, adds a watchdog-based file watcher that auto-ingests
PDFs from a mounted directory, maps subfolders to categories, supports
AI-suggested folder/filename (user-confirmed), and enforces a no-remove
policy. Access is gated by is_superuser or doc-service-admin group.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 02:09:50 +02:00

98 lines
3.3 KiB
Python

"""
Plugin manifest and settings endpoints for doc-service.
These are internal-only — they are called by the main backend's generic plugin
proxy, never directly by the browser. No authentication is applied here because
the backend enforces access control before forwarding the request.
Endpoints:
GET /plugin/manifest → static manifest with JSON Schema for settings
GET /plugin/settings → current storage config values
PATCH /plugin/settings → update storage config (partial update)
"""
from fastapi import APIRouter
from pydantic import BaseModel
from app.services.config_reader import get_storage_config, save_storage_config
router = APIRouter()
_MANIFEST: dict = {
"id": "doc-service",
"name": "Document Service",
"icon": "file-text",
"version": "1.0",
"access": {
"allow_superuser": True,
"required_groups": ["doc-service-admin"],
},
"settings_schema": {
"type": "object",
"title": "Storage & Watch",
"properties": {
"watch_enabled": {
"type": "boolean",
"title": "Enable file watching",
"description": (
"Automatically ingest PDF files added to the mounted watch directory. "
"Requires a service restart to take effect after toggling."
),
},
"watch_path": {
"type": "string",
"title": "Watch path",
"readOnly": True,
"description": "Configured via Docker volume mount — edit docker-compose to change.",
},
"ai_folder_suggestion": {
"type": "boolean",
"title": "AI folder suggestion",
"description": (
"AI suggests a category for each ingested document. "
"You must confirm the suggestion before it is applied."
),
},
"ai_folder_default": {
"type": "string",
"title": "Default import category",
"description": "Category assigned automatically when AI folder suggestion is disabled.",
},
"ai_rename_suggestion": {
"type": "boolean",
"title": "AI rename suggestion",
"description": (
"AI suggests a document title for each ingested file. "
"You must confirm before it is applied."
),
},
},
},
}
class StorageSettingsUpdate(BaseModel):
watch_enabled: bool | None = None
ai_folder_suggestion: bool | None = None
ai_folder_default: str | None = None
ai_rename_suggestion: bool | None = None
# watch_path is intentionally excluded — it cannot be changed via API
@router.get("/manifest")
async def get_manifest() -> dict:
return _MANIFEST
@router.get("/settings")
async def get_settings() -> dict:
return await get_storage_config()
@router.patch("/settings")
async def update_settings(body: StorageSettingsUpdate) -> dict:
update = body.model_dump(exclude_none=True)
if "ai_folder_default" in update:
update["ai_folder_default"] = update["ai_folder_default"][:128].strip() or "imports"
await save_storage_config(update)
return await get_storage_config()