diff --git a/salesflow-saas/backend/app/api/v1/agent_system.py b/salesflow-saas/backend/app/api/v1/agent_system.py index 058c8667..ec6165bc 100644 --- a/salesflow-saas/backend/app/api/v1/agent_system.py +++ b/salesflow-saas/backend/app/api/v1/agent_system.py @@ -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: diff --git a/salesflow-saas/backend/app/api/v1/intelligence.py b/salesflow-saas/backend/app/api/v1/intelligence.py index 1fed1941..2afff994 100644 --- a/salesflow-saas/backend/app/api/v1/intelligence.py +++ b/salesflow-saas/backend/app/api/v1/intelligence.py @@ -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) diff --git a/salesflow-saas/backend/app/main.py b/salesflow-saas/backend/app/main.py index 77466677..b2dfedd6 100644 --- a/salesflow-saas/backend/app/main.py +++ b/salesflow-saas/backend/app/main.py @@ -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(): diff --git a/salesflow-saas/backend/app/middleware/internal_api.py b/salesflow-saas/backend/app/middleware/internal_api.py index 333a29b1..3320265f 100644 --- a/salesflow-saas/backend/app/middleware/internal_api.py +++ b/salesflow-saas/backend/app/middleware/internal_api.py @@ -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 diff --git a/salesflow-saas/backend/app/static/docs/swagger-dealix.css b/salesflow-saas/backend/app/static/docs/swagger-dealix.css new file mode 100644 index 00000000..05865783 --- /dev/null +++ b/salesflow-saas/backend/app/static/docs/swagger-dealix.css @@ -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; +} diff --git a/salesflow-saas/backend/scripts/full_stack_launch_test.py b/salesflow-saas/backend/scripts/full_stack_launch_test.py index 0770df4f..e77c1d06 100644 --- a/salesflow-saas/backend/scripts/full_stack_launch_test.py +++ b/salesflow-saas/backend/scripts/full_stack_launch_test.py @@ -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", diff --git a/salesflow-saas/backend/tests/test_launch_readiness_scenarios.py b/salesflow-saas/backend/tests/test_launch_readiness_scenarios.py index 3877504a..9e05fbcb 100644 --- a/salesflow-saas/backend/tests/test_launch_readiness_scenarios.py +++ b/salesflow-saas/backend/tests/test_launch_readiness_scenarios.py @@ -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", diff --git a/salesflow-saas/docs/LAUNCH_CHECKLIST.md b/salesflow-saas/docs/LAUNCH_CHECKLIST.md index d197a938..7a1143f8 100644 --- a/salesflow-saas/docs/LAUNCH_CHECKLIST.md +++ b/salesflow-saas/docs/LAUNCH_CHECKLIST.md @@ -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) diff --git a/salesflow-saas/frontend/src/app/resources/page.tsx b/salesflow-saas/frontend/src/app/resources/page.tsx index a5e2ccf1..5fcaab3e 100644 --- a/salesflow-saas/frontend/src/app/resources/page.tsx +++ b/salesflow-saas/frontend/src/app/resources/page.tsx @@ -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 — عروض وحالات استخدام", diff --git a/salesflow-saas/frontend/src/components/dealix/intelligence-dashboard.tsx b/salesflow-saas/frontend/src/components/dealix/intelligence-dashboard.tsx index b11569e5..f00ea0f0 100644 --- a/salesflow-saas/frontend/src/components/dealix/intelligence-dashboard.tsx +++ b/salesflow-saas/frontend/src/components/dealix/intelligence-dashboard.tsx @@ -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() {