Settings: add dedicated DAV/Pushover tabs, fix CalDAV/CardDAV bugs

- Add admin DAV tab (rename from CalDAV/CardDAV) and Pushover tab
  - Add per-user Pushover tab (User Key only; App Token stays admin-managed)
  - Remove system-wide CalDAV/CardDAV fallback — per-user config only
  - Rewrite contacts_tool.py using httpx directly (caldav 2.x dropped AddressBook)
  - Fix CardDAV REPORT/PROPFIND using SOGo URL pattern
  - Fix CalDAV/CardDAV test endpoints (POST method, URL scheme normalization)
  - Fix Show Password button — API now returns actual credential values
  - Convert Credentials tab to generic key-value store; dedicated keys
    (CalDAV, Pushover, trusted_proxy) excluded via _DEDICATED_CRED_KEYS
This commit is contained in:
2026-04-10 12:06:23 +02:00
parent a9ca08f13d
commit 7b0a9ccc2b
25 changed files with 4011 additions and 235 deletions

View File

@@ -21,17 +21,50 @@ from . import tasks as agent_store
logger = logging.getLogger(__name__)
# Priority levels for the run queue (lower number = higher priority)
PRIORITY_HIGH = 0 # User-initiated chat runs
PRIORITY_NORMAL = 1 # Webhook / inbox / Telegram triggers
PRIORITY_LOW = 2 # Background monitors
_DEFAULT_MAX_CONCURRENT = 3
class AgentRunner:
def __init__(self) -> None:
self._agent: Agent | None = None
self._scheduler = AsyncIOScheduler(timezone=settings.timezone)
self._running: dict[str, asyncio.Task] = {} # run_id → asyncio.Task
# Concurrency semaphore — initialised in start() once event loop is running
self._semaphore: asyncio.Semaphore | None = None
self._max_concurrent: int = _DEFAULT_MAX_CONCURRENT
@property
def scheduler(self) -> AsyncIOScheduler:
return self._scheduler
@property
def queue_status(self) -> dict:
running = sum(1 for t in self._running.values() if not t.done())
# Tasks waiting for the semaphore are counted as "queued"
queued = max(0, running - self._max_concurrent)
return {"running": min(running, self._max_concurrent), "queued": queued, "max_concurrent": self._max_concurrent}
def init(self, agent: Agent) -> None:
self._agent = agent
async def _load_max_concurrent(self) -> int:
val = await credential_store.get("system:max_concurrent_runs")
try:
return max(1, int(val)) if val else _DEFAULT_MAX_CONCURRENT
except (ValueError, TypeError):
return _DEFAULT_MAX_CONCURRENT
async def start(self) -> None:
"""Load all enabled agents with schedules into APScheduler and start it."""
# Initialise concurrency semaphore (must happen inside a running event loop)
self._max_concurrent = await self._load_max_concurrent()
self._semaphore = asyncio.Semaphore(self._max_concurrent)
for agent in await agent_store.list_agents():
if agent["enabled"] and agent["schedule"]:
self._add_job(agent)
@@ -44,7 +77,9 @@ class AgentRunner:
misfire_grace_time=3600,
)
self._scheduler.start()
logger.info("[agent-runner] Scheduler started, loaded scheduled agents")
logger.info(
"[agent-runner] Scheduler started, max_concurrent=%d", self._max_concurrent
)
def shutdown(self) -> None:
if self._scheduler.running:
@@ -261,7 +296,14 @@ class AgentRunner:
finally:
self._running.pop(run_id, None)
task = asyncio.create_task(_execute())
async def _execute_with_semaphore():
if self._semaphore:
async with self._semaphore:
await _execute()
else:
await _execute()
task = asyncio.create_task(_execute_with_semaphore())
self._running[run_id] = task
return await agent_store.get_run(run_id)