mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-18 15:29:36 +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
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger("dealix.api.agents")
|
logger = logging.getLogger("dealix.api.agents")
|
||||||
router = APIRouter(prefix="/agents", tags=["AI Agent System"])
|
router = APIRouter(prefix="/agent-system", tags=["AI Agent System"])
|
||||||
|
|
||||||
|
|
||||||
# ═══ Schemas ═══════════════════════════════════════════════
|
# ═══ Schemas ═══════════════════════════════════════════════
|
||||||
@ -631,16 +631,38 @@ async def agent_system_overview():
|
|||||||
for k, v in sorted(layers.items())
|
for k, v in sorted(layers.items())
|
||||||
},
|
},
|
||||||
"api_endpoints": {
|
"api_endpoints": {
|
||||||
"Empire": ["/agents/empire/status", "/agents/list", "/agents/overview"],
|
"Empire": ["/agent-system/empire/status", "/agent-system/list", "/agent-system/overview"],
|
||||||
"Discovery": ["/agents/prospect", "/agents/prospect/sectors", "/agents/prospect/market-analysis",
|
"Discovery": [
|
||||||
"/agents/leads/discover", "/agents/leads/sources", "/agents/leads/verify-phone"],
|
"/agent-system/prospect",
|
||||||
"Engagement": ["/agents/whatsapp/campaign", "/agents/whatsapp/stats", "/agents/email/start-sequence"],
|
"/agent-system/prospect/sectors",
|
||||||
"Qualification": ["/agents/qualify/lead", "/agents/qualify/score", "/agents/qualify/intent"],
|
"/agent-system/prospect/market-analysis",
|
||||||
"Revenue": ["/agents/close/handle-objection", "/agents/close/proposal", "/agents/forecast/revenue"],
|
"/agent-system/leads/discover",
|
||||||
"Intelligence": ["/agents/intelligence/analyze-conversation", "/agents/intelligence/deal-health", "/agents/market/competitors"],
|
"/agent-system/leads/sources",
|
||||||
"CRM": ["/agents/crm/deal", "/agents/crm/pipeline"],
|
"/agent-system/leads/verify-phone",
|
||||||
"Content": ["/agents/content/generate"],
|
],
|
||||||
"CEO": ["/agents/ceo/daily-cycle", "/agents/ceo/optimize"],
|
"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:
|
except Exception as e:
|
||||||
|
|||||||
@ -126,7 +126,7 @@ async def alert_stats(tenant_id: str):
|
|||||||
|
|
||||||
@router.get("/digest", summary="Generate Arabic alert digest")
|
@router.get("/digest", summary="Generate Arabic alert digest")
|
||||||
async def generate_digest(tenant_id: str, user_id: Optional[str] = None,
|
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)
|
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 import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
import asyncio
|
import asyncio
|
||||||
@ -79,6 +80,17 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
_docs, _redoc, _openapi = _openapi_urls()
|
_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(
|
app = FastAPI(
|
||||||
title=f"{settings.APP_NAME} API",
|
title=f"{settings.APP_NAME} API",
|
||||||
description=(
|
description=(
|
||||||
@ -91,6 +103,7 @@ app = FastAPI(
|
|||||||
redoc_url=_redoc,
|
redoc_url=_redoc,
|
||||||
openapi_url=_openapi,
|
openapi_url=_openapi,
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
|
swagger_ui_parameters=_swagger_ui_parameters,
|
||||||
)
|
)
|
||||||
|
|
||||||
app.add_middleware(InternalApiTokenMiddleware)
|
app.add_middleware(InternalApiTokenMiddleware)
|
||||||
@ -106,6 +119,27 @@ app.add_middleware(
|
|||||||
# API Routes
|
# API Routes
|
||||||
app.include_router(api_router, prefix="/api/v1")
|
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) ─────────
|
# ── Static marketing assets (browse + direct download) ─────────
|
||||||
def _resolve_salesflow_root() -> Path:
|
def _resolve_salesflow_root() -> Path:
|
||||||
if settings.MARKETING_STATIC_ROOT.strip():
|
if settings.MARKETING_STATIC_ROOT.strip():
|
||||||
|
|||||||
@ -35,6 +35,16 @@ def _exempt_path(path: str) -> bool:
|
|||||||
return True
|
return True
|
||||||
if path.startswith("/api/v1/affiliates/leaderboard"):
|
if path.startswith("/api/v1/affiliates/leaderboard"):
|
||||||
return True
|
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
|
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 program (public)", "GET", "/api/v1/affiliates/program"))
|
||||||
results.append(await check("affiliates leaderboard", "GET", "/api/v1/affiliates/leaderboard/top"))
|
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 list", "GET", "/api/v1/agent-system/list"))
|
||||||
results.append(await check("agents empire status", "GET", "/api/v1/agents/empire/status"))
|
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 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("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(
|
results.append(
|
||||||
await check(
|
await check(
|
||||||
"integration connectivity matrix",
|
"integration connectivity matrix",
|
||||||
@ -204,7 +204,7 @@ async def main() -> int:
|
|||||||
await check(
|
await check(
|
||||||
"LangGraph CEO deal cycle (realistic, slow)",
|
"LangGraph CEO deal cycle (realistic, slow)",
|
||||||
"POST",
|
"POST",
|
||||||
"/api/v1/agents/ceo/langgraph-deal-cycle",
|
"/api/v1/agent-system/ceo/langgraph-deal-cycle",
|
||||||
timeout=120.0,
|
timeout=120.0,
|
||||||
json={
|
json={
|
||||||
"company_name": "Launch Verification Co",
|
"company_name": "Launch Verification Co",
|
||||||
|
|||||||
@ -48,17 +48,17 @@ async def test_go_live_gate_semantics(client):
|
|||||||
@pytest.mark.launch
|
@pytest.mark.launch
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_agents_list_and_empire_and_langgraph_health(client):
|
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
|
assert lst.status_code == 200
|
||||||
body = lst.json()
|
body = lst.json()
|
||||||
assert body.get("total", 0) >= 1
|
assert body.get("total", 0) >= 1
|
||||||
assert isinstance(body.get("agents"), list)
|
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 emp.status_code == 200
|
||||||
assert "empire" in emp.json() or "status" in emp.json()
|
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
|
assert lg.status_code == 200
|
||||||
lgj = lg.json()
|
lgj = lg.json()
|
||||||
assert "graph_version" in lgj or "error" in lgj
|
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)
|
monkeypatch.setattr(lead_engine_mod.LeadEngine, "execute", fake_execute)
|
||||||
|
|
||||||
r = await client.post(
|
r = await client.post(
|
||||||
"/api/v1/agents/ceo/langgraph-deal-cycle",
|
"/api/v1/agent-system/ceo/langgraph-deal-cycle",
|
||||||
json={
|
json={
|
||||||
"company_name": "Scenario Corp LaunchTest",
|
"company_name": "Scenario Corp LaunchTest",
|
||||||
"deal_id": "SC-LT-1",
|
"deal_id": "SC-LT-1",
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
- [ ] `cd backend && py -m pytest tests -q` — يجب أن تمر كل الاختبارات.
|
- [ ] `cd backend && py -m pytest tests -q` — يجب أن تمر كل الاختبارات.
|
||||||
- [ ] `cd frontend && npm run lint && npm run build`.
|
- [ ] `cd frontend && npm run lint && npm run build`.
|
||||||
- [ ] من جذر `salesflow-saas`: `node scripts/sync-marketing-to-public.cjs` (يُشغَّل أيضاً تلقائياً قبل `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)
|
## 2. الخادم (API)
|
||||||
|
|
||||||
|
|||||||
@ -10,8 +10,9 @@ import {
|
|||||||
Landmark,
|
Landmark,
|
||||||
Compass,
|
Compass,
|
||||||
} from "lucide-react";
|
} 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 = {
|
export const metadata = {
|
||||||
title: "موارد Dealix — عروض وحالات استخدام",
|
title: "موارد Dealix — عروض وحالات استخدام",
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { getApiBaseUrl } from "@/lib/api-base";
|
||||||
const API = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
|
||||||
|
|
||||||
interface AgentStatus {
|
interface AgentStatus {
|
||||||
role: string;
|
role: string;
|
||||||
@ -40,7 +39,8 @@ export function IntelligenceDashboard() {
|
|||||||
|
|
||||||
const fetchAgentStatus = async () => {
|
const fetchAgentStatus = async () => {
|
||||||
try {
|
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) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setAgents(data.agents || []);
|
setAgents(data.agents || []);
|
||||||
@ -50,7 +50,8 @@ export function IntelligenceDashboard() {
|
|||||||
|
|
||||||
const fetchHealth = async () => {
|
const fetchHealth = async () => {
|
||||||
try {
|
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());
|
if (res.ok) setHealth(await res.json());
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
@ -59,7 +60,8 @@ export function IntelligenceDashboard() {
|
|||||||
if (!leadForm.contact_name || !leadForm.company_name) return;
|
if (!leadForm.contact_name || !leadForm.company_name) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
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",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ id: `lead_${Date.now()}`, ...leadForm }),
|
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={{ fontWeight: 700, fontSize: 16, color: "#e2e8f0", marginBottom: 8 }}>{report.title}</div>
|
||||||
<div style={{ fontSize: 13, color: "#64748b", marginBottom: 20 }}>{report.desc}</div>
|
<div style={{ fontSize: 13, color: "#64748b", marginBottom: 20 }}>{report.desc}</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => window.open(`${API}${report.endpoint}`, "_blank")}
|
onClick={() =>
|
||||||
|
window.open(
|
||||||
|
`${getApiBaseUrl().replace(/\/$/, "")}${report.endpoint}`,
|
||||||
|
"_blank"
|
||||||
|
)
|
||||||
|
}
|
||||||
style={{
|
style={{
|
||||||
padding: "10px 20px", background: "#0a0a0f", border: "1px solid #F5A623",
|
padding: "10px 20px", background: "#0a0a0f", border: "1px solid #F5A623",
|
||||||
borderRadius: 8, color: "#F5A623", cursor: "pointer", fontWeight: 600, fontSize: 14
|
borderRadius: 8, color: "#F5A623", cursor: "pointer", fontWeight: 600, fontSize: 14
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { getApiBaseUrl } from "@/lib/api-base";
|
||||||
const API = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
|
||||||
|
|
||||||
export function LeadGeneratorView() {
|
export function LeadGeneratorView() {
|
||||||
const [sector, setSector] = useState("تقنية المعلومات");
|
const [sector, setSector] = useState("تقنية المعلومات");
|
||||||
@ -28,7 +27,8 @@ export function LeadGeneratorView() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setLeads([]);
|
setLeads([]);
|
||||||
try {
|
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"
|
method: "POST"
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@ -56,7 +56,8 @@ export function LeadGeneratorView() {
|
|||||||
const runPipeline = async (lead: any) => {
|
const runPipeline = async (lead: any) => {
|
||||||
setPipelineRunning(lead.company_name);
|
setPipelineRunning(lead.company_name);
|
||||||
try {
|
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",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
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