#!/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" echo "============================================================" echo " DOLOR3V BACKEND — MISSING CONTRACTS + REAL TOOL REPAIR" echo "============================================================" echo echo "===== 1. FIX app.py cms_router IMPORT CRASH =====" python3 - <<'PY' from pathlib import Path p = Path("app.py") src = p.read_text() # Remove the broken cms_router line if it's there without an import if "app.include_router(cms_router)" in src and "from" not in src.split("cms_router")[0].split("\n")[-1]: src = src.replace("app.include_router(cms_router)\n", "") p.write_text(src) print("[FIXED] Removed unresolved cms_router from app.py") else: print("[OK] app.py cms_router already handled or import present") PY echo echo "===== 2. ADD /api/deploy/settings ROUTE =====" mkdir -p backend/api/routes cat > backend/api/routes/deploy_settings.py << 'PYEOF' """ Production deployment-settings route. GET /api/deploy/settings — returns current deployment configuration POST /api/deploy/settings — updates deployment configuration """ from __future__ import annotations import json import logging import os from pathlib import Path from typing import Any from fastapi import APIRouter, HTTPException from pydantic import BaseModel logger = logging.getLogger("dolor3v.deploy.settings") router = APIRouter(prefix="/api/deploy", tags=["deploy"]) SETTINGS_FILE = Path(os.environ.get("DEPLOY_SETTINGS_PATH", "/tmp/deploy_settings.json")) DEFAULTS: dict[str, Any] = { "target": os.environ.get("DEPLOY_TARGET", "cloudflare"), "cloudflare_account_id": os.environ.get("CLOUDFLARE_ACCOUNT_ID", ""), "cloudflare_api_token": "", "render_service_id": os.environ.get("RENDER_SERVICE_ID", ""), "hf_space": os.environ.get("HF_SPACE", "Daviddolor/Travelerdev"), "auto_deploy": False, "build_command": "npm run build", "output_dir": ".next", "environment": os.environ.get("ENVIRONMENT", "production"), "backend_url": os.environ.get( "TRAVELER_BACKEND_URL", os.environ.get("NEXT_PUBLIC_BACKEND_URL", ""), ), } def _load() -> dict[str, Any]: if SETTINGS_FILE.exists(): try: return {**DEFAULTS, **json.loads(SETTINGS_FILE.read_text())} except Exception: pass return dict(DEFAULTS) def _save(data: dict[str, Any]) -> None: SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True) SETTINGS_FILE.write_text(json.dumps(data, indent=2)) class DeploySettingsUpdate(BaseModel): target: str | None = None cloudflare_account_id: str | None = None cloudflare_api_token: str | None = None render_service_id: str | None = None hf_space: str | None = None auto_deploy: bool | None = None build_command: str | None = None output_dir: str | None = None environment: str | None = None backend_url: str | None = None @router.get("/settings") async def get_deploy_settings() -> dict[str, Any]: """Return current deployment configuration (secrets redacted).""" settings = _load() redacted = {**settings} if redacted.get("cloudflare_api_token"): redacted["cloudflare_api_token"] = "***" return {"success": True, "settings": redacted} @router.post("/settings") async def update_deploy_settings(body: DeploySettingsUpdate) -> dict[str, Any]: """Persist deployment configuration updates.""" current = _load() updates = body.model_dump(exclude_none=True) current.update(updates) try: _save(current) except Exception as exc: raise HTTPException(status_code=500, detail=f"Failed to persist settings: {exc}") from exc return {"success": True, "updated": list(updates.keys())} PYEOF echo "[CREATED] backend/api/routes/deploy_settings.py" echo echo "===== 3. REAL GITHUB SEARCH TOOL =====" cat > backend/tools/github_search.py << 'PYEOF' """ Real GitHub repository + code search via GitHub REST API. No auth required for public repos (60 req/hr). Set GH_TOKEN or GITHUB_TOKEN env var for 5000 req/hr. """ from __future__ import annotations import logging import os import time from typing import Any import httpx logger = logging.getLogger("dolor3v.tools.github_search") _GITHUB_API = "https://api.github.com" _HEADERS_BASE = { "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", "User-Agent": "DOLOR3V-TravelerDev/1.0", } def _auth_headers() -> dict[str, str]: token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN", "") if token: return {**_HEADERS_BASE, "Authorization": f"Bearer {token}"} return dict(_HEADERS_BASE) async def search_repositories( query: str, sort: str = "stars", order: str = "desc", per_page: int = 10, ) -> dict[str, Any]: """Search GitHub repositories. Returns real API data.""" url = f"{_GITHUB_API}/search/repositories" params = {"q": query, "sort": sort, "order": order, "per_page": per_page} t0 = time.monotonic() async with httpx.AsyncClient(timeout=15.0) as client: resp = await client.get(url, params=params, headers=_auth_headers()) latency_ms = round((time.monotonic() - t0) * 1000) if resp.status_code == 403: remaining = resp.headers.get("X-RateLimit-Remaining", "?") reset = resp.headers.get("X-RateLimit-Reset", "?") return { "error": "GitHub rate limit exceeded", "remaining": remaining, "reset_at": reset, "tip": "Set GH_TOKEN or GITHUB_TOKEN env var for 5000 req/hr", } if resp.status_code != 200: return { "error": f"GitHub API returned {resp.status_code}", "body": resp.text[:500], } data = resp.json() items = data.get("items", []) results = [] for item in items: results.append({ "name": item.get("full_name"), "description": item.get("description"), "stars": item.get("stargazers_count"), "forks": item.get("forks_count"), "language": item.get("language"), "url": item.get("html_url"), "clone_url": item.get("clone_url"), "topics": item.get("topics", []), "updated_at": item.get("updated_at"), "open_issues": item.get("open_issues_count"), }) return { "query": query, "total_count": data.get("total_count", 0), "returned": len(results), "latency_ms": latency_ms, "results": results, "rate_limit_remaining": resp.headers.get("X-RateLimit-Remaining", "unknown"), } async def search_code( query: str, per_page: int = 10, ) -> dict[str, Any]: """Search GitHub code. Requires GH_TOKEN for reliable access.""" url = f"{_GITHUB_API}/search/code" params = {"q": query, "per_page": per_page} token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN", "") if not token: return { "error": "Code search requires authentication", "tip": "Set GH_TOKEN env var", } t0 = time.monotonic() async with httpx.AsyncClient(timeout=15.0) as client: resp = await client.get(url, params=params, headers=_auth_headers()) latency_ms = round((time.monotonic() - t0) * 1000) if resp.status_code != 200: return {"error": f"GitHub code search returned {resp.status_code}", "body": resp.text[:500]} data = resp.json() items = data.get("items", []) return { "query": query, "total_count": data.get("total_count", 0), "latency_ms": latency_ms, "results": [ { "name": i.get("name"), "path": i.get("path"), "repo": i.get("repository", {}).get("full_name"), "url": i.get("html_url"), "sha": i.get("sha"), } for i in items ], } PYEOF echo "[CREATED] backend/tools/github_search.py" echo echo "===== 4. REAL ANDROID DEVELOPER DOCS LOOKUP =====" cat > backend/tools/android_docs.py << 'PYEOF' """ Real Android Developer documentation lookup. Fetches from developer.android.com search and reference pages. No authentication required. """ from __future__ import annotations import logging import re import time from typing import Any from urllib.parse import quote_plus import httpx logger = logging.getLogger("dolor3v.tools.android_docs") _BASE = "https://developer.android.com" _SEARCH_URL = f"{_BASE}/s/results" _HEADERS = { "User-Agent": ( "Mozilla/5.0 (Linux; Android 14; Pixel 8) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/124.0.0.0 Mobile Safari/537.36" ), "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", } # Curated fast-path: well-known Android API references _KNOWN_REFS: dict[str, str] = { "activity": "/reference/android/app/Activity", "fragment": "/reference/androidx/fragment/app/Fragment", "viewmodel": "/reference/androidx/lifecycle/ViewModel", "livedata": "/reference/androidx/lifecycle/LiveData", "room": "/reference/androidx/room/package-summary", "compose": "/jetpack/compose", "navigation": "/guide/navigation", "workmanager": "/reference/androidx/work/WorkManager", "coroutines": "/kotlin/coroutines", "hilt": "/training/dependency-injection/hilt-android", "retrofit": "https://square.github.io/retrofit/", "jetpack": "/jetpack", "manifest": "/guide/topics/manifest/manifest-intro", "permissions": "/guide/topics/permissions/overview", "intent": "/reference/android/content/Intent", "service": "/reference/android/app/Service", "broadcastreceiver": "/reference/android/content/BroadcastReceiver", "contentprovider": "/reference/android/content/ContentProvider", "recyclerview": "/reference/androidx/recyclerview/widget/RecyclerView", "constraintlayout": "/reference/androidx/constraintlayout/widget/ConstraintLayout", "gradle": "/build/releases/gradle-plugin", "apk": "/studio/build/build-variants", "aab": "/guide/app-bundle", "proguard": "/studio/build/shrink-code", "keystore": "/training/articles/keystore", } def _fast_path(query: str) -> str | None: q = query.lower().strip() for key, path in _KNOWN_REFS.items(): if key in q: return path if path.startswith("http") else f"{_BASE}{path}" return None async def lookup_android_docs( query: str, max_results: int = 5, ) -> dict[str, Any]: """ Fetch real Android Developer documentation. Tries fast-path known references first, then falls back to search. """ t0 = time.monotonic() # Fast path for known APIs fast_url = _fast_path(query) results = [] async with httpx.AsyncClient( timeout=20.0, follow_redirects=True, headers=_HEADERS, ) as client: if fast_url: try: resp = await client.get(fast_url) if resp.status_code == 200: # Extract title and description from HTML html = resp.text title_match = re.search(r"