Add web auth, infinite-scroll, agent admission and review fixes across agent/server/web

This commit is contained in:
Stanislav Rossovskii
2026-05-28 23:45:12 +04:00
parent 37b1a1d6d6
commit 1026e9ebbe
75 changed files with 3021 additions and 916 deletions

View File

@@ -53,13 +53,7 @@ async def _blacklist_row(session: AsyncSession, cred: AgentCredential) -> AgentB
return res.scalar_one_or_none()
async def reject_if_blocked(
session: AsyncSession,
cred: AgentCredential,
agent_id: str,
hostname: str,
observed: datetime | None = None,
) -> None:
async def reject_if_blocked(session: AsyncSession, cred: AgentCredential) -> None:
row = await _blacklist_row(session, cred)
if row is None:
return
@@ -108,7 +102,7 @@ async def upsert_agent_heartbeat(
session: AsyncSession, hb: HeartbeatRequest, cred: AgentCredential
) -> str:
observed = to_utc(hb.observed_at)
await reject_if_blocked(session, cred, hb.agent_id, hb.hostname, observed)
await reject_if_blocked(session, cred)
features = hb.features.model_dump()
res = await session.execute(select(Agent).where(Agent.accepted_key_hash == cred.key_hash))
@@ -330,7 +324,6 @@ async def ingest_event(
incident_key=incident_key,
)
await session.execute(stmt)
metrics.events_total.labels(result="accepted").inc()
else:
metrics.events_total.labels(result="unchanged").inc()
@@ -366,6 +359,9 @@ async def ingest_event(
return False
transition, inc = await _apply_incident(session, agent_id, ev, incident_key)
# Count accepted only after _apply_incident: an incident_key collision raises
# 400 and the savepoint rollback discards the Event insert above.
metrics.events_total.labels(result="accepted").inc()
if transition and inc is not None and ev.notifications_enabled:
event_type = "resolved" if ev.status == "ok" else "firing"
await _enqueue_outbox(

View File

@@ -208,9 +208,16 @@ async def tick(
await session.commit()
if not rows:
return 0
await asyncio.gather(
*(_process_row(sm, registry, r, settings.notifier_max_attempts) for r in rows)
)
# Bound concurrent DB sessions: each _process_row opens its own session, so an
# unbounded gather over a large batch can exhaust the connection pool.
sem = asyncio.Semaphore(settings.notifier_concurrency)
async def _guarded(row: dict) -> None:
async with sem:
await _process_row(sm, registry, row, settings.notifier_max_attempts)
await asyncio.gather(*(_guarded(r) for r in rows))
return len(rows)

View File

@@ -1,7 +1,5 @@
from __future__ import annotations
import httpx
def classify_response(status: int) -> tuple[bool, bool]:
"""Return (ok, permanent_failure)."""
@@ -10,9 +8,3 @@ def classify_response(status: int) -> tuple[bool, bool]:
if 400 <= status < 500 and status not in (408, 425, 429):
return False, True
return False, False
def classify_exception(exc: Exception) -> tuple[bool, bool]:
if isinstance(exc, httpx.TimeoutException | httpx.NetworkError):
return False, False
return False, False