mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-17 23:09:35 +00:00
feat(dealix): FE/BE audit — agent-system prefix, OpenAPI path check, Swagger theme
- Move agent_system router to /api/v1/agent-system to avoid /agents conflicts - Exempt demo UI API paths from internal token when DEALIX_INTERNAL_API_TOKEN is set - Replace deprecated Query(regex=) with pattern= in intelligence - GET / redirects to /api/docs; mount docs-assets + custom Swagger CSS - Frontend: use getApiBaseUrl() for API URLs; fix intelligence dashboard base - Add scripts/verify_frontend_openapi_paths.py; note in LAUNCH_CHECKLIST Made-with: Cursor
This commit is contained in:
parent
8c3d91c070
commit
fcdbc1f004
@ -10,7 +10,7 @@ from datetime import datetime, timezone
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("dealix.api.agents")
|
||||
router = APIRouter(prefix="/agents", tags=["AI Agent System"])
|
||||
router = APIRouter(prefix="/agent-system", tags=["AI Agent System"])
|
||||
|
||||
|
||||
# ═══ Schemas ═══════════════════════════════════════════════
|
||||
@ -631,16 +631,38 @@ async def agent_system_overview():
|
||||
for k, v in sorted(layers.items())
|
||||
},
|
||||
"api_endpoints": {
|
||||
"Empire": ["/agents/empire/status", "/agents/list", "/agents/overview"],
|
||||
"Discovery": ["/agents/prospect", "/agents/prospect/sectors", "/agents/prospect/market-analysis",
|
||||
"/agents/leads/discover", "/agents/leads/sources", "/agents/leads/verify-phone"],
|
||||
"Engagement": ["/agents/whatsapp/campaign", "/agents/whatsapp/stats", "/agents/email/start-sequence"],
|
||||
"Qualification": ["/agents/qualify/lead", "/agents/qualify/score", "/agents/qualify/intent"],
|
||||
"Revenue": ["/agents/close/handle-objection", "/agents/close/proposal", "/agents/forecast/revenue"],
|
||||
"Intelligence": ["/agents/intelligence/analyze-conversation", "/agents/intelligence/deal-health", "/agents/market/competitors"],
|
||||
"CRM": ["/agents/crm/deal", "/agents/crm/pipeline"],
|
||||
"Content": ["/agents/content/generate"],
|
||||
"CEO": ["/agents/ceo/daily-cycle", "/agents/ceo/optimize"],
|
||||
"Empire": ["/agent-system/empire/status", "/agent-system/list", "/agent-system/overview"],
|
||||
"Discovery": [
|
||||
"/agent-system/prospect",
|
||||
"/agent-system/prospect/sectors",
|
||||
"/agent-system/prospect/market-analysis",
|
||||
"/agent-system/leads/discover",
|
||||
"/agent-system/leads/sources",
|
||||
"/agent-system/leads/verify-phone",
|
||||
],
|
||||
"Engagement": [
|
||||
"/agent-system/whatsapp/campaign",
|
||||
"/agent-system/whatsapp/stats",
|
||||
"/agent-system/email/start-sequence",
|
||||
],
|
||||
"Qualification": [
|
||||
"/agent-system/qualify/lead",
|
||||
"/agent-system/qualify/score",
|
||||
"/agent-system/qualify/intent",
|
||||
],
|
||||
"Revenue": [
|
||||
"/agent-system/close/handle-objection",
|
||||
"/agent-system/close/proposal",
|
||||
"/agent-system/forecast/revenue",
|
||||
],
|
||||
"Intelligence": [
|
||||
"/agent-system/intelligence/analyze-conversation",
|
||||
"/agent-system/intelligence/deal-health",
|
||||
"/agent-system/market/competitors",
|
||||
],
|
||||
"CRM": ["/agent-system/crm/deal", "/agent-system/crm/pipeline"],
|
||||
"Content": ["/agent-system/content/generate"],
|
||||
"CEO": ["/agent-system/ceo/daily-cycle", "/agent-system/ceo/optimize"],
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
|
||||
@ -126,7 +126,7 @@ async def alert_stats(tenant_id: str):
|
||||
|
||||
@router.get("/digest", summary="Generate Arabic alert digest")
|
||||
async def generate_digest(tenant_id: str, user_id: Optional[str] = None,
|
||||
period: str = Query(default="daily", regex="^(daily|weekly)$")):
|
||||
period: str = Query(default="daily", pattern="^(daily|weekly)$")):
|
||||
return await get_alert_delivery().generate_digest(tenant_id, user_id, period)
|
||||
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from contextlib import asynccontextmanager
|
||||
import asyncio
|
||||
@ -79,6 +80,17 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
_docs, _redoc, _openapi = _openapi_urls()
|
||||
|
||||
_docs_static_dir = Path(__file__).resolve().parent / "static" / "docs"
|
||||
_swagger_ui_parameters = None
|
||||
if _docs and (_docs_static_dir / "swagger-dealix.css").is_file():
|
||||
_swagger_ui_parameters = {
|
||||
"persistAuthorization": True,
|
||||
"displayRequestDuration": True,
|
||||
"filter": True,
|
||||
"tryItOutEnabled": True,
|
||||
"customCssUrl": "/api/docs-assets/swagger-dealix.css",
|
||||
}
|
||||
|
||||
app = FastAPI(
|
||||
title=f"{settings.APP_NAME} API",
|
||||
description=(
|
||||
@ -91,6 +103,7 @@ app = FastAPI(
|
||||
redoc_url=_redoc,
|
||||
openapi_url=_openapi,
|
||||
lifespan=lifespan,
|
||||
swagger_ui_parameters=_swagger_ui_parameters,
|
||||
)
|
||||
|
||||
app.add_middleware(InternalApiTokenMiddleware)
|
||||
@ -106,6 +119,27 @@ app.add_middleware(
|
||||
# API Routes
|
||||
app.include_router(api_router, prefix="/api/v1")
|
||||
|
||||
if _docs and _docs_static_dir.is_dir():
|
||||
app.mount(
|
||||
"/api/docs-assets",
|
||||
StaticFiles(directory=str(_docs_static_dir)),
|
||||
name="docs_assets",
|
||||
)
|
||||
|
||||
|
||||
@app.get("/", include_in_schema=False)
|
||||
async def root_redirect():
|
||||
"""Avoid bare 404 on API origin; send developers to interactive docs."""
|
||||
if _docs:
|
||||
return RedirectResponse(url=_docs, status_code=307)
|
||||
return {
|
||||
"service": settings.APP_NAME,
|
||||
"api": "/api/v1",
|
||||
"health": "/api/v1/health",
|
||||
"note": "OpenAPI UI disabled (EXPOSE_OPENAPI=false).",
|
||||
}
|
||||
|
||||
|
||||
# ── Static marketing assets (browse + direct download) ─────────
|
||||
def _resolve_salesflow_root() -> Path:
|
||||
if settings.MARKETING_STATIC_ROOT.strip():
|
||||
|
||||
@ -35,6 +35,16 @@ def _exempt_path(path: str) -> bool:
|
||||
return True
|
||||
if path.startswith("/api/v1/affiliates/leaderboard"):
|
||||
return True
|
||||
# Dashboard / demo widgets that call the API from the browser without internal token
|
||||
# (still require JWT on routes that use Depends(get_current_user).)
|
||||
if path in (
|
||||
"/api/v1/agents/status",
|
||||
"/api/v1/intelligence/health",
|
||||
"/api/v1/intelligence/run-pipeline",
|
||||
"/api/v1/dealix/generate-leads",
|
||||
"/api/v1/dealix/full-power",
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
|
||||
144
salesflow-saas/backend/app/static/docs/swagger-dealix.css
Normal file
144
salesflow-saas/backend/app/static/docs/swagger-dealix.css
Normal file
@ -0,0 +1,144 @@
|
||||
/* Dealix API docs — Swagger UI theme (dark, RTL-friendly typography) */
|
||||
|
||||
@import url("https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,600;0,9..40,700&family=IBM+Plex+Sans+Arabic:wght@400;600;700&display=swap");
|
||||
|
||||
.swagger-ui {
|
||||
font-family: "DM Sans", "IBM Plex Sans Arabic", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.swagger-ui,
|
||||
.swagger-ui .wrapper {
|
||||
background: linear-gradient(165deg, #070b12 0%, #0f172a 45%, #0c1222 100%);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.swagger-ui .topbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.swagger-ui .information-container,
|
||||
.swagger-ui .scheme-container {
|
||||
background: rgba(15, 23, 42, 0.72);
|
||||
border: 1px solid rgba(45, 212, 191, 0.18);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.35);
|
||||
margin: 1.25rem 0;
|
||||
padding: 1.25rem 1.5rem;
|
||||
}
|
||||
|
||||
.swagger-ui .info .title {
|
||||
color: #f8fafc;
|
||||
font-size: 1.85rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.swagger-ui .info .title small {
|
||||
background: rgba(45, 212, 191, 0.15);
|
||||
border: 1px solid rgba(45, 212, 191, 0.35);
|
||||
border-radius: 999px;
|
||||
color: #5eead4;
|
||||
padding: 0.2rem 0.65rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.swagger-ui .info .base-url,
|
||||
.swagger-ui .info .description,
|
||||
.swagger-ui .info p,
|
||||
.swagger-ui .info li {
|
||||
color: #cbd5e1;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.swagger-ui .info a,
|
||||
.swagger-ui a {
|
||||
color: #5eead4;
|
||||
}
|
||||
|
||||
.swagger-ui .info a:hover,
|
||||
.swagger-ui a:hover {
|
||||
color: #99f6e4;
|
||||
}
|
||||
|
||||
.swagger-ui .btn.authorize {
|
||||
border-color: rgba(45, 212, 191, 0.45);
|
||||
color: #5eead4;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.swagger-ui .btn.authorize:hover {
|
||||
background: rgba(45, 212, 191, 0.12);
|
||||
}
|
||||
|
||||
.swagger-ui .filter .operation-filter-input {
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||
border-radius: 10px;
|
||||
color: #f1f5f9;
|
||||
padding: 0.55rem 0.85rem;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock {
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.15);
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
margin-bottom: 0.65rem;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.swagger-ui .opblock .opblock-summary {
|
||||
border-radius: 11px;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock.opblock-get .opblock-summary-method {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock.opblock-post .opblock-summary-method {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock.opblock-put .opblock-summary-method {
|
||||
background: #d97706;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock.opblock-delete .opblock-summary-method {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock .opblock-summary-path,
|
||||
.swagger-ui .opblock .opblock-summary-description {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.swagger-ui .opblock-body pre,
|
||||
.swagger-ui .microlight {
|
||||
background: #020617 !important;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(51, 65, 85, 0.6);
|
||||
}
|
||||
|
||||
.swagger-ui table thead tr th,
|
||||
.swagger-ui table tbody tr td {
|
||||
border-color: rgba(51, 65, 85, 0.6);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.swagger-ui .model-box,
|
||||
.swagger-ui .model {
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.swagger-ui section.models {
|
||||
border-color: rgba(45, 212, 191, 0.2);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.swagger-ui .tab li button.tablinks {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.swagger-ui .tab li button.tablinks.active {
|
||||
color: #5eead4;
|
||||
}
|
||||
@ -186,11 +186,11 @@ async def main() -> int:
|
||||
)
|
||||
results.append(await check("affiliates program (public)", "GET", "/api/v1/affiliates/program"))
|
||||
results.append(await check("affiliates leaderboard", "GET", "/api/v1/affiliates/leaderboard/top"))
|
||||
results.append(await check("agents list", "GET", "/api/v1/agents/list"))
|
||||
results.append(await check("agents empire status", "GET", "/api/v1/agents/empire/status"))
|
||||
results.append(await check("agents list", "GET", "/api/v1/agent-system/list"))
|
||||
results.append(await check("agents empire status", "GET", "/api/v1/agent-system/empire/status"))
|
||||
results.append(await check("openclaw safe core health", "GET", "/api/v1/autonomous-foundation/openclaw/health"))
|
||||
results.append(await check("openclaw runs telemetry", "GET", "/api/v1/autonomous-foundation/openclaw/runs"))
|
||||
results.append(await check("LangGraph orchestrator health", "GET", "/api/v1/agents/langgraph/health"))
|
||||
results.append(await check("LangGraph orchestrator health", "GET", "/api/v1/agent-system/langgraph/health"))
|
||||
results.append(
|
||||
await check(
|
||||
"integration connectivity matrix",
|
||||
@ -204,7 +204,7 @@ async def main() -> int:
|
||||
await check(
|
||||
"LangGraph CEO deal cycle (realistic, slow)",
|
||||
"POST",
|
||||
"/api/v1/agents/ceo/langgraph-deal-cycle",
|
||||
"/api/v1/agent-system/ceo/langgraph-deal-cycle",
|
||||
timeout=120.0,
|
||||
json={
|
||||
"company_name": "Launch Verification Co",
|
||||
|
||||
@ -48,17 +48,17 @@ async def test_go_live_gate_semantics(client):
|
||||
@pytest.mark.launch
|
||||
@pytest.mark.asyncio
|
||||
async def test_agents_list_and_empire_and_langgraph_health(client):
|
||||
lst = await client.get("/api/v1/agents/list")
|
||||
lst = await client.get("/api/v1/agent-system/list")
|
||||
assert lst.status_code == 200
|
||||
body = lst.json()
|
||||
assert body.get("total", 0) >= 1
|
||||
assert isinstance(body.get("agents"), list)
|
||||
|
||||
emp = await client.get("/api/v1/agents/empire/status")
|
||||
emp = await client.get("/api/v1/agent-system/empire/status")
|
||||
assert emp.status_code == 200
|
||||
assert "empire" in emp.json() or "status" in emp.json()
|
||||
|
||||
lg = await client.get("/api/v1/agents/langgraph/health")
|
||||
lg = await client.get("/api/v1/agent-system/langgraph/health")
|
||||
assert lg.status_code == 200
|
||||
lgj = lg.json()
|
||||
assert "graph_version" in lgj or "error" in lgj
|
||||
@ -113,7 +113,7 @@ async def test_ceo_langgraph_deal_cycle_via_api_mocked_engine(client, monkeypatc
|
||||
monkeypatch.setattr(lead_engine_mod.LeadEngine, "execute", fake_execute)
|
||||
|
||||
r = await client.post(
|
||||
"/api/v1/agents/ceo/langgraph-deal-cycle",
|
||||
"/api/v1/agent-system/ceo/langgraph-deal-cycle",
|
||||
json={
|
||||
"company_name": "Scenario Corp LaunchTest",
|
||||
"deal_id": "SC-LT-1",
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
- [ ] `cd backend && py -m pytest tests -q` — يجب أن تمر كل الاختبارات.
|
||||
- [ ] `cd frontend && npm run lint && npm run build`.
|
||||
- [ ] من جذر `salesflow-saas`: `node scripts/sync-marketing-to-public.cjs` (يُشغَّل أيضاً تلقائياً قبل `npm run build`).
|
||||
- [ ] (اختياري) من جذر `salesflow-saas`: `py scripts/verify_frontend_openapi_paths.py` — يطابق مسارات `/api/v1` الظاهرة حرفيًا في الفرونت مع OpenAPI.
|
||||
|
||||
## 2. الخادم (API)
|
||||
|
||||
|
||||
@ -10,8 +10,9 @@ import {
|
||||
Landmark,
|
||||
Compass,
|
||||
} from "lucide-react";
|
||||
import { getApiBaseUrl } from "@/lib/api-base";
|
||||
|
||||
const API = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8000";
|
||||
const API = getApiBaseUrl();
|
||||
|
||||
export const metadata = {
|
||||
title: "موارد Dealix — عروض وحالات استخدام",
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const API = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
||||
import { getApiBaseUrl } from "@/lib/api-base";
|
||||
|
||||
interface AgentStatus {
|
||||
role: string;
|
||||
@ -40,7 +39,8 @@ export function IntelligenceDashboard() {
|
||||
|
||||
const fetchAgentStatus = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API}/api/v1/agents/status`);
|
||||
const base = getApiBaseUrl().replace(/\/$/, "");
|
||||
const res = await fetch(`${base}/api/v1/agents/status`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setAgents(data.agents || []);
|
||||
@ -50,7 +50,8 @@ export function IntelligenceDashboard() {
|
||||
|
||||
const fetchHealth = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API}/api/v1/intelligence/health`);
|
||||
const base = getApiBaseUrl().replace(/\/$/, "");
|
||||
const res = await fetch(`${base}/api/v1/intelligence/health`);
|
||||
if (res.ok) setHealth(await res.json());
|
||||
} catch {}
|
||||
};
|
||||
@ -59,7 +60,8 @@ export function IntelligenceDashboard() {
|
||||
if (!leadForm.contact_name || !leadForm.company_name) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`${API}/api/v1/intelligence/run-pipeline`, {
|
||||
const base = getApiBaseUrl().replace(/\/$/, "");
|
||||
const res = await fetch(`${base}/api/v1/intelligence/run-pipeline`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id: `lead_${Date.now()}`, ...leadForm }),
|
||||
@ -251,7 +253,12 @@ export function IntelligenceDashboard() {
|
||||
<div style={{ fontWeight: 700, fontSize: 16, color: "#e2e8f0", marginBottom: 8 }}>{report.title}</div>
|
||||
<div style={{ fontSize: 13, color: "#64748b", marginBottom: 20 }}>{report.desc}</div>
|
||||
<button
|
||||
onClick={() => window.open(`${API}${report.endpoint}`, "_blank")}
|
||||
onClick={() =>
|
||||
window.open(
|
||||
`${getApiBaseUrl().replace(/\/$/, "")}${report.endpoint}`,
|
||||
"_blank"
|
||||
)
|
||||
}
|
||||
style={{
|
||||
padding: "10px 20px", background: "#0a0a0f", border: "1px solid #F5A623",
|
||||
borderRadius: 8, color: "#F5A623", cursor: "pointer", fontWeight: 600, fontSize: 14
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
const API = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
||||
import { getApiBaseUrl } from "@/lib/api-base";
|
||||
|
||||
export function LeadGeneratorView() {
|
||||
const [sector, setSector] = useState("تقنية المعلومات");
|
||||
@ -28,7 +27,8 @@ export function LeadGeneratorView() {
|
||||
setLoading(true);
|
||||
setLeads([]);
|
||||
try {
|
||||
const res = await fetch(`${API}/api/v1/dealix/generate-leads?sector=${encodeURIComponent(sector)}&city=${encodeURIComponent(city)}&count=${count}`, {
|
||||
const base = getApiBaseUrl().replace(/\/$/, "");
|
||||
const res = await fetch(`${base}/api/v1/dealix/generate-leads?sector=${encodeURIComponent(sector)}&city=${encodeURIComponent(city)}&count=${count}`, {
|
||||
method: "POST"
|
||||
});
|
||||
if (res.ok) {
|
||||
@ -56,7 +56,8 @@ export function LeadGeneratorView() {
|
||||
const runPipeline = async (lead: any) => {
|
||||
setPipelineRunning(lead.company_name);
|
||||
try {
|
||||
const res = await fetch(`${API}/api/v1/dealix/full-power`, {
|
||||
const base = getApiBaseUrl().replace(/\/$/, "");
|
||||
const res = await fetch(`${base}/api/v1/dealix/full-power`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
|
||||
62
salesflow-saas/scripts/verify_frontend_openapi_paths.py
Normal file
62
salesflow-saas/scripts/verify_frontend_openapi_paths.py
Normal file
@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Scan frontend/src for literal /api/v1/... path strings and verify exact matches
|
||||
against the FastAPI OpenAPI schema.
|
||||
|
||||
Run from anywhere:
|
||||
py salesflow-saas/scripts/verify_frontend_openapi_paths.py
|
||||
|
||||
Requires backend deps on PYTHONPATH (run after: cd salesflow-saas/backend && py -m pip install -r requirements.txt).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
saas = Path(__file__).resolve().parent.parent
|
||||
backend = saas / "backend"
|
||||
fe_src = saas / "frontend" / "src"
|
||||
|
||||
os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///./openapi_verify.db")
|
||||
os.environ.setdefault("DEALIX_INTERNAL_API_TOKEN", "")
|
||||
sys.path.insert(0, str(backend))
|
||||
os.chdir(backend)
|
||||
|
||||
from app.main import app
|
||||
|
||||
schema = app.openapi()
|
||||
open_paths = {p.rstrip("/") or "/" for p in schema.get("paths", {}).keys()}
|
||||
|
||||
# Literal path segments in quotes or template strings (no ${...} inside path)
|
||||
pat = re.compile(r"""['"`]((/api/v1/[a-zA-Z0-9_\-./]+))['"`]""")
|
||||
found: set[str] = set()
|
||||
for p in fe_src.rglob("*"):
|
||||
if p.suffix not in (".ts", ".tsx"):
|
||||
continue
|
||||
text = p.read_text(encoding="utf-8", errors="ignore")
|
||||
for m in pat.finditer(text):
|
||||
raw = m.group(1).rstrip("/")
|
||||
if "${" in raw or "{" in raw:
|
||||
continue
|
||||
if raw.endswith("/api/v1"):
|
||||
continue
|
||||
found.add(raw)
|
||||
|
||||
missing = sorted(p for p in found if p not in open_paths)
|
||||
if missing:
|
||||
print("Frontend literal paths not found as exact OpenAPI paths (may use path params or be dynamic):")
|
||||
for m in missing:
|
||||
print(f" - {m}")
|
||||
print("\nTip: paths with {{id}} in OpenAPI need manual review.")
|
||||
return 1
|
||||
|
||||
print(f"OK: {len(found)} literal /api/v1 paths match OpenAPI.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Reference in New Issue
Block a user