feat: implement pngx-controller with Gitea CI/CD deployment
All checks were successful
Deploy / deploy (push) Successful in 30s
All checks were successful
Deploy / deploy (push) Successful in 30s
- Full FastAPI sync engine: master→replica document sync via paperless REST API - Web UI: dashboard, replicas, logs, settings (Jinja2 + HTMX + Pico CSS) - APScheduler background sync, SSE live log stream, Prometheus metrics - Fernet encryption for API tokens at rest - pngx.env credential file: written on save, pre-fills forms on load - Dockerfile with layer-cached uv build, Python healthcheck - docker-compose with host networking for Tailscale access - Gitea Actions workflow: version bump, secret injection, docker compose deploy Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
88
app/api/status.py
Normal file
88
app/api/status.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""Dashboard status endpoint."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from ..database import get_session
|
||||
from ..models import Replica, SyncRun
|
||||
from ..sync.engine import get_progress
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["status"])
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
def get_status(session: Session = Depends(get_session)):
|
||||
replicas = session.exec(select(Replica)).all()
|
||||
progress = get_progress()
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
replica_data = []
|
||||
for r in replicas:
|
||||
lag = None
|
||||
if r.last_sync_ts:
|
||||
ts = r.last_sync_ts
|
||||
if ts.tzinfo is None:
|
||||
ts = ts.replace(tzinfo=timezone.utc)
|
||||
lag = int((now - ts).total_seconds())
|
||||
|
||||
if r.suspended_at:
|
||||
status = "suspended"
|
||||
elif progress.running and progress.phase and r.name in progress.phase:
|
||||
status = "syncing"
|
||||
elif r.consecutive_failures > 0:
|
||||
status = "error"
|
||||
elif r.last_sync_ts:
|
||||
status = "synced"
|
||||
else:
|
||||
status = "pending"
|
||||
|
||||
# Last run stats for this replica
|
||||
last_run = session.exec(
|
||||
select(SyncRun)
|
||||
.where(SyncRun.replica_id == r.id)
|
||||
.order_by(SyncRun.started_at.desc()) # type: ignore[attr-defined]
|
||||
.limit(1)
|
||||
).first()
|
||||
|
||||
replica_data.append(
|
||||
{
|
||||
"id": r.id,
|
||||
"name": r.name,
|
||||
"url": r.url,
|
||||
"enabled": r.enabled,
|
||||
"status": status,
|
||||
"lag_seconds": lag,
|
||||
"last_sync_ts": r.last_sync_ts.isoformat() if r.last_sync_ts else None,
|
||||
"consecutive_failures": r.consecutive_failures,
|
||||
"suspended": r.suspended_at is not None,
|
||||
"docs_synced_last_run": last_run.docs_synced if last_run else 0,
|
||||
"docs_failed_last_run": last_run.docs_failed if last_run else 0,
|
||||
}
|
||||
)
|
||||
|
||||
last_run = session.exec(
|
||||
select(SyncRun)
|
||||
.order_by(SyncRun.started_at.desc()) # type: ignore[attr-defined]
|
||||
.limit(1)
|
||||
).first()
|
||||
|
||||
return {
|
||||
"replicas": replica_data,
|
||||
"sync_progress": {
|
||||
"running": progress.running,
|
||||
"phase": progress.phase,
|
||||
"docs_done": progress.docs_done,
|
||||
"docs_total": progress.docs_total,
|
||||
},
|
||||
"last_sync_run": {
|
||||
"id": last_run.id,
|
||||
"started_at": last_run.started_at.isoformat() if last_run and last_run.started_at else None,
|
||||
"finished_at": last_run.finished_at.isoformat() if last_run and last_run.finished_at else None,
|
||||
"docs_synced": last_run.docs_synced if last_run else 0,
|
||||
"docs_failed": last_run.docs_failed if last_run else 0,
|
||||
"timed_out": last_run.timed_out if last_run else False,
|
||||
}
|
||||
if last_run
|
||||
else None,
|
||||
}
|
||||
Reference in New Issue
Block a user