Premiere version fonctionnelle, demarrable avec `docker compose up -d --build` sur http://localhost:8080. Architecture - Frontend React 18 / TypeScript strict / Vite / Tailwind, canvas @xyflow/react, etat Zustand, persistance IndexedDB via Dexie, auto-layout dagre. - Backend FastAPI / SQLAlchemy 2 / Pydantic v2 / SQLite, qui sert aussi les fichiers statiques du frontend compile. - Image Docker multi-stage, un seul service, bind mount ./data pour SQLite. Local-first Les cartes restent dans le navigateur. Le serveur ne recoit une carte que sur demande explicite de partage. Partage Deux secrets distincts : share_token public en lecture seule dans l'URL, et management_token conserve uniquement cote client, transmis en en-tete pour les modifications et stocke hashe en base. Fonctionnalites Tableau de bord, editeur complet (undo/redo, copier/coller, raccourcis, branches repliables, recherche), 9 themes, 4 dispositions automatiques, panneau de proprietes, import/export .mmap, export PNG/SVG/PDF, impression, page publique en lecture seule, PWA. Qualite 25 tests pytest et 43 tests vitest au vert, ruff et ESLint sans erreur, TypeScript strict sans erreur. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
77 lines
2.8 KiB
Python
77 lines
2.8 KiB
Python
"""Service des fichiers statiques du frontend (SPA) par FastAPI.
|
|
|
|
Le build Vite est copie dans `settings.static_dir`. Toute route qui n'est ni
|
|
/api/... ni un fichier existant retourne index.html, afin que le routeur
|
|
cote client puisse gerer /map/:id et /s/:token.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, FastAPI, Request
|
|
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_PLACEHOLDER = """<!doctype html>
|
|
<html lang="fr"><head><meta charset="utf-8"><title>Frontend non compile</title></head>
|
|
<body style="font-family:system-ui;padding:2rem;max-width:40rem;margin:auto">
|
|
<h1>Frontend non compile</h1>
|
|
<p>L'API est disponible sur <code>/api/health</code>.</p>
|
|
<p>En developpement, lancez <code>npm run dev</code> dans <code>frontend/</code>.</p>
|
|
</body></html>"""
|
|
|
|
# Fichiers servis a la racine et qui ne doivent pas etre mis en cache longtemps.
|
|
_NO_CACHE_FILES = {"index.html", "sw.js", "manifest.webmanifest"}
|
|
|
|
|
|
def _safe_resolve(static_dir: Path, relative_path: str) -> Path | None:
|
|
"""Empeche toute sortie du dossier statique (path traversal)."""
|
|
candidate = (static_dir / relative_path.lstrip("/")).resolve()
|
|
try:
|
|
candidate.relative_to(static_dir.resolve())
|
|
except ValueError:
|
|
return None
|
|
return candidate
|
|
|
|
|
|
def mount_frontend(app: FastAPI, static_dir_path: str) -> None:
|
|
static_dir = Path(static_dir_path)
|
|
router = APIRouter(include_in_schema=False)
|
|
|
|
if (static_dir / "assets").is_dir():
|
|
# Les assets Vite sont hashes : cache long sans risque.
|
|
app.mount(
|
|
"/assets",
|
|
StaticFiles(directory=static_dir / "assets"),
|
|
name="assets",
|
|
)
|
|
|
|
@router.api_route("/{full_path:path}", methods=["GET", "HEAD"])
|
|
def serve_spa(full_path: str, request: Request): # type: ignore[no-untyped-def]
|
|
if full_path.startswith("api/"):
|
|
return JSONResponse(status_code=404, content={"detail": "Ressource introuvable."})
|
|
|
|
if not static_dir.is_dir():
|
|
return HTMLResponse(_PLACEHOLDER, status_code=200)
|
|
|
|
if full_path:
|
|
candidate = _safe_resolve(static_dir, full_path)
|
|
if candidate is not None and candidate.is_file():
|
|
headers = (
|
|
{"Cache-Control": "no-cache"}
|
|
if candidate.name in _NO_CACHE_FILES
|
|
else {"Cache-Control": "public, max-age=3600"}
|
|
)
|
|
return FileResponse(candidate, headers=headers)
|
|
|
|
index = static_dir / "index.html"
|
|
if index.is_file():
|
|
return FileResponse(index, headers={"Cache-Control": "no-cache"})
|
|
return HTMLResponse(_PLACEHOLDER, status_code=200)
|
|
|
|
app.include_router(router)
|