OpenMind v1 : cartes mentales libres, local-first et dockerisees
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>
This commit is contained in:
76
backend/app/web.py
Normal file
76
backend/app/web.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user