feat(agent): migrate compliance-check results to banner + documents (M1-M5)
After a compliance-check run finishes, the user can now apply the
extracted vendor inventory directly to their own:
- CookieBanner config (admin /sdk/einwilligungen)
- Cookie-Policy / VVT-Register / Privacy-Policy templates
(admin /sdk/document-generator)
Backend:
- migration_to_banner.py: vendor list -> CookieBannerConfig with
ESSENTIAL/PERFORMANCE/PERSONALIZATION/EXTERNAL_MEDIA buckets +
review flags (broken opt-out URLs, missing expiry, no cookies listed)
- migration_to_document.py: vendor list -> pre-fills for 3 doc
templates, recipient-type aware (INTERNAL/GROUP/PROCESSOR/CONTROLLER)
- agent_migration_routes.py: GET /banner-preview, /document-preview,
/summary keyed on check_id
- compliance_audit_log: new check_payloads table persists cmp_vendors +
extracted_profile so the preview survives an app restart
- tests: 9 mapper units + 4 endpoint integration tests
Frontend:
- MigrationPanel.tsx: modal showing banner-config diff + document
pre-fills, plus links into the existing editors
- ComplianceCheckTab.tsx: replaces standalone audit link with the
panel; net -3 lines, stays at the 500-cap
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -551,6 +551,12 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
|
||||
},
|
||||
mc_records=audit_rows,
|
||||
)
|
||||
from compliance.services.compliance_audit_log import record_check_payload
|
||||
record_check_payload(
|
||||
check_id=check_id,
|
||||
vendors=cmp_vendors,
|
||||
profile=extracted_profile,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("Audit persistence skipped: %s", e)
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Migration endpoints: Compliance-Check → Customer Banner / Documents.
|
||||
|
||||
After a /compliance/agent/compliance-check run finishes, the user can
|
||||
migrate the extracted CMP vendor list + extracted profile into:
|
||||
- their CookieBanner config (admin-compliance /sdk/einwilligungen)
|
||||
- their Document-Generator (Cookie-Policy / VVT / Privacy-Policy)
|
||||
|
||||
These endpoints are read-only previews — the actual write to a tenant's
|
||||
SDK state is initiated by the frontend with the existing save endpoints
|
||||
(/sdk/cookie-banner, /sdk/document-generator). We only return the
|
||||
ready-to-apply payload + flags for manual review.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
|
||||
from compliance.services.compliance_audit_log import (
|
||||
get_check_payload, get_check_run,
|
||||
)
|
||||
from compliance.services.migration_to_banner import build_banner_config
|
||||
from compliance.services.migration_to_document import build_document_prefills
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/compliance/agent/migration", tags=["agent-migration"])
|
||||
|
||||
|
||||
def _load_check_context(check_id: str) -> tuple[list[dict], dict, dict]:
|
||||
"""Return (vendors, profile, run_meta) for a stored check_id."""
|
||||
# Prefer the in-memory job cache (richest data, before sidecar trim).
|
||||
try:
|
||||
from compliance.api.agent_compliance_check_routes import (
|
||||
_compliance_check_jobs,
|
||||
)
|
||||
job = _compliance_check_jobs.get(check_id)
|
||||
except Exception:
|
||||
job = None
|
||||
if job and (result := job.get("result")):
|
||||
return (
|
||||
result.get("cmp_vendors") or [],
|
||||
result.get("extracted_profile") or {},
|
||||
{
|
||||
"site_name": result.get("business_profile", {}).get("siteName", ""),
|
||||
"base_domain": result.get("business_profile", {}).get("baseUrl", ""),
|
||||
},
|
||||
)
|
||||
|
||||
payload = get_check_payload(check_id)
|
||||
if payload is None:
|
||||
raise HTTPException(404, f"Unknown check_id '{check_id}'")
|
||||
run = get_check_run(check_id) or {}
|
||||
return (
|
||||
payload.get("vendors") or [],
|
||||
payload.get("profile") or {},
|
||||
{
|
||||
"site_name": run.get("site_name", ""),
|
||||
"base_domain": run.get("base_domain", ""),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{check_id}/banner-preview")
|
||||
async def preview_banner_migration(
|
||||
check_id: str,
|
||||
language: str = Query("de", pattern="^(de|en)$"),
|
||||
) -> dict[str, Any]:
|
||||
"""Build a CookieBannerConfig from a finished compliance-check run.
|
||||
|
||||
Returns: { config, flags, summary } — the frontend renders a diff
|
||||
against the tenant's current banner and lets the user accept.
|
||||
"""
|
||||
vendors, _profile, meta = _load_check_context(check_id)
|
||||
return build_banner_config(
|
||||
vendors=vendors,
|
||||
site_name=meta["site_name"] or meta["base_domain"],
|
||||
privacy_policy_url="",
|
||||
language=language,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{check_id}/document-preview")
|
||||
async def preview_document_migration(check_id: str) -> dict[str, Any]:
|
||||
"""Return pre-fills for cookie_policy / vvt_register / privacy_policy."""
|
||||
vendors, profile, meta = _load_check_context(check_id)
|
||||
prefills = build_document_prefills(
|
||||
vendors=vendors,
|
||||
extracted_profile={"company_profile": profile} if profile else None,
|
||||
site_name=meta["site_name"] or meta["base_domain"],
|
||||
privacy_policy_url="",
|
||||
)
|
||||
return {
|
||||
"check_id": check_id,
|
||||
"site_name": meta["site_name"],
|
||||
"base_domain": meta["base_domain"],
|
||||
"vendor_count": len(vendors),
|
||||
"templates": prefills,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{check_id}/summary")
|
||||
async def migration_summary(check_id: str) -> dict[str, Any]:
|
||||
"""High-level summary: how many vendors, how many cookies, how many issues."""
|
||||
vendors, profile, meta = _load_check_context(check_id)
|
||||
banner = build_banner_config(
|
||||
vendors=vendors,
|
||||
site_name=meta["site_name"] or meta["base_domain"],
|
||||
)
|
||||
return {
|
||||
"check_id": check_id,
|
||||
"site_name": meta["site_name"],
|
||||
"base_domain": meta["base_domain"],
|
||||
"company_name": (profile or {}).get("companyName", ""),
|
||||
"vendor_count": len(vendors),
|
||||
"banner_summary": banner.get("summary"),
|
||||
"available_templates": [
|
||||
"cookie_policy", "vvt_register", "privacy_policy",
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user