# ----------------------------------------------------------------------------- # Ora-Ka — Plateforme unifiée des agrégateurs Ka # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # web.py : app maîtresse — landing + montage des 5 apps sous leurs préfixes # ----------------------------------------------------------------------------- from __future__ import annotations import sqlite3 import sys import time from pathlib import Path from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from fastapi.middleware.gzip import GZipMiddleware ROOT = Path(__file__).resolve().parent.parent APPS_DIR = ROOT / "apps" # Chaque app garde son arborescence d'origine (/, data/, frontend/dist/) # sous apps// — on rend les 5 packages importables tels quels. for _d in ("immo", "lou", "fabri", "auto", "food"): p = str(APPS_DIR / _d) if p not in sys.path: sys.path.insert(0, p) from immoka.web import app as immo_app # noqa: E402 from louka.web import app as lou_app # noqa: E402 from fabrika.web import app as fabri_app # noqa: E402 from autoka.web import app as auto_app # noqa: E402 from foodka.web import app as food_app # noqa: E402 from oraka.auth import current_user, router as auth_router # noqa: E402 from oraka.hubfav import hub_list_all # noqa: E402 from oraka.landing import LANDING # noqa: E402 from oraka.legal import PRIVACY, TERMS # noqa: E402 from oraka.search import search_hits # noqa: E402 app = FastAPI(title="Ora-Ka", version="1.0", docs_url=None, redoc_url=None) app.add_middleware(GZipMiddleware, minimum_size=1024) # Connexion « KA ID » (SSO du Groupe KA) — inclus AVANT les montages /immo etc. # pour que /api/auth/* soit toujours servi par l'app racine. app.include_router(auth_router) # --------------------------------------------------------------------------- # Statistiques agrégées (lecture directe des 5 bases, cache mémoire 5 min) # --------------------------------------------------------------------------- _DBS = { "immo": APPS_DIR / "immo" / "data" / "immoka.db", "lou": APPS_DIR / "lou" / "data" / "louka.db", "fabri": APPS_DIR / "fabri" / "data" / "fabrika.db", "auto": APPS_DIR / "auto" / "data" / "autoka.db", "food": APPS_DIR / "food" / "data" / "foodka.db", } _COUNT_SQL = { "immo": "SELECT COUNT(*) FROM listings WHERE active=1 AND dup_hidden=0 AND price IS NOT NULL", "lou": "SELECT COUNT(*) FROM listings WHERE active=1", "fabri": "SELECT COUNT(*) FROM products WHERE active=1", "auto": "SELECT COUNT(*) FROM vehicles WHERE active=1", "food": "SELECT COUNT(*) FROM products WHERE active=1", } _overview_cache: dict = {"ts": 0.0, "data": None} def _count(db_path: Path, sql: str) -> int: if not db_path.exists(): return 0 try: con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=5) try: return int(con.execute(sql).fetchone()[0]) finally: con.close() except Exception: return 0 @app.get("/api/overview") def overview() -> JSONResponse: now = time.time() if _overview_cache["data"] is not None and now - _overview_cache["ts"] < 300: return JSONResponse(_overview_cache["data"]) counts = {k: _count(_DBS[k], _COUNT_SQL[k]) for k in _DBS} data = { "apps": { "immo": {"label": "propriétés à vendre", "n": counts["immo"]}, "lou": {"label": "logements à louer", "n": counts["lou"]}, "fabri": {"label": "produits québécois", "n": counts["fabri"]}, "auto": {"label": "véhicules d'occasion", "n": counts["auto"]}, "food": {"label": "produits d'épicerie", "n": counts["food"]}, }, "total": sum(counts.values()), "generated_at": now, } _overview_cache.update(ts=now, data=data) return JSONResponse(data) @app.get("/api/search") def api_search(q: str = "", scope: str | None = None, limit: int = 60) -> JSONResponse: """Moteur hybride : exact + sémantique dans les 5 univers en une requête.""" limit = max(1, min(int(limit), 120)) return JSONResponse(search_hits(q, scope, limit)) @app.get("/api/favorites") def api_favorites(request: Request) -> JSONResponse: """« Mon univers Ka » : les favoris du membre connecté, TOUTES plateformes du groupe confondues, lus depuis le magasin central du hub Groupe KA.""" user = current_user(request) if not user: return JSONResponse({"error": "non connecté"}, status_code=401) return JSONResponse({"favorites": hub_list_all(user.get("ka_id"))}) @app.get("/healthz") def healthz() -> dict: return {"status": "ok", "apps": {k: _DBS[k].exists() for k in _DBS}} # Redirections sans barre oblique finale vers la racine de chaque app for _alias in ("immo", "lou", "fabri", "auto", "food"): def _mk(alias: str): def _redir() -> RedirectResponse: return RedirectResponse(url=f"/{alias}/", status_code=308) return _redir app.get(f"/{_alias}", include_in_schema=False)(_mk(_alias)) @app.get("/", response_class=HTMLResponse, include_in_schema=False) def landing() -> HTMLResponse: return HTMLResponse(LANDING) @app.get("/conditions", response_class=HTMLResponse, include_in_schema=False) def terms() -> HTMLResponse: return HTMLResponse(TERMS) @app.get("/confidentialite", response_class=HTMLResponse, include_in_schema=False) def privacy() -> HTMLResponse: return HTMLResponse(PRIVACY) # --------------------------------------------------------------------------- # Montage des 5 apps — chacune reproduite fidèlement sous son préfixe. # Leurs catch-all SPA restent confinés à leur préfixe grâce au mount. # --------------------------------------------------------------------------- app.mount("/immo", immo_app) app.mount("/lou", lou_app) app.mount("/fabri", fabri_app) app.mount("/auto", auto_app) app.mount("/food", food_app)