Files
openmind/backend/app/web.py
snoopy 0b2e5e326d 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>
2026-09-08 22:19:18 +02:00

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)