snoopy af6fd519c5 Phase 1.1 : mise en page et edition visuelle des noeuds
Evolution incrementale : aucun changement d'architecture, aucune fonctionnalite
retiree, compatibilite .mmap v1 et IndexedDB preservee.

Modele (format v2)
- MindMapNode gagne `layout`, `iconStyle` et `branch`, tous optionnels.
- NodeStyle gagne fontFamily, verticalAlign, lineHeight et shadow.
- MapSettings gagne layoutSpacing, snapToGrid, snapGrid et alignmentGuides.
- Un champ absent signifie "herite du theme" : rien n'est ecrit dans le
  document pour materialiser un defaut, donc les cartes deja stockees restent
  lisibles telles quelles. Migration v1 -> v2 : style.width devient
  layout.width, toujours lu en repli par le resolveur.

Mise en page
- Nouveau resolveur de style qui fusionne theme et personnalisations.
- Taille automatique fidele au contenu, taille personnalisee bornee,
  redimensionnement a la souris via NodeResizer.
- Padding horizontal et vertical, retour a la ligne, largeur maximale du texte,
  interligne, police, alignement horizontal et vertical, ombre.
- Icones : emojis conserves, bibliotheque Lucide ajoutee, position, taille et
  espacement configurables.

Multi-selection
- Barre contextuelle d'alignement (6 axes) et de distribution (2 axes).
- Application d'un reglage a toute la selection, avec etat "Mixte".

Style
- Copier / coller / reinitialiser le style, au clavier et par clic droit.

Auto-layout
- Espacement parent/enfant et entre branches configurable, presets Compact,
  Normal et Aere. Le layout reste declenche uniquement a la demande.

Historique
- Une seule entree par redimensionnement complet, par alignement, par
  distribution, par application multiple et par collage de style.

Panneau Proprietes
- Reorganise en cinq sections repliables dont l'etat est conserve pendant la
  session. Vraies icones d'alignement, tooltips et aria-labels partout.

Corrections
- Largeur des noeuds : word-break et overflow-wrap:anywhere ramenaient la
  largeur min-content a un caractere, ce qui faisait s'effondrer tous les
  noeuds automatiques.
- Largeur auto en max-content bornee, pour qu'une phrase longue occupe la
  largeur maximale au lieu de former une colonne etroite.

Tests
- 85 tests vitest (43 -> 85) et 31 tests pytest (25 -> 31).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 17:29:48 +02:00

OpenMind

Cartes mentales libres, local-first, sans compte et sans abonnement.

OpenMind est une alternative gratuite et open source aux services de mind mapping qui placent l'export, les themes ou le nombre de cartes derriere un abonnement. Toutes les fonctionnalites sont disponibles, sans exception et sans compte.

  • Aucune limitation artificielle, aucun paiement, aucun "premium".
  • Aucun compte obligatoire : il n'y a d'ailleurs aucun systeme de comptes.
  • Aucune telemetrie, aucun tracker, aucune publicite.
  • Vos cartes vous appartiennent : elles restent dans votre navigateur.
  • Export PDF, PNG, SVG et impression : gratuits.
  • Un partage par lien, uniquement si vous le demandez explicitement.
  • Un seul conteneur Docker a demarrer.

Philosophie local-first

Une carte que vous creez n'est jamais envoyee au serveur.

Elle est enregistree dans l'IndexedDB de votre navigateur, automatiquement, apres une courte periode d'inactivite. Le backend n'existe que pour une seule chose : generer un lien de partage lorsque vous cliquez explicitement sur Partager.

Consequences concretes :

  • Fermer le navigateur, redemarrer le PC ou redemarrer le conteneur ne fait perdre aucune carte locale.
  • Une fois l'application chargee une premiere fois, elle continue de fonctionner sans reseau (PWA) : creation, edition, export, impression.
  • Vider les donnees du site, ou changer de navigateur/machine, fait disparaitre les cartes locales. Exportez vos cartes en .mmap pour en conserver une sauvegarde portable.

Demarrage rapide

Prerequis : Docker et Docker Compose.

git clone <url-du-depot>
cd Mind-Mapping
cp .env.example .env
docker compose up -d --build

Puis ouvrez :

http://localhost:8080

Verifier que le service repond :

curl http://localhost:8080/api/health

Reponse attendue : {"status":"ok"}

Etat du conteneur (le healthcheck Docker interroge /api/health) :

docker compose ps

Arreter :

docker compose down

Architecture

Un seul service applicatif. FastAPI sert a la fois l'API et les fichiers statiques du frontend compile.

Navigateur
   |
   +-- React / TypeScript (Vite, Tailwind, React Flow, Zustand)
   |
   +-- IndexedDB (Dexie)
   |      +-- cartes privees/locales  <-- ne quittent jamais le navigateur
   |
   +-- API, uniquement pour les fonctions serveur (partage)
            |
            v
        FastAPI  (+ fichiers statiques du frontend)
            |
            v
          SQLite
            |
            +-- cartes explicitement partagees

Choix structurants :

Sujet Choix Pourquoi
Hierarchie parentId fait autorite, les liens en sont derives un seul invariant a maintenir, pas de desynchronisation
Modele types metier propres, adaptes vers React Flow a l'affichage le format de fichier ne depend pas de la bibliotheque de rendu
Historique instantanes {nodes, edges}, groupes par action un glisser-deposer = une seule annulation
Auto-layout dagre, declenche uniquement a la demande les deplacements manuels ne sont jamais ecrases
Versionnement champ version + point d'extension de migration le format peut evoluer sans casser les fichiers existants

Variables d'environnement

Toutes definies dans .env (modele : .env.example). Aucun secret n'est requis.

Variable Defaut Role
APP_NAME OpenMind Nom du produit, affiche dans l'interface et l'onglet
APP_PORT 8080 Port publie sur l'hote (le conteneur ecoute toujours 8080)
DATABASE_URL sqlite:////app/data/openmind.db Base des cartes partagees
MAX_MAP_SIZE_MB 10 Taille maximale d'une carte envoyee au serveur
MAX_NODES 5000 Nombre maximal de noeuds par carte
CORS_ORIGINS vide Origines externes autorisees, separees par des virgules
LOG_LEVEL INFO DEBUG, INFO, WARNING, ERROR

Le nom du produit est centralise : cote backend via APP_NAME, cote frontend dans frontend/src/config/app.ts. Le frontend recupere le nom a l'execution via GET /api/config : changer APP_NAME suffit, sans recompiler.


Donnees et sauvegarde

Deux stockages totalement distincts :

Donnee Ou Sauvegarde
Cartes privees IndexedDB du navigateur Export .mmap depuis le tableau de bord ou l'editeur
Cartes partagees SQLite, dans ./data/openmind.db Copie du dossier ./data

Le dossier ./data est monte en bind mount (aucun volume Docker nomme) : les fichiers sont donc directement lisibles depuis l'hote.

docker compose stop
tar czf openmind-partages-$(date +%F).tar.gz data/
docker compose start

SQLite fonctionne en mode WAL : openmind.db, openmind.db-wal et openmind.db-shm doivent etre sauvegardes ensemble.

Redemarrer le serveur n'a aucun impact sur les cartes privees locales.


Fonctionnement du partage

Une carte n'est envoyee au serveur que lorsque vous cliquez sur Partager, apres une explication et une confirmation.

Deux secrets distincts sont generes cote serveur avec le module Python secrets :

Jeton Entropie Ou il vit A quoi il sert
share_token secrets.token_urlsafe(12), environ 96 bits dans l'URL publique /s/<token> lecture seule
management_token secrets.token_urlsafe(32), environ 256 bits uniquement dans l'IndexedDB du proprietaire modification et suppression

Regles appliquees :

  • Le management_token n'apparait jamais dans une URL, jamais dans un export .mmap, jamais dans les logs.
  • Il est transmis dans l'en-tete X-Management-Token pour PUT et DELETE.
  • Seul son hash SHA-256 est stocke en base ; la comparaison est faite en temps constant avec secrets.compare_digest.
  • Connaitre le share_token ne permet donc ni de modifier ni de supprimer un partage.
  • Le lien est non repertorie : il n'apparait dans aucune liste publique, mais toute personne qui le connait peut consulter la carte.

Cycle de vie :

  1. Partager : un instantane de la carte est copie sur le serveur, un lien est renvoye, par exemple http://localhost:8080/s/a8F3xP9k.
  2. Mettre a jour le partage : remplace l'instantane par la version actuelle. Tant que vous ne le faites pas, le lien public montre l'ancienne version.
  3. Supprimer le partage : efface l'instantane serveur. Le lien affiche alors une page propre indiquant que le partage n'existe plus. La carte locale n'est pas affectee.

Si vous supprimez une carte locale, sa reference de partage locale part avec elle : supprimez d'abord le partage si vous voulez aussi retirer la copie serveur.

API

Methode Route Auth Role
GET /api/health - Etat du service
GET /api/config - Nom de l'application et limites
POST /api/shares - Creer un partage
GET /api/shares/{share_token} - Lire une carte partagee
PUT /api/shares/{share_token} X-Management-Token Mettre a jour
DELETE /api/shares/{share_token} X-Management-Token Supprimer

Documentation interactive : http://localhost:8080/api/docs.


Fonctionnalites

Tableau de bord : creation, import, recherche, date de derniere modification, renommage, duplication, export, suppression avec confirmation. Une carte d'exemple est creee au tout premier lancement.

Editeur : canvas infini, zoom, deplacement, zoom-to-fit, minimap, grille optionnelle, creation/suppression/deplacement de noeuds, selection multiple, connexion entre noeuds avec detection de boucle, edition du texte au double-clic, undo/redo, copier/coller, duplication, sauvegarde automatique.

Mind mapping : noeud racine et noeuds standard, branches droites, courbes ou Bezier, branches repliables avec compteur d'elements masques.

Auto-layout, uniquement sur demande : mind map droite, mind map gauche, arbre horizontal, arbre vertical. L'espacement est configurable (distance parent/enfant et espacement entre branches), avec les presets Compact, Normal et Aere. Modifier l'espacement ne deplace rien tant que l'organisation n'est pas relancee.

Proprietes du noeud, regroupees en sections repliables (Contenu, Typographie, Apparence, Mise en page, Branche) : texte, icone, URL, notes ; police, taille, gras, italique, alignement horizontal et vertical, interligne ; couleurs de fond/texte/bordure, forme (rectangle, arrondi, capsule, cercle), ombre ; taille automatique ou personnalisee, largeur, hauteur, padding horizontal et vertical, retour a la ligne, largeur maximale du texte ; style, epaisseur et couleur de la branche. Un reglage s'applique a toute la selection, avec un etat Mixte quand les valeurs different.

Mise en page visuelle : redimensionnement des noeuds a la souris (une seule annulation par redimensionnement), taille automatique qui epouse le contenu, alignement et distribution d'une multi-selection, aimantation sur la grille, guides d'alignement pendant le deplacement, copier / coller / reinitialiser le style (Ctrl+Maj+C, Ctrl+Maj+V), menu contextuel par clic droit.

Bibliotheque d'icones : emojis et icones integrees (Lucide), avec position (gauche, droite, au-dessus), taille et espacement configurables.

Themes : Default, Minimal, Dark, Midnight, Ocean, Forest, Pastel, Monochrome, Blueprint. Plus un mode clair/sombre pour l'interface. Changer de theme ne supprime jamais une personnalisation explicite d'un noeud.

Recherche dans la carte : resultats listes, navigation entre eux, centrage et mise en evidence temporaire du noeud, depliage automatique des branches repliees.

Exports : .mmap (format natif), PNG (x1, x2, fond transparent), SVG, PDF (A4/A3, portrait/paysage, marges, adapter a une page ou taille reelle, fond, titre, date), impression via @media print.

PWA : installable, et utilisable hors ligne pour les cartes locales une fois l'application chargee.


Raccourcis clavier

Cmd remplace Ctrl sur macOS.

Raccourci Action
Tab Ajouter un noeud enfant
Entree Ajouter un noeud frere
Suppr / Retour arriere Supprimer la selection, hors saisie de texte
F2 Editer le texte du noeud selectionne
Double-clic Editer le texte du noeud
Ctrl+Z Annuler
Ctrl+Y ou Ctrl+Shift+Z Retablir
Ctrl+C Copier
Ctrl+V Coller
Ctrl+D Dupliquer
Ctrl+Maj+C Copier le style du noeud
Ctrl+Maj+V Coller le style sur la selection
Ctrl+A Tout selectionner
Ctrl+S Sauvegarde explicite
Ctrl+F Rechercher dans la carte
F Adapter la carte a l'ecran
Echap Quitter l'edition ou deselectionner

Format de fichier .mmap

Un .mmap est un fichier JSON UTF-8 contenant le document complet de la carte. Il ne contient jamais de jeton de partage.

Le format est en version 2. Les fichiers v1 restent lisibles : tous les champs ajoutes en v2 (layout, iconStyle, branch, reglages d'espacement) sont optionnels et un champ absent signifie simplement herite du theme. La seule transformation appliquee a l'import d'un fichier v1 est le deplacement de style.width vers layout.width.

{
  "version": 1,
  "id": "uuid",
  "name": "Architecture Homelab",
  "createdAt": "2026-01-01T10:00:00.000Z",
  "updatedAt": "2026-01-01T12:30:00.000Z",
  "nodes": [
    {
      "id": "uuid",
      "type": "root",
      "text": "Infrastructure",
      "position": { "x": 0, "y": 0 },
      "parentId": null,
      "collapsed": false,
      "icon": "\ud83d\udda5\ufe0f",
      "url": "https://exemple.fr",
      "note": "Texte libre",
      "style": {
        "backgroundColor": "#eef2ff",
        "shape": "rounded",
        "textAlign": "center",
        "verticalAlign": "middle",
        "lineHeight": 1.35,
        "fontFamily": "sans",
        "shadow": "soft"
      },
      "layout": {
        "widthMode": "fixed",
        "width": 240,
        "heightMode": "auto",
        "paddingX": 14,
        "paddingY": 8,
        "wrap": true,
        "maxTextWidth": 260
      },
      "iconStyle": { "kind": "emoji", "position": "left", "size": 16, "gap": 8 },
      "branch": { "style": "bezier", "width": 2, "color": "#94a3b8" }
    }
  ],
  "edges": [{ "id": "e-a-b", "source": "a", "target": "b" }],
  "viewport": { "x": 0, "y": 0, "zoom": 1 },
  "settings": {
    "theme": "default",
    "layout": "mindmap-right",
    "branchStyle": "bezier",
    "showGrid": true,
    "layoutSpacing": { "parentGap": 100, "siblingGap": 50 },
    "snapToGrid": false,
    "snapGrid": 20,
    "alignmentGuides": true
  }
}

A l'import, le fichier est valide et normalise : structure, version, champs essentiels, limite de noeuds, assainissement des URL (javascript: et data: sont supprimes), rejet des styles invalides. Le contenu n'est jamais execute ni interprete comme du HTML ; un fichier invalide produit un message clair.

Le champ version permet les migrations futures : le point d'extension se trouve dans frontend/src/services/mapSerialization.ts, fonction migrate.


Developpement

Backend

cd backend
python -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt
DATABASE_URL=sqlite:///./openmind-dev.db STATIC_DIR=./static uvicorn app.main:app --reload --port 8080

Sous Windows, activez l'environnement avec .venv\Scripts\activate.

Frontend

cd frontend
npm install
npm run dev

Le serveur Vite ecoute sur http://localhost:5173 et proxyfie /api vers http://localhost:8080.

Autres commandes :

npm run build
npm run lint
npm run typecheck
npm run test

Regenerer les icones de la PWA :

python scripts/generate_icons.py

Tests et qualite

Backend :

cd backend
python -m pytest -q
ruff check .
ruff format --check .

Frontend :

cd frontend
npm run test
npm run lint
npm run typecheck

Sans installation locale de Node ou Python, tout peut tourner via Docker :

docker run --rm -v "$PWD/backend:/work" -w /work python:3.12-slim sh -c "pip install -q -r requirements-dev.txt && ruff check . && python -m pytest -q"
docker run --rm -v "$PWD/frontend:/app" -w /app node:20-alpine sh -c "npm ci && npm run lint && npm run test && npm run build"

Les tests backend couvrent : healthcheck, creation, lecture, mise a jour et suppression d'un partage, refus avec un mauvais jeton de gestion, refus sans jeton, partage inexistant, payload invalide, limites de taille et de noeuds, non-divulgation du jeton de gestion, repli SPA.

Les tests frontend couvrent : stockage IndexedDB, serialisation et validation d'import, migrations, store Zustand (creation, suppression de branche, undo/redo, groupement du glisser-deposer, copier/coller, duplication, detection de boucle, recherche, auto-layout, lecture seule), assainissement des URL.


Structure du projet

.
├── Dockerfile               # build multi-stage : node -> python -> image finale
├── docker-compose.yml       # un seul service, bind mount ./data
├── .env.example
├── README.md
├── scripts/
│   └── generate_icons.py    # generation des icones PWA
├── data/                    # bind mount : SQLite des partages
├── backend/
│   ├── requirements.txt
│   ├── pyproject.toml       # configuration ruff et pytest
│   ├── app/
│   │   ├── main.py          # assemblage uniquement
│   │   ├── config.py
│   │   ├── logging_config.py
│   │   ├── middleware.py    # en-tetes de securite, limite de taille
│   │   ├── web.py           # service des fichiers statiques du frontend
│   │   ├── api/             # routes health, config, shares
│   │   ├── db/              # moteur, session, initialisation du schema
│   │   ├── models/          # SharedMap (SQLAlchemy)
│   │   ├── schemas/         # validation Pydantic
│   │   └── services/        # jetons, logique de partage, erreurs metier
│   └── tests/
└── frontend/
    ├── index.html
    ├── public/              # manifest PWA, service worker, icones
    └── src/
        ├── config/app.ts    # nom du produit et constantes
        ├── types/map.ts     # modele metier versionne
        ├── db/              # schema Dexie
        ├── services/        # depots, serialisation, client API
        ├── stores/          # Zustand : editeur, interface, configuration
        ├── hooks/           # routeur minimal, debounce
        ├── components/      # briques d'interface reutilisables
        ├── features/
        │   ├── editor/      # canvas, noeud, barre d'outils, panneaux, layout, themes
        │   ├── maps/        # tableau de bord
        │   ├── export/      # PNG, SVG, PDF, impression
        │   └── sharing/     # dialogue de partage, page publique
        ├── utils/
        └── styles/

Securite

  • Le texte d'un noeud est toujours traite comme du texte. Aucun dangerouslySetInnerHTML, aucun HTML issu d'une carte n'est interprete.
  • Les URL sont assainies : seuls http, https et mailto sont acceptes. Les liens s'ouvrent avec rel="noopener noreferrer nofollow".
  • Validation Pydantic de tout ce qui entre dans l'API, avec limites de taille et de nombre de noeuds ; un depassement produit un message clair, jamais un plantage.
  • En-tetes de securite : CSP stricte, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, Cross-Origin-Opener-Policy.
  • CORS restrictif : aucune origine externe par defaut.
  • Acces SQL exclusivement via l'ORM SQLAlchemy.
  • Jetons cryptographiquement aleatoires, jeton de gestion stocke hashe et jamais journalise.
  • Le conteneur tourne avec un utilisateur non-root.
  • Aucun secret dans le frontend.

Limitations connues

  • Export SVG : les noeuds sont encapsules dans un foreignObject (HTML embarque). Le fichier reste vectoriel et se redimensionne sans perte, les branches sont de vrais chemins SVG, mais certains editeurs vectoriels n'exploitent pas le contenu HTML des noeuds. Preferez PNG ou PDF pour un usage bureautique.
  • Exports image et PDF en arriere-plan : la generation s'appuie sur une frame d'animation du navigateur, qui n'est pas produite lorsque l'onglet est masque. Gardez l'onglet au premier plan pendant un export ; sinon l'operation est interrompue au bout de 45 secondes avec un message explicite.
  • PDF en taille reelle : si la carte depasse la page choisie, elle est reduite pour tenir entierement plutot que d'etre coupee ; un message le signale.
  • Gestion d'un partage : le jeton de gestion vit dans l'IndexedDB du navigateur d'origine. Depuis un autre appareil, un partage ne peut etre ni mis a jour ni supprime.
  • Dispositions reservees : mindmap-both, organization et freeform existent dans le modele de donnees mais ne sont pas proposees dans l'interface.
  • Types de noeuds reserves : note, image, group, link et task sont acceptes par le format, mais la V1 ne produit que root et standard.
  • Migrations de schema SQLite : la V1 initialise le schema avec create_all, une seule table. Alembic n'est pas utilise ; il faudra l'introduire au premier changement de schema.
  • Guides d'alignement : ils apparaissent pendant un deplacement lorsqu'un bord ou un centre coincide (a 6 px pres) avec celui d'un autre noeud. Ils sont purement visuels : ils n'aimantent pas le noeud. Pour aimanter, activez Aimanter sur la grille.
  • Alignement vertical : il n'a d'effet visible que si le noeud est plus haut que son texte, c'est-a-dire avec une hauteur fixe ou un padding vertical important.
  • Mobile : consultation et edition basique fonctionnent, mais l'interface est concue pour desktop et tablette.
  • Aucun compte utilisateur : c'est voulu. Pas de connexion, pas de collaboration temps reel, pas de synchronisation cloud.

Licences des dependances

Toutes les dependances sont sous licence libre permissive, compatible avec une application publique et gratuite. Aucune bibliotheque a licence commerciale n'est utilisee.

Dependance Licence
React, React DOM MIT
@xyflow/react (React Flow) MIT
Zustand MIT
Dexie Apache-2.0
dagre MIT
html-to-image MIT
jsPDF MIT
lucide-react ISC
Vite, Tailwind CSS, TypeScript, ESLint, Vitest MIT
FastAPI, Starlette, Pydantic, SQLAlchemy, Uvicorn MIT / BSD

Licence

OpenMind est distribue sous licence MIT : voir le fichier LICENSE.

Description
Cartes mentales libres, local-first, sans compte et sans abonnement. React + FastAPI + SQLite, un seul conteneur Docker.
Readme MIT 524 KiB
Languages
TypeScript 77.5%
Python 21%
JavaScript 0.5%
CSS 0.5%
Dockerfile 0.3%