feat(n8n): add scheduled quality-scan workflow

The original docs described two n8n workflows but the repository only ever
shipped one (return-processing); the sketched second workflow (knowledge
sync) depends on RAGcore, which isn't connected here, so it stays deferred.

Add POST /api/v1/integrations/n8n/scheduled-scan (X-Service-Token
protected, same pattern as the return callback), calling the same
run_scan() the manual "Run quality scan" UI action uses and recording a
service-actor data_quality_scan_run audit event. run_scan() already only
creates an issue for a condition without one open, so overlapping triggers
do no duplicate domain work.

n8n/mobilityops-scheduled-quality-scan.json (hourly schedule + manual test
trigger, both feeding the same HTTP call) ships "active": false so it can't
fire anywhere until deliberately published. Verified live against the
local n8n instance via the Manual test trigger: full green execution, and
the resulting data_quality_scan_run audit event (actor_type=service,
actor_label="n8n scheduled scan") confirms the real round trip, not just a
contract test. deploy/unraid/setup-scheduled-scan.sh mirrors the existing
return-workflow publish script for the shared Unraid n8n.
This commit is contained in:
NuklearRabbit
2026-08-02 06:59:17 +02:00
parent ec8f809497
commit e115031a57
9 changed files with 306 additions and 8 deletions
+18
View File
@@ -13,7 +13,9 @@ from app.core.config import get_settings
from app.core.errors import AppError
from app.models.audit import AuditEvent
from app.models.outbox import OutboxEvent
from app.schemas import ScanResultOut
from app.services.audit import record_audit_event
from app.services.data_quality import run_scan
router = APIRouter(prefix="/api/v1/integrations/n8n", tags=["integrations"])
settings = get_settings()
@@ -71,3 +73,19 @@ def return_callback(
"event_id": str(event_id),
"occurred_at": datetime.now(UTC).isoformat(),
}
@router.post("/scheduled-scan", response_model=ScanResultOut)
def scheduled_scan(
service_token: str = Header(..., alias="X-Service-Token"),
db: Session = Depends(get_db),
) -> ScanResultOut:
"""Triggered by the scheduled n8n quality-scan workflow. Narrow, read-mostly, and
safe to call repeatedly: run_scan() only ever creates an issue for a condition that
doesn't already have one open, so a duplicate or overlapping trigger does no
duplicate domain work -- it just reports zero new issues for anything already known."""
if service_token != settings.n8n_callback_token:
raise AppError("UNAUTHORIZED_SERVICE", "Invalid service token.", status_code=401)
result = run_scan(db, actor_label="n8n scheduled scan", actor_type="service")
return ScanResultOut(created=result.created)