7289b770e5
Self-host Leaflet locally, proxy map tiles through cockpit, set explicit map height, and invalidate size after init.
254 lines
8.8 KiB
Python
254 lines
8.8 KiB
Python
"""Export Intel Cockpit page + API proxy."""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any, Optional
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, Query, Request
|
|
from fastapi.responses import RedirectResponse, Response, StreamingResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
from pathlib import Path
|
|
|
|
router = APIRouter()
|
|
BASE = Path(__file__).resolve().parent.parent.parent
|
|
templates = Jinja2Templates(directory=str(BASE / "templates"))
|
|
TOOLS = os.getenv("TOOLS_API_URL", "http://tools-api:8700").rstrip("/")
|
|
|
|
|
|
async def _proxy(method: str, path: str, **kwargs) -> Any:
|
|
url = f"{TOOLS}/export-intel{path}"
|
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
r = await client.request(method, url, **kwargs)
|
|
if r.status_code >= 400:
|
|
return {"error": r.text, "status": r.status_code}
|
|
if "text/csv" in r.headers.get("content-type", ""):
|
|
return r
|
|
return r.json()
|
|
|
|
|
|
@router.get("/foodlinkk")
|
|
def foodlinkk_home():
|
|
return RedirectResponse(url="/export-intel", status_code=302)
|
|
|
|
|
|
@router.get("/export-intel")
|
|
def export_intel_page(request: Request):
|
|
return templates.TemplateResponse(
|
|
"export_intel.html",
|
|
{"request": request, "page_title": "Wereldexport"},
|
|
)
|
|
|
|
|
|
@router.get("/api/export-intel/stats")
|
|
async def api_stats(country: Optional[str] = None, region: Optional[str] = None):
|
|
params = {k: v for k, v in {"country": country, "region": region}.items() if v}
|
|
return await _proxy("GET", "/stats", params=params)
|
|
|
|
|
|
@router.get("/api/export-intel/regions")
|
|
async def api_regions():
|
|
return await _proxy("GET", "/regions")
|
|
|
|
|
|
@router.get("/api/export-intel/territories")
|
|
async def api_territories(region: Optional[str] = None):
|
|
params = {"region": region} if region else {}
|
|
return await _proxy("GET", "/territories", params=params)
|
|
|
|
|
|
@router.get("/api/export-intel/entities")
|
|
async def api_entities(
|
|
country: Optional[str] = None,
|
|
region: Optional[str] = None,
|
|
entity_type: Optional[str] = None,
|
|
entity_types: Optional[str] = None,
|
|
q: Optional[str] = None,
|
|
favorite_only: bool = False,
|
|
crm_linked: Optional[bool] = None,
|
|
limit: int = 200,
|
|
offset: int = 0,
|
|
):
|
|
params = {k: v for k, v in {
|
|
"country": country, "region": region, "entity_type": entity_type,
|
|
"entity_types": entity_types, "q": q, "limit": limit, "offset": offset,
|
|
"favorite_only": favorite_only, "crm_linked": crm_linked,
|
|
}.items() if v is not None and v is not False}
|
|
return await _proxy("GET", "/entities", params=params)
|
|
|
|
|
|
@router.get("/api/export-intel/entities/{entity_id}")
|
|
async def api_entity(entity_id: int):
|
|
return await _proxy("GET", f"/entities/{entity_id}")
|
|
|
|
|
|
@router.get("/api/export-intel/contacts")
|
|
async def api_contacts(
|
|
country: Optional[str] = None,
|
|
region: Optional[str] = None,
|
|
entity_type: Optional[str] = None,
|
|
has_email: Optional[bool] = None,
|
|
crm_linked: Optional[bool] = None,
|
|
q: Optional[str] = None,
|
|
limit: int = 200,
|
|
offset: int = 0,
|
|
):
|
|
params: dict[str, Any] = {"limit": limit, "offset": offset}
|
|
if country:
|
|
params["country"] = country
|
|
if region:
|
|
params["region"] = region
|
|
if entity_type:
|
|
params["entity_type"] = entity_type
|
|
if has_email is not None:
|
|
params["has_email"] = has_email
|
|
if crm_linked is not None:
|
|
params["crm_linked"] = crm_linked
|
|
if q:
|
|
params["q"] = q
|
|
return await _proxy("GET", "/contacts", params=params)
|
|
|
|
|
|
@router.get("/api/export-intel/contacts/export.csv")
|
|
async def api_contacts_export(
|
|
country: Optional[str] = None,
|
|
region: Optional[str] = None,
|
|
entity_type: Optional[str] = None,
|
|
has_email: Optional[bool] = None,
|
|
crm_linked: Optional[bool] = None,
|
|
q: Optional[str] = None,
|
|
):
|
|
params = {
|
|
k: v
|
|
for k, v in {
|
|
"country": country,
|
|
"region": region,
|
|
"entity_type": entity_type,
|
|
"has_email": has_email,
|
|
"crm_linked": crm_linked,
|
|
"q": q,
|
|
}.items()
|
|
if v is not None and v != ""
|
|
}
|
|
url = f"{TOOLS}/export-intel/contacts/export.csv"
|
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
r = await client.get(url, params=params)
|
|
return StreamingResponse(
|
|
iter([r.content]),
|
|
media_type="text/csv",
|
|
headers={"Content-Disposition": "attachment; filename=export-intel-contacts.csv"},
|
|
)
|
|
|
|
|
|
@router.get("/api/export-intel/caterers/brands")
|
|
async def api_caterer_brands():
|
|
return await _proxy("GET", "/caterers/brands")
|
|
|
|
|
|
@router.get("/api/export-intel/caterers/presence")
|
|
async def api_caterer_presence(country: Optional[str] = None):
|
|
params = {"country": country} if country else {}
|
|
return await _proxy("GET", "/caterers/presence", params=params)
|
|
|
|
|
|
@router.get("/api/export-intel/gov-sources")
|
|
async def api_gov_sources(country: Optional[str] = None):
|
|
params = {"country": country} if country else {}
|
|
return await _proxy("GET", "/gov-sources", params=params)
|
|
|
|
|
|
|
|
|
|
@router.get("/api/map/tiles/{z}/{x}/{y}.png")
|
|
async def map_tile_proxy(z: int, x: int, y: int) -> Response:
|
|
"""Proxy Carto dark basemap tiles via cockpit (no client internet needed)."""
|
|
host = ("a", "b", "c", "d")[(x + y) % 4]
|
|
url = f"https://{host}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png"
|
|
try:
|
|
async with httpx.AsyncClient(timeout=20.0, follow_redirects=True) as client:
|
|
r = await client.get(url)
|
|
if r.status_code != 200:
|
|
return Response(status_code=502, content=b"")
|
|
return Response(
|
|
content=r.content,
|
|
media_type="image/png",
|
|
headers={"Cache-Control": "public, max-age=604800"},
|
|
)
|
|
except Exception:
|
|
return Response(status_code=502, content=b"")
|
|
|
|
@router.get("/api/export-intel/map/bundle")
|
|
async def api_map_bundle(
|
|
country: Optional[str] = None,
|
|
region: Optional[str] = None,
|
|
entity_type: Optional[str] = None,
|
|
entity_types: Optional[str] = None,
|
|
q: Optional[str] = None,
|
|
halal_min: Optional[float] = None,
|
|
favorite_only: bool = False,
|
|
crm_linked: Optional[bool] = None,
|
|
):
|
|
params = {k: v for k, v in {
|
|
"country": country, "region": region, "entity_type": entity_type,
|
|
"entity_types": entity_types, "q": q, "halal_min": halal_min,
|
|
"favorite_only": favorite_only, "crm_linked": crm_linked,
|
|
}.items() if v is not None and v is not False}
|
|
return await _proxy("GET", "/map/bundle", params=params)
|
|
|
|
|
|
@router.post("/api/export-intel/entities/favorites")
|
|
async def api_set_favorites(request: Request):
|
|
body = await request.json()
|
|
return await _proxy("POST", "/entities/favorites", json=body)
|
|
|
|
|
|
@router.get("/api/export-intel/entities/favorites")
|
|
async def api_list_favorites(country: Optional[str] = None, region: Optional[str] = None, limit: int = 200):
|
|
params = {k: v for k, v in {"country": country, "region": region, "limit": limit}.items() if v is not None}
|
|
return await _proxy("GET", "/entities/favorites", params=params)
|
|
|
|
|
|
@router.post("/api/export-intel/crm/push")
|
|
async def api_crm_push(request: Request):
|
|
body = await request.json()
|
|
return await _proxy("POST", "/crm/push", json=body)
|
|
|
|
|
|
@router.get("/api/export-intel/crm/pipeline")
|
|
async def api_crm_pipeline(country: Optional[str] = None, region: Optional[str] = None, limit: int = 100):
|
|
params = {k: v for k, v in {"country": country, "region": region, "limit": limit}.items() if v is not None}
|
|
return await _proxy("GET", "/crm/pipeline", params=params)
|
|
|
|
|
|
@router.get("/api/export-intel/halal/markets")
|
|
async def api_halal_markets(
|
|
region: Optional[str] = None,
|
|
country: Optional[str] = None,
|
|
limit: int = 30,
|
|
):
|
|
params = {k: v for k, v in {"region": region, "country": country, "limit": limit}.items() if v is not None}
|
|
return await _proxy("GET", "/halal/markets", params=params)
|
|
|
|
|
|
@router.get("/api/export-intel/tenders")
|
|
async def api_tenders(country: Optional[str] = None, status: Optional[str] = "open", limit: int = 100):
|
|
params = {k: v for k, v in {"country": country, "status": status, "limit": limit}.items() if v is not None}
|
|
return await _proxy("GET", "/tenders", params=params)
|
|
|
|
|
|
@router.post("/api/export-intel/sync/{kind}")
|
|
async def api_sync(kind: str, request: Request):
|
|
body = {}
|
|
if request.headers.get("content-type", "").startswith("application/json"):
|
|
try:
|
|
body = await request.json()
|
|
except Exception:
|
|
body = {}
|
|
if kind not in ("contacts", "caterers", "distributors", "customers", "tenders", "all", "world", "region"):
|
|
return {"error": "unknown sync kind"}
|
|
timeout = 7200.0 if kind in ("world", "region", "all") else 600.0
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
url = f"{TOOLS}/export-intel/sync/{kind}"
|
|
r = await client.post(url, json=body)
|
|
return r.json()
|