diff --git a/salesflow-saas/docs/LAUNCH_CHECKLIST.md b/salesflow-saas/docs/LAUNCH_CHECKLIST.md
index 7a1143f8..603cdfeb 100644
--- a/salesflow-saas/docs/LAUNCH_CHECKLIST.md
+++ b/salesflow-saas/docs/LAUNCH_CHECKLIST.md
@@ -2,19 +2,22 @@
## 1. الكود والاختبارات
-- [ ] `cd backend && py -m pytest tests -q` — يجب أن تمر كل الاختبارات.
-- [ ] `cd frontend && npm run lint && npm run build`.
+- [ ] **اختبارات الباكند:** من `backend/` شغّل `python -m pytest tests -q` (مثل CI على Linux) أو على ويندوز إذا الأمر `python` غير موجود: `py -3 -m pytest tests -q`.
+- [ ] **بوابة موحّدة (موصى به):** من جذر `salesflow-saas`: `.\verify-launch.ps1` — يشغّل pytest + مزامنة التسويق + lint + build.
+- [ ] `cd frontend && npm run lint && npm run build` (أو تُغطّى بواسطة `verify-launch.ps1`).
- [ ] من جذر `salesflow-saas`: `node scripts/sync-marketing-to-public.cjs` (يُشغَّل أيضاً تلقائياً قبل `npm run build`).
-- [ ] (اختياري) من جذر `salesflow-saas`: `py scripts/verify_frontend_openapi_paths.py` — يطابق مسارات `/api/v1` الظاهرة حرفيًا في الفرونت مع OpenAPI.
+- [ ] **E2E (Playwright):** بعد `npm run build`، حرّر المنفذ **3000** ثم من `frontend/`: `CI=true npm run test:e2e`. إن ظهر «port already in use» أو timeout على `webServer`: من جذر `salesflow-saas` شغّل `.\scripts\kill-port-3000.ps1` ثم أعد المحاولة.
+- [ ] (اختياري) من جذر `salesflow-saas`: `py -3 scripts/verify_frontend_openapi_paths.py` (أو `python3 scripts/...`) — يطابق مسارات `/api/v1` في الفرونت مع OpenAPI (حرفيًا وفي قوالب مثل `` `${base}/api/v1/...` ``).
## 2. الخادم (API)
- [ ] تشغيل من **أحدث** كود في المستودع:
- `cd backend && py -m uvicorn app.main:app --host 0.0.0.0 --port 8000`
+ `cd backend && python -m uvicorn app.main:app --host 0.0.0.0 --port 8000`
+ (ويندوز: `py -3 -m uvicorn app.main:app --host 0.0.0.0 --port 8000`)
- [ ] إذا ظهر **404** على `/api/v1/marketing/hub` أو `/api/v1/strategy/summary` فالعملية غالباً **قديمة** — أعد تشغيل `uvicorn` بعد `git pull`.
-- [ ] اختبار HTTP:
- `py scripts/full_stack_launch_test.py --http-only --soft-ready`
- أو:
+- [ ] اختبار HTTP (من مجلد `backend/`):
+ `py -3 scripts/full_stack_launch_test.py --http-only --soft-ready`
+ أو من جذر `salesflow-saas`:
`.\scripts\grand_launch_verify.ps1 -HttpCheck -SoftReady`
مع `DEALIX_BASE_URL` إذا لم يكن الـ API على `http://127.0.0.1:8000`.
diff --git a/salesflow-saas/frontend/next-env.d.ts b/salesflow-saas/frontend/next-env.d.ts
index 1b3be084..830fb594 100644
--- a/salesflow-saas/frontend/next-env.d.ts
+++ b/salesflow-saas/frontend/next-env.d.ts
@@ -1,5 +1,6 @@
///
///
+///
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/salesflow-saas/frontend/playwright.config.ts b/salesflow-saas/frontend/playwright.config.ts
index 959a6744..43ea66e7 100644
--- a/salesflow-saas/frontend/playwright.config.ts
+++ b/salesflow-saas/frontend/playwright.config.ts
@@ -16,7 +16,7 @@ export default defineConfig({
webServer: {
command: "node .next/standalone/server.js",
url: "http://127.0.0.1:3000",
- timeout: 120_000,
+ timeout: 180_000,
reuseExistingServer: !process.env.CI,
},
});
diff --git a/salesflow-saas/scripts/kill-port-3000.ps1 b/salesflow-saas/scripts/kill-port-3000.ps1
new file mode 100644
index 00000000..1f601cc1
--- /dev/null
+++ b/salesflow-saas/scripts/kill-port-3000.ps1
@@ -0,0 +1,7 @@
+# Stops processes listening on TCP port 3000 (fixes Playwright webServer "port already in use").
+# Run from salesflow-saas: .\scripts\kill-port-3000.ps1
+$ErrorActionPreference = "SilentlyContinue"
+Get-NetTCPConnection -LocalPort 3000 -ErrorAction SilentlyContinue | ForEach-Object {
+ Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue
+}
+Write-Host "Port 3000 cleared (if anything was listening)." -ForegroundColor DarkGray
diff --git a/salesflow-saas/scripts/verify_frontend_openapi_paths.py b/salesflow-saas/scripts/verify_frontend_openapi_paths.py
index 759de530..0a1ab5b2 100644
--- a/salesflow-saas/scripts/verify_frontend_openapi_paths.py
+++ b/salesflow-saas/scripts/verify_frontend_openapi_paths.py
@@ -1,10 +1,14 @@
#!/usr/bin/env python3
"""
-Scan frontend/src for literal /api/v1/... path strings and verify exact matches
+Scan frontend/src for /api/v1/... path strings and verify exact matches
against the FastAPI OpenAPI schema.
+Detects:
+ - Quoted literals: '/api/v1/foo', "/api/v1/foo", `/api/v1/foo`
+ - Template tails after ${...}: `${base}/api/v1/foo` (query string stripped)
+
Run from anywhere:
- py salesflow-saas/scripts/verify_frontend_openapi_paths.py
+ py -3 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).
"""
@@ -15,6 +19,10 @@ import re
import sys
from pathlib import Path
+# Paths that appear in the frontend but use OpenAPI path parameters ({id}, etc.)
+# or are intentionally not registered as separate operations — extend only with a comment.
+OPENAPI_PATH_ALLOWLIST: frozenset[str] = frozenset()
+
def main() -> int:
saas = Path(__file__).resolve().parent.parent
@@ -31,30 +39,37 @@ def main() -> int:
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_\-./]+))['"`]""")
+ quoted = re.compile(r"""['"`]((/api/v1/[a-zA-Z0-9_\-./]+))['"`]""")
+ after_subst = 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
+ for pat in (quoted,):
+ for m in pat.finditer(text):
+ raw = m.group(1).split("?")[0].rstrip("/")
+ if "${" in raw or "{" in raw:
+ continue
+ if raw.endswith("/api/v1"):
+ continue
+ found.add(raw)
+ for m in after_subst.finditer(text):
+ raw = m.group(1).split("?")[0].rstrip("/")
if raw.endswith("/api/v1"):
continue
found.add(raw)
- missing = sorted(p for p in found if p not in open_paths)
+ missing = sorted(p for p in found if p not in open_paths and p not in OPENAPI_PATH_ALLOWLIST)
if missing:
- print("Frontend literal paths not found as exact OpenAPI paths (may use path params or be dynamic):")
+ print("Frontend 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.")
+ print("\nTip: paths with {id} in OpenAPI need allowlisting or a manual mapping.")
return 1
- print(f"OK: {len(found)} literal /api/v1 paths match OpenAPI.")
+ print(f"OK: {len(found)} /api/v1 paths in frontend match OpenAPI.")
return 0