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:
2026-09-08 22:19:18 +02:00
commit 0b2e5e326d
113 changed files with 14576 additions and 0 deletions

84
backend/tests/conftest.py Normal file
View File

@@ -0,0 +1,84 @@
"""Fixtures de test : base SQLite temporaire, isolee du volume de production."""
from __future__ import annotations
import os
import tempfile
from collections.abc import Iterator
import pytest
# Doit etre defini AVANT le premier import de l'application.
_TMP_DIR = tempfile.mkdtemp(prefix="openmind-tests-")
os.environ["DATABASE_URL"] = f"sqlite:///{_TMP_DIR}/test.db"
os.environ["STATIC_DIR"] = f"{_TMP_DIR}/static"
os.environ["APP_NAME"] = "OpenMind"
os.environ["MAX_NODES"] = "5000"
os.environ["MAX_MAP_SIZE_MB"] = "10"
from fastapi.testclient import TestClient # noqa: E402
from app.config import get_settings # noqa: E402
from app.db.base import Base # noqa: E402
from app.db.session import engine # noqa: E402
from app.main import app # noqa: E402
@pytest.fixture(autouse=True)
def _fresh_database() -> Iterator[None]:
get_settings.cache_clear()
Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine)
yield
get_settings.cache_clear()
@pytest.fixture()
def client() -> Iterator[TestClient]:
with TestClient(app) as test_client:
yield test_client
def build_map(node_count: int = 3, name: str = "Carte de test") -> dict:
"""Construit un document carte minimal mais valide."""
nodes = [
{
"id": "root",
"type": "root",
"text": name,
"position": {"x": 0, "y": 0},
"parentId": None,
}
]
edges = []
for index in range(1, node_count):
node_id = f"n{index}"
nodes.append(
{
"id": node_id,
"type": "standard",
"text": f"Noeud {index}",
"position": {"x": 200, "y": index * 80},
"parentId": "root",
}
)
edges.append({"id": f"e-root-{node_id}", "source": "root", "target": node_id})
return {
"version": 1,
"id": "11111111-1111-4111-8111-111111111111",
"name": name,
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z",
"nodes": nodes,
"edges": edges,
"viewport": {"x": 0, "y": 0, "zoom": 1},
"settings": {"theme": "default", "layout": "mindmap-right", "branchStyle": "bezier"},
}
@pytest.fixture()
def created_share(client: TestClient) -> dict:
response = client.post("/api/shares", json={"title": "Carte de test", "map": build_map()})
assert response.status_code == 201, response.text
return response.json()