|
2 | 2 |
|
3 | 3 | from fastapi import APIRouter, Depends, HTTPException, Query, Response, status |
4 | 4 | from fastapi.encoders import jsonable_encoder |
5 | | -from fastapi.responses import JSONResponse |
| 5 | +from fastapi.responses import FileResponse, JSONResponse |
6 | 6 | from sqlalchemy import func, select |
7 | 7 | from sqlalchemy.orm import Session |
8 | 8 |
|
|
19 | 19 | from app.intelligence import runner as intelligence_runner |
20 | 20 | from app.inventory import get_asset, list_assets, list_observations, list_services |
21 | 21 | from app.models import Asset, Dockyard, EvidenceRecord, Finding, FindingEvidence, Service |
| 22 | +from app.reporting import runner as reporting_runner |
22 | 23 | from app.schemas import ( |
23 | 24 | AdapterRead, |
24 | 25 | AssetDetailRead, |
|
45 | 46 | ObservationRead, |
46 | 47 | ProfileRead, |
47 | 48 | RedPathGraphRead, |
| 49 | + ReportCreate, |
| 50 | + ReportRunRead, |
48 | 51 | ScopeEntryCreate, |
49 | 52 | ScopeEntryRead, |
50 | 53 | ScopeEvaluateRequest, |
@@ -453,6 +456,90 @@ def approve_intelligence_run( |
453 | 456 | raise HTTPException(status_code=code, detail=message) from error |
454 | 457 |
|
455 | 458 |
|
| 459 | +@router.get("/dockyards/{dockyard_id}/reports", response_model=list[ReportRunRead]) |
| 460 | +def read_reports( |
| 461 | + dockyard_id: int, limit: int = ListLimit, session: Session = Depends(get_session) |
| 462 | +) -> list[ReportRunRead]: |
| 463 | + require_dockyard(dockyard_id, session) |
| 464 | + return reporting_runner.list_runs(session, dockyard_id, limit) |
| 465 | + |
| 466 | + |
| 467 | +@router.post( |
| 468 | + "/dockyards/{dockyard_id}/reports", |
| 469 | + response_model=ReportRunRead, |
| 470 | + status_code=status.HTTP_201_CREATED, |
| 471 | +) |
| 472 | +def create_report( |
| 473 | + dockyard_id: int, |
| 474 | + payload: ReportCreate, |
| 475 | + session: Session = Depends(get_session), |
| 476 | +) -> ReportRunRead: |
| 477 | + """Snapshot all retained state; the empty request chooses no target or path.""" |
| 478 | + require_dockyard(dockyard_id, session) |
| 479 | + try: |
| 480 | + return reporting_runner.start_report(session, dockyard_id) |
| 481 | + except reporting_runner.ReportRejected as error: |
| 482 | + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(error)) from error |
| 483 | + |
| 484 | + |
| 485 | +@router.get("/dockyards/{dockyard_id}/reports/{run_id}", response_model=ReportRunRead) |
| 486 | +def read_report( |
| 487 | + dockyard_id: int, run_id: int, session: Session = Depends(get_session) |
| 488 | +) -> ReportRunRead: |
| 489 | + require_dockyard(dockyard_id, session) |
| 490 | + run = reporting_runner.get_run(session, dockyard_id, run_id) |
| 491 | + if run is None: |
| 492 | + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Report run not found") |
| 493 | + return run |
| 494 | + |
| 495 | + |
| 496 | +def _report_artifact(dockyard_id: int, run_id: int, artifact: str, session: Session): |
| 497 | + require_dockyard(dockyard_id, session) |
| 498 | + run = reporting_runner.get_run(session, dockyard_id, run_id) |
| 499 | + if run is None: |
| 500 | + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Report run not found") |
| 501 | + try: |
| 502 | + return reporting_runner.artifact_path(run, artifact) |
| 503 | + except reporting_runner.ReportRejected as error: |
| 504 | + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(error)) from error |
| 505 | + |
| 506 | + |
| 507 | +@router.get("/dockyards/{dockyard_id}/reports/{run_id}/technical") |
| 508 | +def read_technical_report( |
| 509 | + dockyard_id: int, run_id: int, session: Session = Depends(get_session) |
| 510 | +): |
| 511 | + path = _report_artifact(dockyard_id, run_id, "technical", session) |
| 512 | + return FileResponse(path, media_type="text/markdown; charset=utf-8") |
| 513 | + |
| 514 | + |
| 515 | +@router.get("/dockyards/{dockyard_id}/reports/{run_id}/executive") |
| 516 | +def read_executive_report( |
| 517 | + dockyard_id: int, run_id: int, session: Session = Depends(get_session) |
| 518 | +): |
| 519 | + path = _report_artifact(dockyard_id, run_id, "executive", session) |
| 520 | + return FileResponse(path, media_type="text/markdown; charset=utf-8") |
| 521 | + |
| 522 | + |
| 523 | +@router.get("/dockyards/{dockyard_id}/reports/{run_id}/manifest") |
| 524 | +def read_report_manifest( |
| 525 | + dockyard_id: int, run_id: int, session: Session = Depends(get_session) |
| 526 | +): |
| 527 | + path = _report_artifact(dockyard_id, run_id, "manifest", session) |
| 528 | + return FileResponse(path, media_type="application/json") |
| 529 | + |
| 530 | + |
| 531 | +@router.get("/dockyards/{dockyard_id}/reports/{run_id}/dockpack") |
| 532 | +def download_dockpack( |
| 533 | + dockyard_id: int, run_id: int, session: Session = Depends(get_session) |
| 534 | +): |
| 535 | + path = _report_artifact(dockyard_id, run_id, "dockpack", session) |
| 536 | + return FileResponse( |
| 537 | + path, |
| 538 | + media_type="application/zip", |
| 539 | + filename=f"reddock-dockyard-{dockyard_id}-report-{run_id}.dockpack.zip", |
| 540 | + ) |
| 541 | + |
| 542 | + |
456 | 543 | @router.get("/dockyards/{dockyard_id}/validations", response_model=list[ValidationRunRead]) |
457 | 544 | def read_validations( |
458 | 545 | dockyard_id: int, limit: int = ListLimit, session: Session = Depends(get_session) |
|
0 commit comments