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:
0
backend/app/db/__init__.py
Normal file
0
backend/app/db/__init__.py
Normal file
9
backend/app/db/base.py
Normal file
9
backend/app/db/base.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Base declarative SQLAlchemy commune a tous les modeles."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
"""Classe de base des modeles ORM."""
|
||||
26
backend/app/db/init_db.py
Normal file
26
backend/app/db/init_db.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Initialisation du schema SQLite.
|
||||
|
||||
La V1 n'utilise pas Alembic : le schema tient dans une seule table et
|
||||
`create_all` suffit. Toute evolution future du schema devra soit passer par
|
||||
Alembic, soit par une migration explicite ajoutee ici (voir README).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from sqlalchemy import Engine
|
||||
|
||||
from app.db.base import Base
|
||||
from app.db.session import engine as default_engine
|
||||
from app.models import share as _share_model # noqa: F401 (enregistre la table)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
|
||||
def init_db(engine: Engine | None = None) -> None:
|
||||
target = engine or default_engine
|
||||
Base.metadata.create_all(bind=target)
|
||||
logger.info("Schema SQLite initialise (version %s)", SCHEMA_VERSION)
|
||||
62
backend/app/db/session.py
Normal file
62
backend/app/db/session.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""Creation du moteur SQLAlchemy et de la session de requete."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import Engine, create_engine, event
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from app.config import get_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_SQLITE_FILE_PREFIX = "sqlite:///"
|
||||
|
||||
|
||||
def _ensure_sqlite_directory(database_url: str) -> None:
|
||||
"""Cree le dossier parent du fichier SQLite s'il n'existe pas encore."""
|
||||
if not database_url.startswith(_SQLITE_FILE_PREFIX):
|
||||
return
|
||||
raw_path = database_url[len(_SQLITE_FILE_PREFIX) :]
|
||||
if not raw_path or raw_path == ":memory:":
|
||||
return
|
||||
path = Path(raw_path)
|
||||
if path.parent and str(path.parent) not in ("", "."):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def create_db_engine(database_url: str | None = None) -> Engine:
|
||||
settings = get_settings()
|
||||
url = database_url or settings.database_url
|
||||
_ensure_sqlite_directory(url)
|
||||
|
||||
connect_args = {"check_same_thread": False} if url.startswith("sqlite") else {}
|
||||
engine = create_engine(url, connect_args=connect_args, future=True, echo=False)
|
||||
|
||||
if url.startswith("sqlite"):
|
||||
|
||||
@event.listens_for(engine, "connect")
|
||||
def _set_sqlite_pragma(dbapi_connection, _connection_record): # type: ignore[no-untyped-def]
|
||||
cursor = dbapi_connection.cursor()
|
||||
# WAL : meilleure resistance aux arrets brutaux du conteneur.
|
||||
cursor.execute("PRAGMA journal_mode=WAL")
|
||||
cursor.execute("PRAGMA foreign_keys=ON")
|
||||
cursor.close()
|
||||
|
||||
return engine
|
||||
|
||||
|
||||
engine: Engine = create_db_engine()
|
||||
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
|
||||
|
||||
|
||||
def get_db() -> Generator[Session, None, None]:
|
||||
"""Dependance FastAPI : ouvre une session et la referme systematiquement."""
|
||||
session = SessionLocal()
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
session.close()
|
||||
Reference in New Issue
Block a user