# Acadenice Patches

Liste des patches custom appliques sur le fork Acadenice de Docmost.
Ce document est maintenu manuellement pour faciliter le rebase upstream.

Repo upstream : `github.com/docmost/docmost`
Branche fork : `acadenice/main`

## Conventions

- Chaque patch est commit isole avec scope `feat(rebrand)` / `feat(custom)` / etc.
- Les modifications in-line de fichiers upstream sont documentees ici avec rationale.
- Les nouveaux fichiers (extensions Tiptap custom, hooks, etc.) vont dans des emplacements dedies pour minimiser les conflits de rebase.

---

## Patch 015 — R3.7 @user mentions + notifications in-app + email

**Date** : 2026-05-08
**Scope** : mention detection bridge (REST API path), Acadenice notification facade API, /notifications page, /settings/notifications preferences page, i18n FR+EN

### Architecture Decision

The native Docmost system already provides the complete mention notification pipeline:
- `persistence.extension.ts` (collab path) extracts `entityType:"user"` mention nodes via `extractUserMentions`, queues `PAGE_MENTION_NOTIFICATION` jobs.
- `PageNotificationService.processPageMention()` handles RBAC checks (space + page access), deduplication (old vs new mentions), `notification.create()` + email send.
- `NotificationPopover` + bell icon are already in the header.
- Notification preferences are stored in `users.settings.notifications` (native `NotificationPref` component in `/settings/account/preferences`).

R3.7 adds:
1. `MentionDetectorService` — pure service that walks Tiptap JSON and extracts user mentions (no DB). Used by the emitter and independently testable.
2. `NotificationEmitterService` — listens to `acadenice.page.content.updated` (REST API save path, not collab) and queues `PAGE_MENTION_NOTIFICATION`. Bridges the gap between the collab-only native detection and pages saved via REST (templates instantiate, import, etc.).
3. `AcadeniceNotificationsController` — facade over native `NotificationService`, prefix `/api/v1/notifications`.
4. `NotificationPreferencesController` — GET/PUT `/api/v1/notification-preferences` (reads/writes native `users.settings.notifications`).
5. Frontend `/notifications` page — full inbox using native `NotificationItem` component.
6. Frontend `/settings/notifications` preferences page — dedicated toggles via Acadenice API.

**No new DB migration** — native `notifications` table covers `page.user_mention` fully.
**Poll 30s** for unread count (vs SSE bridge) — sufficient for notification freshness.

### Nouveaux fichiers backend

| Fichier | Role |
|---------|------|
| `apps/server/src/core/acadenice/notifications/notifications.module.ts` | NestJS module |
| `apps/server/src/core/acadenice/notifications/dto/notification.dto.ts` | Zod schemas |
| `apps/server/src/core/acadenice/notifications/services/mention-detector.service.ts` | Tiptap mention walker (pure) |
| `apps/server/src/core/acadenice/notifications/services/notification-emitter.service.ts` | Event listener -> PAGE_MENTION_NOTIFICATION queue |
| `apps/server/src/core/acadenice/notifications/services/notification-preferences.service.ts` | Read/write users.settings.notifications |
| `apps/server/src/core/acadenice/notifications/controllers/notifications.controller.ts` | REST facade /api/v1/notifications |
| `apps/server/src/core/acadenice/notifications/controllers/notification-preferences.controller.ts` | REST /api/v1/notification-preferences |
| `apps/server/src/core/acadenice/notifications/spec/mention-detector.service.spec.ts` | 18 unit tests |
| `apps/server/src/core/acadenice/notifications/spec/notifications.controller.spec.ts` | 10 unit tests |
| `apps/server/src/core/acadenice/notifications/spec/notification-preferences.spec.ts` | 4 unit tests |

### Nouveaux fichiers frontend

| Fichier | Role |
|---------|------|
| `apps/client/src/features/acadenice/notifications/services/notifications-client.ts` | Axios HTTP client |
| `apps/client/src/features/acadenice/notifications/queries/notifications-query.ts` | React Query hooks + 30s poll |
| `apps/client/src/features/acadenice/notifications/pages/notifications-page.tsx` | Route /notifications |
| `apps/client/src/features/acadenice/notifications/pages/notification-preferences-page.tsx` | Route /settings/notifications |
| `apps/client/src/features/acadenice/notifications/__tests__/notifications-client.test.ts` | 9 client tests |
| `apps/client/src/features/acadenice/notifications/__tests__/notifications-page.test.tsx` | 4 page render tests |

### Fichiers upstream modifies (patches)

| Fichier | Modification |
|---------|-------------|
| `apps/server/src/core/core.module.ts` | +AcadeniceNotificationsModule import + registration |
| `apps/client/src/App.tsx` | +import AcadeniceNotificationsPage, NotificationPreferencesPage + routes /notifications, /settings/notifications |
| `apps/client/src/components/settings/settings-sidebar.tsx` | +entree "Notifications" (IconBell) dans group Account |
| `apps/client/public/locales/en-US/translation.json` | +20 cles acadenice.notifications.* |
| `apps/client/public/locales/fr-FR/translation.json` | +20 cles acadenice.notifications.* |

### Endpoints

```
GET  /api/v1/notifications               paginated list
GET  /api/v1/notifications/unread-count  unread badge count (polled 30s)
POST /api/v1/notifications/read-all      mark all read
POST /api/v1/notifications/mark-read     bulk mark read
POST /api/v1/notifications/:id/read      single mark read
GET  /api/v1/notification-preferences    get prefs
PUT  /api/v1/notification-preferences    update prefs
```

### Tests

- Backend : 18 tests MentionDetectorService + 10 tests controller + 4 tests preferences = 32 tests
- Frontend : 9 tests client + 4 tests page render = 13 tests
- Total R3.7 : 45 tests

### Points a debattre avec Corentin

1. **REST path mention dedup** : `NotificationEmitterService` passe `oldMentionedUserIds: []` — toutes les mentions existantes sont reenvoyees a chaque save REST. La native `processPageMention` filtre les self-mentions mais pas les doubles (si le doc est re-sauvegarde sans changer les mentions, une double notif est creee). Solution : passer l'ancienne content snapshot dans l'event payload `acadenice.page.content.updated`. Patch mineur post-R3.7.
2. **Collab path vs REST path** : en collab (hocuspocus), la detection est deja faite par `PersistenceExtension`. En REST, le `NotificationEmitterService` prend le relais. Les deux peuvent coexister sans conflit car la deduplication native se base sur `oldMentionedUserIds` (diff). Mais si une page est sauvee en collab ET en REST dans la meme session, un doublon est possible. Acceptable pour v1.
3. **SSE optionnel** : le bell count est actuellement poll 30s. Si Corentin veut sub-seconde, brancher le bridge SSE (R3.7+ — note dans SESSION-RESUME).
4. **Notification preferences granularite** : les prefs in-app et email partagent la meme cle dans `users.settings.notifications` (native impl). Pour une granularite fine (ex: email ON, in-app OFF), il faudrait soit etendre le schema JSON soit creer une table `acadenice_notification_preferences` dediee. A evaluer selon le besoin utilisateur.

### Prochaine etape

R3.8 LIVRE (Patch 016). R3 ENTIEREMENT TERMINE.

---

## Patch 016 — R3.8 inline comments threads (page resolve + row comments)

**Date** : 2026-05-08
**Scope** : REST resolve facade for page comments, new acadenice_row_comment table, row comment threads UI in row-detail-modal, i18n FR+EN, 4 new permissions (30 total)

### Architecture Decision

**Page comments** : Docmost native `comments` table is already complete (inline
selection via yjsSelection, resolve tracking via `resolved_at`/`resolved_by_id`,
threaded replies, WS events). The full client-side comment panel, comment mark
Tiptap extension, and create/list/edit/delete/resolve flows are already shipped
natively (CommentController, CommentService, comment-list-with-tabs, etc.).

R3.8 adds the ONLY missing piece: a **REST resolve endpoint**. The native resolve
is collab-only (hocuspocus websocket). The frontend `resolveComment()` already
calls `/comments/resolve` but that route is absent from the native OSS controller.
`PageCommentResolveService` adds the endpoint and synchronizes the yjs mark
(best-effort via CollaborationGateway.handleYjsEvent).

**Row comments** : New `acadenice_row_comment` table. Row identity = (table_id,
row_id) — external string pair, no FK to Baserow. Thread model mirrors native
page comments (flat, root + replies only, root-only resolve). Comments panel
added as a new tab in `RowDetailModal`.

**Permissions** : 4 new keys added (comments:read/write/resolve/moderate).
Catalogue now at 30 named permissions + admin:*.

### Nouveaux fichiers backend

| Fichier | Role |
|---------|------|
| `apps/server/src/database/migrations/20260508T180000-create-acadenice-comments.ts` | Migration acadenice_row_comment + indexes |
| `apps/server/src/core/acadenice/comments/comments.module.ts` | NestJS module |
| `apps/server/src/core/acadenice/comments/dto/comment.dto.ts` | DTOs (row + page resolve) |
| `apps/server/src/core/acadenice/comments/services/row-comment.service.ts` | CRUD + resolve row comments |
| `apps/server/src/core/acadenice/comments/services/page-comment-resolve.service.ts` | REST resolve facade for native comments |
| `apps/server/src/core/acadenice/comments/controllers/row-comments.controller.ts` | REST /api/acadenice/row-comments/* |
| `apps/server/src/core/acadenice/comments/controllers/page-comments.controller.ts` | REST /api/acadenice/page-comments/resolve |
| `apps/server/src/core/acadenice/comments/spec/row-comment.service.spec.ts` | 8 unit tests |
| `apps/server/src/core/acadenice/comments/spec/page-comment-resolve.service.spec.ts` | 5 unit tests |
| `apps/server/src/core/acadenice/comments/spec/row-comments.controller.spec.ts` | 6 unit tests |

### Nouveaux fichiers frontend

| Fichier | Role |
|---------|------|
| `apps/client/src/features/acadenice/comments/services/row-comments-client.ts` | Axios HTTP client (6 functions) |
| `apps/client/src/features/acadenice/comments/hooks/use-row-comments.ts` | React Query hooks (list, count, create, resolve, delete) |
| `apps/client/src/features/acadenice/comments/components/row-comments-panel.tsx` | Row comment thread panel + composer |
| `apps/client/src/features/acadenice/comments/__tests__/row-comments-client.test.ts` | 7 client tests |
| `apps/client/src/features/acadenice/comments/__tests__/row-comments-panel.test.tsx` | 4 render tests |

### Fichiers upstream modifies (patches)

| Fichier | Modification |
|---------|-------------|
| `apps/server/src/core/core.module.ts` | +AcadeniceCommentsModule import + registration |
| `apps/server/src/core/acadenice/rbac/permissions-catalog.ts` | +4 comment permissions (comments:read/write/resolve/moderate) — 30 total |
| `apps/client/src/features/acadenice/database-view/components/row-detail-modal.tsx` | +Comments tab (Tabs) + RowCommentsPanel |
| `apps/client/public/locales/en-US/translation.json` | +17 acadenice.comments.* + 2 database_view.row_detail.tab_* keys |
| `apps/client/public/locales/fr-FR/translation.json` | +17 cles FR |

### Endpoints

```
POST /api/acadenice/page-comments/resolve    resolve/unresolve native page comment thread

POST /api/acadenice/row-comments/list        list row comment threads
POST /api/acadenice/row-comments/create      create root or reply
POST /api/acadenice/row-comments/update      edit own comment
POST /api/acadenice/row-comments/resolve     resolve/unresolve root thread
POST /api/acadenice/row-comments/delete      delete own (+ moderator)
POST /api/acadenice/row-comments/count       count comments for badge
```

### Tests

- Backend : 8 row-comment.service + 5 page-comment-resolve.service + 6 row-comments.controller = 19 tests
- Frontend : 7 client + 4 panel render = 11 tests
- Total R3.8 : 30 tests

### Points a debattre avec Corentin

1. **Page comment resolve REST vs collab-only** : La native resolve passe par hocuspocus (WS). Le nouvel endpoint REST met a jour la DB + tente de syncer le yjs mark via CollaborationGateway. Si le document n'est pas ouvert dans hocuspocus, le mark ne change pas visuellement jusqu'au prochain reload. C'est acceptable pour v1 — une alternative serait de stocker le resolved state uniquement en DB et ne pas dependre du yjs mark.
2. **Row comment content** : stocke en Tiptap JSON mais affiche en texte brut dans le panel. Pour la richesse (bold, mentions dans les row comments), il faudrait un mini TipTap read-only renderer. Slote pour R4+.
3. **Permissions row comments** : actuellement pas de check explicite `comments:write` dans le controller — tout user authentifie avec acces workspace peut commenter. Ajouter `RequirePermission('comments:write')` si un RBAC plus fin est souhaite.

### Prochaine etape

R3 ENTIEREMENT TERMINE. Recommandation : audit (pnpm install + pnpm typecheck + pnpm test) + install deps manquants si necessaire + e2e local.

---

## Patch 014 — R3.6 page templates system

**Date** : 2026-05-08
**Scope** : workspace page templates — DB table, backend module, frontend gallery + picker modal, built-in seed, 26 permissions, i18n FR+EN
**Rationale** : permet de sauvegarder des pages existantes comme templates, de lister les templates du workspace en gallery filtrable, et d'instancier un nouveau template depuis la sidebar "New page" ou le slash `/template`.

### Architecture

- Table DB : `acadenice_template` (workspace-scoped, UNIQUE workspace_id+name, JSONB content, is_built_in, is_workspace_default, usage_count).
- Backend `AcadeniceTemplatesModule` (NestJS) : TemplateService + TemplateSeedService + TemplatesController.
- 6 endpoints : `GET/POST /api/acadenice/templates`, `GET/PATCH/DELETE /api/acadenice/templates/:id`, `POST /api/acadenice/templates/:id/instantiate`, `PATCH /api/acadenice/templates/:id/default`.
- Built-in seed : 5 templates au boot (Meeting Note, Project Brief, Daily Standup, Weekly Review, Empty Page). is_built_in = true, clone-then-edit pattern.
- 3 nouvelles permissions : `templates:read`, `templates:create`, `templates:manage` (catalogue passe a 26).
- Frontend : gallery grid filtrable par categorie + search, TemplateForm (create/edit), TemplatePicker modal (sidebar New page + slash /template).
- i18n : 39 cles EN + 39 cles FR.

### Nouveaux fichiers backend

| Fichier | Role |
|---------|------|
| `apps/server/src/database/migrations/20260508T140000-create-acadenice-template.ts` | Migration up/down table + index |
| `apps/server/src/core/acadenice/templates/templates.module.ts` | NestJS module |
| `apps/server/src/core/acadenice/templates/dto/template.dto.ts` | Zod schemas + TypeScript types |
| `apps/server/src/core/acadenice/templates/services/template.service.ts` | CRUD + instantiate + setDefault |
| `apps/server/src/core/acadenice/templates/services/template-seed.service.ts` | 5 built-in templates seed au boot |
| `apps/server/src/core/acadenice/templates/controllers/templates.controller.ts` | REST controller |
| `apps/server/src/core/acadenice/templates/spec/template.service.spec.ts` | 22 tests service |
| `apps/server/src/core/acadenice/templates/spec/templates.controller.spec.ts` | 18 tests controller |

### Nouveaux fichiers frontend

| Fichier | Role |
|---------|------|
| `apps/client/src/features/acadenice/templates-admin/services/templates-client.ts` | Axios client HTTP |
| `apps/client/src/features/acadenice/templates-admin/queries/templates-query.ts` | React Query hooks |
| `apps/client/src/features/acadenice/templates-admin/components/template-card.tsx` | Card single template |
| `apps/client/src/features/acadenice/templates-admin/components/template-form.tsx` | Modal create/edit |
| `apps/client/src/features/acadenice/templates-admin/components/template-gallery.tsx` | Grid gallery + filtres |
| `apps/client/src/features/acadenice/templates-admin/pages/templates-page.tsx` | Page /settings/templates |
| `apps/client/src/features/acadenice/templates/components/template-picker-modal.tsx` | Picker modal + hook useInstantiateTemplate |
| `apps/client/src/features/acadenice/templates-admin/__tests__/templates-client.test.ts` | 9 tests client |
| `apps/client/src/features/acadenice/templates-admin/__tests__/templates-page.test.tsx` | 9 tests page |
| `apps/client/src/features/acadenice/templates-admin/__tests__/template-card.test.tsx` | 7 tests card |

### Fichiers upstream modifies (patches)

| Fichier | Modification |
|---------|-------------|
| `apps/server/src/core/core.module.ts` | +AcadeniceTemplatesModule import + registration |
| `apps/server/src/core/acadenice/rbac/permissions-catalog.ts` | +3 permissions templates:read/create/manage (26 total) |
| `apps/server/src/core/acadenice/rbac/services/seed.service.ts` | +templates:read/create/manage aux roles Admin/Editor/Member/Guest |
| `apps/client/src/App.tsx` | +import TemplatesAdminPage + route /settings/templates |
| `apps/client/src/components/settings/settings-sidebar.tsx` | +entree "Templates" + import IconTemplate |
| `apps/client/src/features/space/components/sidebar/space-sidebar.tsx` | "New page" -> dropdown (New page / From template), import TemplatePickerModal |
| `apps/client/src/features/editor/components/slash-menu/menu-items.ts` | +slash /template -> CustomEvent acadenice:open-template-picker |
| `apps/client/public/locales/en-US/translation.json` | +39 cles templates.* |
| `apps/client/public/locales/fr-FR/translation.json` | +39 cles templates.* |

### Catalogue permissions a jour (26 permissions)

```
pages:read|write|delete|share, space:read|create|write|delete|invite,
tables:list|create|write|delete, rows:read|write|delete,
attachments:upload|delete, users:invite|write|delete, roles:manage,
slash_commands:manage (R3.3),
templates:read|create|manage (R3.6 - nouveaux),
admin:*
```

### Tests

- Backend : 22 tests service + 18 tests controller = 40 tests (unit, mocked DB)
- Frontend : 9 tests client + 9 tests page + 7 tests card = 25 tests
- Total R3.6 : 65 tests

### Points a debattre avec Corentin

1. **Positionnement instantiate dans l'arbre** : la methode `getNextPagePosition` utilise une approche simple (append suffixe '0'). Pour une integration propre dans le Tiptap tree, il faudra utiliser `fractional-indexing-jittered` comme le fait `PageService`. Patch mineur post-R3.6.
2. **Instantiate depuis le picker** : le custom event `acadenice:open-template-picker` dispatche sur `document`. Cela fonctionne si le composant ecoutant (ex. Page.tsx) subscribe au mount. Alternativement, utiliser un atom jotai global `templatePickerOpenAtom` pour plus de robustesse. Bonne option pour R3.7 refactor.
3. **Cover image** : `cover_url` est optionnel (URL externe). Pas d'upload prevu dans ce sub-bloc. Si un template a une cover, elle s'affiche en CSS `background-image` — a implementer dans la card si Corentin le souhaite.
4. **Built-in vs custom template** : le pattern "clone-then-edit" n'est pas encore expose en UI (pas de bouton "Clone" sur les built-ins). A ajouter facilement dans la TemplateCard (action "Duplicate as custom").

### Prochaine etape

R3.7 — mentions `@user` + notifs in-app : mention declenche notif + email, center notifs UI + bell icon.

---

## Patch 013 — R3.5.2 frontend graph view (page /graph, react-force-graph-2d)

**Date** : 2026-05-08
**Scope** : knowledge graph frontend — page `/graph`, force-directed canvas, controls sidebar, side panel
**Rationale** : rend le graphe de liens inter-pages interactif (style Obsidian/AFFiNE).
Consomme `GET /api/acadenice/graph` (R3.5.1). Finalise R3.5 entierement.

### Architecture

- Lib : `react-force-graph-2d` (Canvas-based, d3-force, jusqu'a 5k nodes interactifs).
  Peer deps : `d3-force`. A installer : `pnpm add react-force-graph-2d d3-force`.
- Chargee en dynamic import (`React.lazy`) avec fallback placeholder si lib absente.
- Filter state : jotai atoms (`graphFiltersAtom`, `selectedNodeIdAtom`, `focusNodeIdAtom`, `sidePanelOpenAtom`).
- Data : React Query hook avec 300ms debounce sur les filtres.
- Side panel : Mantine Drawer ancre a droite.
- Navigation : `useNavigate` vers `/p/{slugId}` (slugId map a etendre quand le backend inclura `slugId` dans `GraphNode`).

### Route

`/graph` — workspace-level, accessible via sidebar (icone `IconAffiliate`).

### Nouveaux fichiers

| Fichier | Role |
|---------|------|
| `apps/client/src/features/acadenice/graph/services/graph-client.ts` | HTTP client fetchGraph |
| `apps/client/src/features/acadenice/graph/hooks/use-graph-controls.ts` | Jotai atoms filtres + UI state |
| `apps/client/src/features/acadenice/graph/hooks/use-graph-data.ts` | React Query hook + debounce |
| `apps/client/src/features/acadenice/graph/components/graph-canvas.tsx` | Canvas force-graph wrapper |
| `apps/client/src/features/acadenice/graph/components/graph-controls.tsx` | Sidebar filtres + stats |
| `apps/client/src/features/acadenice/graph/components/graph-node-tooltip.tsx` | Card tooltip node hover |
| `apps/client/src/features/acadenice/graph/components/graph-side-panel.tsx` | Drawer detail node |
| `apps/client/src/features/acadenice/graph/pages/graph-page.tsx` | Page orchestratrice |
| `apps/client/src/features/acadenice/graph/__tests__/graph-client.test.ts` | 10 tests |
| `apps/client/src/features/acadenice/graph/__tests__/use-graph-controls.test.ts` | 16 tests |
| `apps/client/src/features/acadenice/graph/__tests__/use-graph-data.test.ts` | 8 tests |
| `apps/client/src/features/acadenice/graph/__tests__/graph-controls.test.tsx` | 11 tests |
| `apps/client/src/features/acadenice/graph/__tests__/graph-canvas.smoke.test.tsx` | 5 smoke tests |
| `apps/client/src/features/acadenice/graph/__tests__/graph-side-panel.test.tsx` | 8 tests |

### Fichiers upstream modifies (patches)

| Fichier | Modification |
|---------|-------------|
| `apps/client/src/App.tsx` | +import GraphPage + route `/graph` dans `<Layout>` |
| `apps/client/src/components/layouts/global/global-sidebar.tsx` | +import IconAffiliate + entree `graph.page_title` dans mainNavItems |
| `apps/client/public/locales/en-US/translation.json` | +24 cles `graph.*` |
| `apps/client/public/locales/fr-FR/translation.json` | +24 cles `graph.*` (traductions FR) |

### Tests

- 58 tests total (10 graph-client + 16 use-graph-controls + 8 use-graph-data + 11 graph-controls + 5 canvas-smoke + 8 graph-side-panel)
- Canvas smoke : verifie le rendu sans crash quand react-force-graph-2d est absent (fallback)
- Hooks : debounce behavior, state transitions jotai, React Query staleTime

### Nouvelles deps (a installer)

```
react-force-graph-2d  ^1.43.x  (peer: d3-force ^3.0.x)
d3-force              ^3.0.x
```

### Choix techniques

- Dynamic import (`React.lazy`) : isole la dep Canvas du bundle critique. Fallback
  `<GraphPlaceholder>` affiche les instructions d'installation si la lib est absente.
- Jotai vs useState local : les 4 atoms sont cross-composant (canvas <-> controls <-> side panel).
  Un Context React aurait necessit un provider supplementaire ; jotai reste hors de l'arbre JSX.
- `spaceColorCache` module-level : mapping deterministe spaceId -> couleur Mantine palette.
  Reset automatique au rechargement de page (pas de persistence necessaire).
- slugId map actuellement vide : le backend GraphNode (R3.5.1) n'expose pas `slugId`.
  Extension possible sans casser le contrat : ajouter `slugId?: string` a GraphNode.

### Point a debattre avec Corentin

1. **slugId dans GraphNode** : faut-il enrichir le backend (R3.5.1) pour inclure `slugId`
   dans chaque node ? Permettrait l'activation du bouton "Open page" et la navigation
   double-click. Patch mineur service + DTO cote backend.
2. **react-force-graph-2d version exacte** : pinner a la derniere stable avant install
   (`pnpm add react-force-graph-2d@latest d3-force@latest --filter docmost-client`).
3. **Context menu right-click** : actuellement le right-click fait "Focus mode" (recenter).
   Un vrai context menu (Mantine Menu) avec "Open in new tab" / "Focus" / "Copy link"
   est possible mais necessite un overlay positionne sur le canvas (hors Mantine portals).

---

## Patch 012 — R3.5.1 backend graph endpoint GET /api/acadenice/graph

**Date** : 2026-05-08
**Scope** : knowledge graph backend — nodes + edges from acadenice_backlink table
**Rationale** : expose workspace link-graph as JSON for the R3.5.2 frontend
(Obsidian-style graph view). Reads the `acadenice_backlink` table populated by R3.2.

### Architecture

- Source de verite : table `acadenice_backlink` (indexee par R3.2 sur chaque save).
- Permission filter : meme join space_members / visibility='public' que BacklinkService.
- BFS iteratif en memoire (apres chargement des edges) pour les graphes centres (pageId).
- Cache Redis TTL 60s par cle composite. Invalidation sur evenement `acadenice.page.content.updated` (meme event que R3.2).
- Truncation a 1000 nodes pour les workspaces larges (top-inDegree + flag `truncated: true`).

### Endpoint

`GET /api/acadenice/graph?workspaceId=X&spaceId=Y&pageId=Z&depth=N&types=wikilink,mention,database_embed&includeOrphans=false`

- `workspaceId` : ignore — resolu depuis le JWT pour eviter les fuites cross-workspace
- `spaceId` : optionnel, filtre les nodes au space
- `pageId` : optionnel, centre le graphe + BFS depth hops
- `depth` : 1-5, default 2
- `types` : filtre par type de lien (defaut: tous les 3)
- `includeOrphans` : default false

### Reponse

```ts
{
  nodes: Array<{ id, label, type, spaceId, spaceName, icon, isOrphan, metrics: { inDegree, outDegree } }>,
  edges: Array<{ id, source, target, type, weight }>,
  meta: { totalNodes, totalEdges, workspaceId, rootPageId?, depth?, truncated }
}
```

### Fichiers crees

| Fichier | Role |
|---------|------|
| `apps/server/src/core/acadenice/graph/graph.module.ts` | NestJS module (R3.5.1) |
| `apps/server/src/core/acadenice/graph/dto/graph.dto.ts` | Zod schemas + interfaces response |
| `apps/server/src/core/acadenice/graph/services/graph.service.ts` | buildGraph, BFS, Redis cache |
| `apps/server/src/core/acadenice/graph/controllers/graph.controller.ts` | GET /api/acadenice/graph |
| `apps/server/src/core/acadenice/graph/spec/graph.service.spec.ts` | Tests service (21 tests) |
| `apps/server/src/core/acadenice/graph/spec/graph.controller.spec.ts` | Tests controller (14 tests) |

### Fichiers modifies (patches upstream)

| Fichier | Modification |
|---------|-------------|
| `apps/server/src/core/core.module.ts` | +AcadeniceGraphModule import + declaration |

### Tests

- 35 tests total (21 service + 14 controller)
- Service : full graph, edge weight, BFS depth=1/2, spaceId filter, types filter,
  permission filter, truncation@1000, orphans inclus/exclus, inDegree/outDegree,
  cache hit, cache invalidation, bfsReachable unit, buildCacheKey, error resilience
- Controller : routing, params parsing (depth/spaceId/pageId/types/includeOrphans),
  validation errors (bad UUID, depth>5, depth<1), workspace isolation

### Strategies techniques

- SQL : GROUP BY (source_page_id, target_page_id, link_type) avec COUNT(*) pour weight.
  Double join pages/spaces (source ET target) avec permission check inline.
- BFS : iteratif avec Set visited. Graphe non-oriente (in+out edges). Cap MAX_NODES=1000.
- Cache key : `acadenice:graph:<wsId>:<spaceId|'all'>:<pageId|'root'>:<depth>:<types-sorted>:<includeOrphans>`
- Invalidation : pattern `acadenice:graph:<wsId>:*` via KEYS + DEL. Acceptable pour TTL=60s.

---

## Patch 011 — R3.4 dual editor (WYSIWYG + markdown source)

**Date** : 2026-05-08
**Scope** : toggle WYSIWYG <-> raw markdown source, custom-node round-trip
**Rationale** : permet aux utilisateurs power-users d'editer le source markdown
directement, avec une conversion aller-retour complete preservant les nodes
Acadenice custom (database-view, wikilink, mention).

### Architecture

- Source de verite : Tiptap JSON (persiste en DB). Le markdown est une vue.
- Mode persist : localStorage `acadenice:editor-mode:<pageId>` par page.
- Switch lossy : modal de confirmation listant les elements alteres.
- Save : en mode markdown, le doc Tiptap est maintenu sync (setContent) a chaque
  keystroke pour que le mecanisme save Docmost natif reste fonctionnel.

### Syntaxe custom nodes en markdown

| Node | Syntaxe markdown |
|------|-----------------|
| `database-view` | `[[!db tableId=X viewId=Y viewType=Z]]` |
| `wikilink` | `[[Page Title]]` ou `[[Page Title\|alias]]` |
| `mention` | `@<userId>(displayName)` |

Choix : tokens entre `[[...]]` pour etre lisibles et reversibles. Le prefixe
`!db` distingue les database-view des wikilinks. Les mentions encodent le userId
(UUID) pour eviter la necessite d'une resolution serveur au re-parse.

### Fichiers crees

| Fichier | Role |
|---------|------|
| `apps/client/src/features/acadenice/dual-editor/services/custom-node-serializers.ts` | Registre des serializers custom (databaseView, wikilink, mention) |
| `apps/client/src/features/acadenice/dual-editor/services/markdown-converter.ts` | `tiptapToMarkdown` + `markdownToTiptap` — converter custom sans dep externe |
| `apps/client/src/features/acadenice/dual-editor/hooks/use-editor-mode.ts` | Jotai atom `editorModeAtom` + `useEditorMode` hook + `initEditorMode` |
| `apps/client/src/features/acadenice/dual-editor/components/mode-toggle-button.tsx` | Bouton toggle (IconCode / IconEye) dans la toolbar |
| `apps/client/src/features/acadenice/dual-editor/components/markdown-editor.tsx` | Textarea monospace auto-resize (Tab -> 2 espaces) |
| `apps/client/src/features/acadenice/dual-editor/components/dual-editor.tsx` | Wrapper WYSIWYG / markdown avec modal warning lossy |
| `apps/client/src/features/acadenice/dual-editor/__tests__/markdown-converter.test.ts` | 61 tests round-trip (JSON->MD->JSON et MD->JSON->MD) |
| `apps/client/src/features/acadenice/dual-editor/__tests__/custom-node-serializers.test.ts` | 12 tests unitaires serializers |
| `apps/client/src/features/acadenice/dual-editor/__tests__/use-editor-mode.test.ts` | 4 tests persistence localStorage |

### Fichiers modifies (patches upstream)

| Fichier | Modification |
|---------|-------------|
| `apps/client/src/features/editor/full-editor.tsx` | +import DualEditor + wrap `<MemoizedPageEditor>` avec `<DualEditor>` |
| `apps/client/public/locales/en-US/translation.json` | +8 cles `dual_editor.*` |
| `apps/client/public/locales/fr-FR/translation.json` | +8 cles `dual_editor.*` traduits |

### Nouvelles dependances requises

Aucune. Le converter est custom TypeScript pur. L'editeur markdown utilise une
`<Textarea>` Mantine (deja installee). Si CodeMirror 6 est desire dans une
iteration future, le swap est localise dans `markdown-editor.tsx` uniquement.

### Tests

- 77 tests total (61 converter + 12 serializers + 4 hook)
- Round-trip cases : 16 JSON->MD->JSON + 8 MD->JSON->MD = 24 cas round-trip
- Custom nodes : 4 serializers x 2 directions = 8 cas specifiques
- Edge cases : 9 cas (empty doc, unknown nodes, malformed tokens, etc.)

---

## Patch 001 — Rebrand minimal "Docmost" -> "DocAdenice"

**Date** : 2026-05-07
**Scope** : strings UI visibles utilisateur uniquement
**Rationale** : nom temporaire pour les beta-testeurs en attendant le vrai rebranding (logo SVG + design system + manifest PWA). Conserve les identifiants techniques pour ne rien casser et faciliter le rebase upstream.

### Fichiers modifies

| Fichier | Avant | Apres |
|---------|-------|-------|
| `apps/client/index.html` | `<title>Docmost</title>` | `<title>DocAdenice</title>` |
| `apps/client/index.html` | `apple-mobile-web-app-title content="Docmost"` | `content="DocAdenice"` |
| `apps/client/src/lib/config.ts` | `getAppName() return "Docmost"` | `return "DocAdenice"` |
| `apps/client/src/components/layouts/global/app-header.tsx` | brand `aria-label`, `alt`, texte `Docmost` | `DocAdenice` |
| `apps/client/src/features/auth/components/auth-layout.tsx` | brand `alt`, texte `Docmost` | `DocAdenice` |
| `apps/client/src/components/ui/error-404.tsx` | titre 404 ` - Docmost` | ` - DocAdenice` |
| `apps/client/src/features/home/components/home-ai-prompt.tsx` | fallback workspace name `"Docmost"` | `"DocAdenice"` |
| `apps/server/src/integrations/transactional/emails/invitation-email.tsx` | `"You have been invited to Docmost."` | `"...DocAdenice."` |
| `apps/server/src/integrations/transactional/partials/partials.tsx` | footer `© Docmost` | `© DocAdenice` |
| `apps/server/src/core/workspace/services/workspace-invitation.service.ts` | sujet `... has accepted your Docmost invite` | `... DocAdenice invite` |
| `apps/server/src/core/workspace/services/workspace-invitation.service.ts` | sujet `... invited you to Docmost` | `... DocAdenice` |
| `apps/server/src/integrations/environment/environment.service.ts` | `MAIL_FROM_NAME` default `'Docmost'` | `'DocAdenice'` |
| `README.md` | header initial Docmost | bloc "DocAdenice" ajoute au-dessus |

### KEEP volontairement (non modifies)

| Element | Raison |
|---------|--------|
| `package.json` `name: "docmost"` | nom du package npm interne, casserait les imports/scripts Nx |
| `@docmost/editor-ext` workspace package | identifiant pnpm workspace |
| `docker-compose.yml` service `docmost` | identifiant technique |
| `apps/server/src/core/auth/token.module.ts` JWT issuer `'Docmost'` | changer invaliderait les tokens existants |
| `apps/server/src/core/workspace/workspace.constants.ts` `'docmost'` dans DISALLOWED_HOSTNAMES | blacklist hostnames reserves, technique |
| `apps/server/src/common/helpers/types/export-metadata.types.ts` `source: 'docmost'` | format export pour interop avec Docmost officiel |
| `apps/server/src/integrations/export/export.service.ts` filename `docmost-metadata.json` | format export, interop |
| `apps/server/src/integrations/import/services/file-import-task.service.ts` (vars `docmostMetadata`, prefix `docmost-import`, fonction `readDocmostMetadata`) | identifiants techniques + lecture du format export Docmost |
| `apps/server/src/integrations/import/utils/import.utils.ts` `readDocmostMetadata` | API publique du module import |
| `apps/server/src/integrations/security/version.service.ts` URL `github.com/docmost/docmost/releases` | check de version vs upstream officiel |
| `apps/server/src/integrations/telemetry/telemetry.service.ts` endpoint `tel.docmost.com` | telemetry upstream (a desactiver dans une iteration future via env var) |
| `apps/client/src/components/settings/settings-sidebar.tsx` `help@docmost.com` | email support upstream officiel, on n'usurpe pas |
| `apps/client/src/components/settings/app-version.tsx` URL releases | check de version upstream |
| `apps/client/src/ee/**` (license, AI, MCP, API keys, share-branding "Powered by Docmost") | code Enterprise Edition propriete Docmost — copy commerciale, ne pas masquer |
| `apps/client/src/ee/components/posthog-user.tsx` `source: "docmost-app"` | identifiant analytics upstream |
| `apps/server/src/integrations/environment/environment.validation.ts` URL clickhouse exemple | message d'erreur dev-facing technique |
| `apps/server/src/core/workspace/services/workspace.service.ts` `@deleted.docmost.com` | placeholder technique pour soft-delete |

---

## Patch 002 — Bloc 4b : OIDC client (Authentik) via openid-client

**Date** : 2026-05-07
**Scope** : nouveau flow d'authentification SSO via Authentik (ou tout IdP OIDC), desactive par defaut
**Rationale** : preparer l'integration SSO pour le hub Acadenice. Le code est dormant tant que `OIDC_ENABLED=true` n'est pas pose, donc zero impact sur les deploiements actuels. Les fichiers sont isoles dans un sous-dossier dedie pour faciliter le rebase upstream.

### Lib utilisee

`openid-client` v6.8.2 — deja en dependance dans `apps/server/package.json`. API fonctionnelle (pas un client object-oriented), import lazy au boot pour eviter l'overhead quand OIDC est off.

### Fichiers crees

| Fichier | Role |
|---------|------|
| `apps/server/src/core/auth/oidc/oidc.module.ts` | Module Nest dedie, importe par CoreModule |
| `apps/server/src/core/auth/oidc/oidc.service.ts` | Discovery, PKCE, callback handler, JIT provisioning |
| `apps/server/src/core/auth/oidc/oidc.controller.ts` | Routes `/api/auth/oidc/login`, `/callback`, `/status` |
| `apps/server/src/core/auth/oidc/oidc.service.spec.ts` | 8 tests unitaires (Jest) avec `openid-client` mocke |
| `apps/client/src/features/auth/queries/oidc-query.ts` | Hook `useOidcStatus()` (React Query) |
| `apps/client/src/features/auth/components/oidc-login-button.tsx` | Bouton SSO conditionnel sur le formulaire login |

### Fichiers modifies (touches minimales)

| Fichier | Modification |
|---------|--------------|
| `apps/server/src/integrations/environment/environment.service.ts` | +9 getters OIDC (isOidcEnabled, getOidcIssuer, ...) appendus en fin de classe |
| `apps/server/src/core/core.module.ts` | +1 import + 1 ligne dans `imports[]` pour `OidcModule` |
| `apps/client/src/features/auth/components/login-form.tsx` | +2 lignes : import + `<OidcLoginButton />` au-dessus de `<SsoLogin />` |
| `.env.example` | bloc OIDC commente ajoute en fin de fichier |

### Securite

- PKCE S256 (verifier + challenge generes par `openid-client`)
- State CSRF stocke en cookie httpOnly signe (5 min TTL)
- ID token verifie par signature JWKS (gere par `openid-client` v6 via la `Configuration` cachee)
- userInfo refetched apres l'echange — on ne fait pas confiance aux claims ID token seuls pour `email`
- Cookies temporaires `oidc_state` / `oidc_pkce` clear immediatement apres consommation

### Variables d'env

| Var | Defaut | Role |
|-----|--------|------|
| `OIDC_ENABLED` | `false` | master switch |
| `OIDC_ISSUER` | (vide) | URL discovery (ex `https://auth.example.com/application/o/docadenice/`) |
| `OIDC_CLIENT_ID` | (vide) | requis |
| `OIDC_CLIENT_SECRET` | (vide) | requis |
| `OIDC_REDIRECT_URI` | `${APP_URL}/api/auth/oidc/callback` | derive auto si non set |
| `OIDC_SCOPES` | `openid email profile` | Authentik : `groups` claim arrive via le scope `profile` (pas un scope standard) |
| `OIDC_PROVIDER_NAME` | `SSO` | label affiche sur le bouton |
| `OIDC_AUTO_PROVISION` | `false` | si true : cree le user a la volee si email inconnu |
| `OIDC_DEFAULT_WORKSPACE_ID` | (vide) | requis si multi-workspace + auto-provision |

### TODO Bloc 4b suivants

- Mapping groupes Authentik vers roles Docmost (`OWNER` / `ADMIN` / `MEMBER`)
- Logout federe (RP-initiated logout vers Authentik)
- Tests E2E avec un vrai container Authentik (Testcontainers)
- Bouton login OIDC integre au flow `enforceSso` cote workspace (actuellement le bouton apparait des que `OIDC_ENABLED=true`, sans condition supplementaire)

---

## Patch 003 — R2.1 : RBAC dynamique multi-roles (backend)

**Date** : 2026-05-07
**Scope** : nouveau systeme RBAC source-de-verite cote DocAdenice + JWT enrichi `acadenice_permissions[]` + 5 roles classiques pre-seed
**Rationale** : DocAdenice devient generique (pivot R1) — il faut un RBAC dynamique multi-roles (un user peut cumuler plusieurs roles, union des permissions) editable par l'admin via UI, qui signe ses permissions effectives dans le JWT pour que le bridge formation-hub les consume sans re-query la base. Pattern Notion. Le RBAC custom cohabite avec les roles natifs Docmost (`WorkspaceUser.role` `OWNER`/`ADMIN`/`MEMBER`) — natifs gardes pour les guards Docmost upstream, le RBAC Acadenice est une couche par-dessus.

### Catalogue de permissions

22 permissions atomiques generiques, catalogue ferme en TS dans `apps/server/src/core/acadenice/rbac/permissions-catalog.ts`. Toute insertion en base est validee contre ce catalogue cote service.

Groupes : `pages`, `space`, `tables`, `rows`, `attachments`, `users` + meta `roles:manage` + wildcard `admin:*`.

### Tables Postgres

Toutes prefixees `acadenice_` (zero conflit avec les tables upstream Docmost) :

- `acadenice_role` (id, workspace_id FK, name, description, is_system_role, ts) — unique (workspace_id, name)
- `acadenice_role_permission` (role_id FK cascade, permission_key) — pk composee
- `acadenice_user_role` (user_id FK cascade, role_id FK cascade, workspace_id FK, assigned_by FK set null, assigned_at) — pk composee (user_id, role_id)

Migration : `apps/server/src/database/migrations/20260507T120000-create-acadenice-rbac.ts`. Idempotente (`ifNotExists` partout).

### 5 roles classiques pre-seed (au boot, idempotent)

| Role | is_system | Permissions |
|------|-----------|-------------|
| Owner | true | `admin:*` |
| Admin | true | tout sauf `*:delete` et `roles:manage` |
| Editor | true | pages:read/write/share, space:read/write, rows:read/write, attachments:upload |
| Member | true | read-only + attachments:upload |
| Guest | true | `pages:read` |

`is_system_role=true` -> rename / delete refuses cote service. Permissions modifiables (admin peut reshape les roles seed sans perdre l'identite system).

`AcadeniceRbacSeedService.onModuleInit()` au boot : seed tous les workspaces existants. Idempotent : ne duplique pas. Failure -> log et le boot poursuit (in my tests, le seed echoue tot si la migration n'a pas tourne, et le boot suivant retente).

### JWT enrichi

`TokenService.generateAccessToken()` injecte `acadenice_permissions: string[]` dans le payload du token d'acces. Cache Redis 60s par user/workspace (key `acadenice:perms:user:<userId>:ws:<workspaceId>`). Si le user a `admin:*`, la liste est court-circuitee a `["admin:*"]`. Failure de resolution -> claim vide (degradation graceful, le bridge a son propre fallback).

### Guard NestJS

Decorator `@RequirePermission('roles:manage')` + `AcadenicePermissionsGuard` :
- Lit `req.user = { user, workspace }` (pose en amont par `JwtAuthGuard`)
- Resoud les permissions via `AcadeniceRoleService.getUserPermissions` (cache Redis 60s)
- Match avec wildcard support : `admin:*` couvre tout, `<group>:*` couvre `<group>:<action>`
- Throw `ForbiddenException` si miss

### Endpoints REST

```
GET    /api/acadenice/permissions                          (auth)
GET    /api/acadenice/roles                                (auth)
POST   /api/acadenice/roles                                (perm: roles:manage)
GET    /api/acadenice/roles/:id                            (perm: roles:manage)
PATCH  /api/acadenice/roles/:id                            (perm: roles:manage)
DELETE /api/acadenice/roles/:id                            (perm: roles:manage)
GET    /api/acadenice/roles/:id/permissions                (perm: roles:manage)
PUT    /api/acadenice/roles/:id/permissions                (perm: roles:manage)
GET    /api/acadenice/users/:userId/roles                  (perm: roles:manage OR self)
POST   /api/acadenice/users/:userId/roles                  (perm: roles:manage, body { roleIds: [...] })
DELETE /api/acadenice/users/:userId/roles/:roleId          (perm: roles:manage)
```

Les routes user-roles refusent l'auto-modification meme avec `roles:manage` (anti-escalation).

### Fichiers crees

| Fichier | Role |
|---------|------|
| `apps/server/src/core/acadenice/rbac/permissions-catalog.ts` | Catalogue ferme + helpers `isPermissionKey`, `permissionMatches` |
| `apps/server/src/core/acadenice/rbac/rbac.module.ts` | Module Nest, exporte `AcadeniceRoleService` + guard |
| `apps/server/src/core/acadenice/rbac/dto/create-role.dto.ts` | CreateRoleDto |
| `apps/server/src/core/acadenice/rbac/dto/update-role.dto.ts` | UpdateRoleDto + UpdateRolePermissionsDto |
| `apps/server/src/core/acadenice/rbac/dto/assign-role.dto.ts` | AssignRolesDto |
| `apps/server/src/core/acadenice/rbac/dto/role.dto.ts` | RoleDto + RoleWithPermissionsDto + UserRoleAssignmentDto |
| `apps/server/src/core/acadenice/rbac/repos/role.repo.ts` | Repo SQL natif (pas de typed Kysely : tables hors `db.d.ts`, rebase-friendly) |
| `apps/server/src/core/acadenice/rbac/repos/user-role.repo.ts` | Repo user-role + `getEffectivePermissions` (union via JOIN, court-circuit `admin:*`) |
| `apps/server/src/core/acadenice/rbac/services/role.service.ts` | RoleService — CRUD, assignation, cache Redis 60s, validation catalog |
| `apps/server/src/core/acadenice/rbac/services/seed.service.ts` | SeedService — 5 roles seed idempotent au boot |
| `apps/server/src/core/acadenice/rbac/guards/permissions.guard.ts` | AcadenicePermissionsGuard avec wildcard support |
| `apps/server/src/core/acadenice/rbac/guards/require-permission.decorator.ts` | `@RequirePermission(...)` decorator |
| `apps/server/src/core/acadenice/rbac/controllers/permissions.controller.ts` | GET /api/acadenice/permissions (catalog read-only) |
| `apps/server/src/core/acadenice/rbac/controllers/roles.controller.ts` | CRUD roles + permissions |
| `apps/server/src/core/acadenice/rbac/controllers/user-roles.controller.ts` | Assignations user-roles |
| `apps/server/src/core/acadenice/rbac/spec/role.service.spec.ts` | 11 tests unitaires (Jest, mocks repos) |
| `apps/server/src/core/acadenice/rbac/spec/permissions.guard.spec.ts` | 11 tests unitaires (Reflector mock + permissionMatches) |
| `apps/server/src/core/acadenice/rbac/spec/seed.service.spec.ts` | 3 tests unitaires (idempotence + creation initiale) |
| `apps/server/src/database/migrations/20260507T120000-create-acadenice-rbac.ts` | Migration Kysely 3 tables |

### Fichiers modifies (touches minimales)

| Fichier | Modification |
|---------|--------------|
| `apps/server/src/core/auth/dto/jwt-payload.ts` | +1 champ optionnel `acadenice_permissions?: string[]` sur `JwtPayload` |
| `apps/server/src/core/auth/services/token.service.ts` | +injection optionnelle `AcadeniceRoleService`, +resoud les perms a chaque sign d'access token (failure graceful = []) |
| `apps/server/src/core/auth/token.module.ts` | +import `AcadeniceRbacModule`, re-export pour les consumers de TokenModule |
| `apps/server/src/core/core.module.ts` | +import `AcadeniceRbacModule` dans `imports[]` |

### Cache strategy

- Cle Redis : `acadenice:perms:user:<userId>:ws:<workspaceId>` (TTL 60s)
- Best-effort : un failure cache (Redis down, parse error) -> fallback sur SQL, in my tests pas de 500
- Invalidation : sur mutation user-role, on `DEL` la cle du user. Sur mutation de role/permission, on `SCAN` + `DEL` toutes les cles du workspace (pas de `KEYS *` bloquant)
- Service `AcadeniceRoleService` rendu utilisable hors-Redis (`@Optional()` sur l'injection) — utile pour les tests unitaires

### Cohabitation avec roles natifs Docmost

- `WorkspaceUser.role` (OWNER/ADMIN/MEMBER) : KEEP — utilise par les guards natifs Docmost (creation page, invitation, etc.)
- `acadenice_role` : couche par-dessus, source de verite pour le JWT et le bridge
- L'admin DocAdenice peut couvrir 100% des cas via les `acadenice_role` + Owner/Admin natifs s'il decide de mapper les permissions natives plus tard. Compatible.

### Edge cases couverts

- User sans aucun role -> `acadenice_permissions: []` dans le JWT
- User avec `admin:*` -> `["admin:*"]` (court-circuit, payload tiny)
- Role rename quand un autre role porte deja le nouveau nom -> `409 Conflict`
- Rename / delete d'un system role -> `403 Forbidden` (cote service)
- Auto-modification de ses propres roles -> `403 Forbidden` (anti-escalation)
- Assignation d'un role d'un autre workspace -> `404 NotFound` validation prealable atomique
- Assignation duplique du meme role -> idempotent (`ON CONFLICT DO NOTHING`)
- Permission inconnue dans une payload -> `400 BadRequest` avec liste autorisee
- Migration re-run sur DB partiellement migree -> `ifNotExists` evite l'erreur
- Seed echoue au boot -> log, le boot continue, retry au boot suivant
- Redis down -> service degrade en SQL direct, JWT a quand meme les perms (a chaque sign)

### TODO laisses

- Frontend `/settings/roles` (R2.2) — page admin pour gerer les roles
- Mapping group sync OIDC -> `acadenice_role` (utile en couplage avec Patch 002)
- Audit log des changements de role (qui a assigne quoi a qui)
- Quand un nouveau workspace est cree, le seed actuel ne s'execute qu'au prochain boot — il faudrait hooker `WorkspaceService.create` pour seeder en live (R2.2 ou R2.3)
- Endpoint `POST /api/acadenice/permissions/me` pour query rapidement les perms du user courant cote front (alternativement : decoder le JWT)
- Permissions cache : invalidation cross-workspace via Redis Streams si on monte plusieurs instances Docmost (probleme deja present dans Docmost natif, hors scope R2.1)

### Bugs detectes dans Docmost natif

Aucun bug bloquant. Le test stub `apps/server/src/core/auth/services/token.service.spec.ts` etait deja casse avant ce patch (provider declare sans ses dependances) — non touche pour ne pas mentir sur la dette upstream.

### Verifications skipped

- `pnpm install` : pas execute (deps absents en local par convention de l'agent fork — Corentin install + build)
- TypeScript build : pas execute (cf ci-dessus)
- Migration runtime : pas executee (pas de Postgres local pour le moment)
- Tests Jest : ecrits mais pas runs en local (pnpm absent)

---

## Patch 004 — R2.2 : Frontend pages settings RBAC dynamique

**Date** : 2026-05-07
**Scope** : UI admin pour CRUD roles + assignation user-roles + matrix permissions wildcard-aware
**Rationale** : R2.1 a livre l'API REST + 22 permissions catalog. R2.2 consomme cette API cote front. Toute l'UI est isolee dans `apps/client/src/features/acadenice/rbac/` pour minimiser les conflits de rebase upstream. Les patches sur les fichiers Docmost upstream sont strictement minimaux : 1 import + 1 entree sidebar, 3 imports + 3 routes router, 0 modification dans les pages existantes.

### Pages livrees

```
/settings/roles                 → liste + filtres + create
/settings/roles/:id             → identite + matrix permissions + danger zone
/settings/users/:userId/roles   → multi-select roles + preview permissions effectives
```

### Composants cles

`PermissionMatrix` — accordeon de cards Mantine, une par groupe (`pages`, `space`, `tables`, `rows`, `attachments`, `users`, `meta`). Trois niveaux de granularite :
- `admin:*` carte dediee : grise toutes les autres permissions quand cochee
- `<group>:*` wildcard par groupe : grise les permissions atomiques du groupe
- Atomic checkboxes : tooltips avec descriptions du catalogue

Indeterminate state Mantine quand le groupe est partiellement coche. Disabled mode pour les system roles (avec Alert explicatif).

### Hook useAcadenicePermissions

Tente de lire le claim `acadenice_permissions[]` que R2.1 pose dans le JWT. Limites connues :
- Le `authToken` est en cookie HttpOnly cote serveur (impossible a lire en JS) ; on tente le cookie non-HttpOnly `authTokens` et l'atom jotai legacy (au cas ou un flow OIDC pose le token cote client)
- Si aucun claim disponible : fallback sur le role natif Docmost (`OWNER` / `ADMIN` -> presume manage-capable pour la sidebar uniquement)
- Le backend reste source de verite : il renvoie 403 si `roles:manage` manque vraiment

### Strategie i18n

Cles ajoutees dans `apps/client/public/locales/en-US/translation.json` et `fr-FR/translation.json` (~80 cles). Pas de namespace separe — le pattern Docmost utilise un seul `translation.json` par langue, on s'aligne. Les autres langues (ja/de/it/etc.) heriteront du fallback en-US tant qu'elles ne sont pas traduites.

### Tests Vitest + Testing Library

`apps/client` n'avait pas de runner de tests. R2.2 introduit Vitest + jsdom + Testing Library :
- `apps/client/vitest.config.ts` — config dediee, alias `@` / `src`
- `apps/client/src/test-setup.ts` — stubs `matchMedia` + `ResizeObserver` (Mantine en a besoin)
- `apps/client/package.json` — scripts `test` + `test:watch` + devDeps (`vitest`, `@testing-library/react`, `@testing-library/user-event`, `@testing-library/jest-dom`, `jsdom`)

Les 4 fichiers de tests dans `features/acadenice/rbac/__tests__/` mockent `rbac-service` et `useAcadenicePermissions` via `vi.mock`. Pas de setup MSW : on intercepte directement les fonctions de service (le boundary front-back).

### Fichiers crees

| Fichier | Role |
|---------|------|
| `apps/client/src/features/acadenice/rbac/types/rbac.types.ts` | Types alignes sur DTOs backend R2.1 |
| `apps/client/src/features/acadenice/rbac/services/rbac-service.ts` | Wrapper REST sur axios (10 endpoints) |
| `apps/client/src/features/acadenice/rbac/queries/permissions-query.ts` | `usePermissionsCatalogQuery` (cache 30 min) |
| `apps/client/src/features/acadenice/rbac/queries/roles-query.ts` | `useRolesQuery`, `useRoleQuery`, `useCreateRoleMutation`, `useUpdateRoleMutation`, `useDeleteRoleMutation`, `useSetRolePermissionsMutation` |
| `apps/client/src/features/acadenice/rbac/queries/user-roles-query.ts` | `useUserRolesQuery`, `useAssignRolesMutation`, `useUnassignRoleMutation` |
| `apps/client/src/features/acadenice/rbac/hooks/use-acadenice-permissions.ts` | Best-effort JWT claim reader + fallback admin natif |
| `apps/client/src/features/acadenice/rbac/components/permission-matrix.tsx` | Composant cle — wildcard-aware, indeterminate, tooltips |
| `apps/client/src/features/acadenice/rbac/components/role-form.tsx` | Form Mantine create/edit name+description avec validation |
| `apps/client/src/features/acadenice/rbac/components/delete-role-modal.tsx` | Confirmation modale avec saisie obligatoire du nom |
| `apps/client/src/features/acadenice/rbac/components/role-row.tsx` | Row table avec badges system/custom |
| `apps/client/src/features/acadenice/rbac/pages/roles-list.page.tsx` | Page `/settings/roles` |
| `apps/client/src/features/acadenice/rbac/pages/role-detail.page.tsx` | Page `/settings/roles/:id` |
| `apps/client/src/features/acadenice/rbac/pages/user-roles-panel.tsx` | Page + composant reutilisable `UserRolesPanel` |
| `apps/client/src/features/acadenice/rbac/styles/permission-matrix.module.css` | Style admin card |
| `apps/client/src/features/acadenice/rbac/styles/role-detail.module.css` | Sections + danger zone |
| `apps/client/src/features/acadenice/rbac/__tests__/test-utils.tsx` | Wrapper providers (QueryClient + Mantine + MemoryRouter) |
| `apps/client/src/features/acadenice/rbac/__tests__/permission-matrix.test.tsx` | 8 tests sur la matrix |
| `apps/client/src/features/acadenice/rbac/__tests__/roles-list.page.test.tsx` | 5 tests sur la liste |
| `apps/client/src/features/acadenice/rbac/__tests__/role-detail.page.test.tsx` | 4 tests sur le detail |
| `apps/client/src/features/acadenice/rbac/__tests__/user-roles-panel.test.tsx` | 5 tests sur les assignments |
| `apps/client/vitest.config.ts` | Config Vitest |
| `apps/client/src/test-setup.ts` | Setup global testing (matchMedia, ResizeObserver) |

### Fichiers modifies (touches minimales)

| Fichier | Modification |
|---------|--------------|
| `apps/client/src/App.tsx` | +3 imports + 3 `<Route>` enfants de `/settings` |
| `apps/client/src/components/settings/settings-sidebar.tsx` | +1 import (`useAcadenicePermissions`) +1 import icon (`IconShieldLock`) +1 entree dans `groupedData.Workspace.items` apres "Groups" +1 ligne dans `canShowItem` (filtre `acadeniceCanManageRoles`) +1 champ TS sur `DataItem` |
| `apps/client/src/i18n/.../translation.json` (en-US, fr-FR) | +80 cles RBAC |
| `apps/client/package.json` | +5 devDeps (vitest, @testing-library/{react,user-event,jest-dom}, jsdom) +2 scripts npm |

### Edge cases couverts UX

- Loading state : Mantine `Loader` centre dans chaque page
- Error state : `Alert` + bouton Retry qui appelle `refetch`
- Empty state : message contextuel ("seed roles will appear" vs "try clearing filters")
- System role : nom locked, delete locked + tooltip explicatif, matrix editable mais avec banner "system protected"
- Anti-escalation : `UserRolesPanel` n'auto-modifie pas le user (le backend rejette de toute facon — l'UI ne tente pas)
- Permission preview : se desactive si `canMutate=false` car les calls `getRole` necessitent `roles:manage`
- Dirty tracking : boutons Save/Discard se desactivent si les drafts == server state (compare ensembles tries)
- A11y : `aria-label` sur tous les inputs / icon buttons, `Helmet` titres, `aria-live="polite"` sur Alerts d'etat

### TODO laisses (non bloquants R2.2)

- Endpoint backend `GET /api/acadenice/permissions/me` pour eviter le hack JWT cookie (R2.3)
- Pagination de la liste des roles (actuellement on assume < 100 roles par workspace, raisonnable)
- Section "Members" dans la page detail role (lookup inverse `roleId -> users`) — necessite un nouvel endpoint backend
- Integration dans la table `WorkspaceMembersTable` existante (un menu "Manage Acadenice roles" inline plutot que la page dediee `/settings/users/:userId/roles`)
- Bulk assign : assigner un role a N users d'un coup
- Audit log des changements de role (qui a assigne quoi a qui — necessite backend R2.3)
- jwt-decode : remplacer le hack cookie par un endpoint dedie quand la backend feature `permissions/me` arrive

### Bugs Docmost detectes

Aucun bug bloquant. L'atom `authTokens` (`apps/client/src/features/auth/atoms/auth-tokens-atom.ts`) semble vestigial : in my tests il n'est pas set par les flows actuels (les tokens vont en cookie HttpOnly cote serveur via `setAuthCookie`). L'atom est conservé pour ne pas casser un eventuel flow OIDC / EE qui le consommerait.

### Verifications skipped

- `pnpm install` : pas execute (convention agent fork)
- TypeScript build : pas execute
- Tests Vitest : ecrits, runners non installes en local (devDeps ajoutes — Corentin install pour run)
- Lint ESLint : pas execute
- E2E manuel sur les pages : impossible sans backend en route + Postgres + Redis

---

## Patch 005 — R2.3a : Endpoint `GET /api/acadenice/permissions/me` + hook frontend propre

**Date** : 2026-05-07
**Scope** : 1 nouvel endpoint backend + refactor du hook `useAcadenicePermissions` pour consommer cet endpoint via React Query au lieu du hack jwt-decode sur cookie.
**Rationale** : R2.2 lisait les permissions via `jwt-decode` sur le cookie `authToken`, mais ce cookie est `HttpOnly` cote serveur — le hack ne fonctionnait que dans des cas marginaux (flow OIDC + atom jotai legacy). R2.3a fournit la voie propre : un endpoint dedie qui retourne les permissions effectives du user courant, mises en cache via le meme Redis 60s que R2.1. Le frontend consomme via React Query.

### Endpoint

```
GET /api/acadenice/permissions/me        (auth JWT)
```

Body :
```json
{
  "userId": "uuid",
  "workspaceId": "uuid",
  "permissions": ["pages:read", "rows:write", ...],
  "is_admin_wildcard": false
}
```

- Auth via `JwtAuthGuard` (deja en place sur `@Controller('acadenice/permissions')`)
- userId / workspaceId derives des decorators `@AuthUser` et `@AuthWorkspace` — anti-spoof : le caller ne peut pas forger un autre user
- Delegation a `AcadeniceRoleService.getUserPermissions` (cache Redis 60s, court-circuit `admin:*`)
- `is_admin_wildcard` est un boolean cheap pour eviter au front de scanner l'array

### Fichiers crees

| Fichier | Role |
|---------|------|
| `apps/server/src/core/acadenice/rbac/spec/permissions.controller.spec.ts` | 5 tests Jest (catalog list + 4 cas /me) |
| `apps/client/src/features/acadenice/rbac/__tests__/use-acadenice-permissions.test.tsx` | 3 tests Vitest (wildcard, admin:*, fallback OWNER pre-resolution) |

### Fichiers modifies (touches minimales)

| Fichier | Modification |
|---------|--------------|
| `apps/server/src/core/acadenice/rbac/controllers/permissions.controller.ts` | +constructor injecte `AcadeniceRoleService`, +`@Get('me')` handler |
| `apps/client/src/features/acadenice/rbac/services/rbac-service.ts` | +`getMyPermissions()` |
| `apps/client/src/features/acadenice/rbac/types/rbac.types.ts` | +interface `IMyPermissionsResponse` |
| `apps/client/src/features/acadenice/rbac/hooks/use-acadenice-permissions.ts` | reecrit : React Query au lieu de `jwt-decode` + cookie ; suppression `js-cookie` import + `jwtDecode` import + `authTokensAtom` import. Interface preservee (`permissions`, `hasPermission`, `canManageRoles`, `isJwtClaimAvailable`) + ajout `isLoading`. |
| `apps/client/src/features/acadenice/rbac/__tests__/roles-list.page.test.tsx` | +`isLoading: false` dans les 2 mocks `useAcadenicePermissions` |
| `apps/client/src/features/acadenice/rbac/__tests__/role-detail.page.test.tsx` | +`isLoading: false` dans le mock |

### Tests count

- Backend : 5 tests Jest sur `permissions.controller.spec.ts`
- Frontend : 3 tests Vitest sur le nouveau hook + 4 suites R2.2 maintenues vertes

### Edge cases couverts

- User sans aucun role -> `permissions: []`, `is_admin_wildcard: false`
- User avec `admin:*` -> court-circuit `["admin:*"]` + flag wildcard true
- Spoof attempt : userId/workspaceId viennent strictement des decorators auth, et non du body/query — le caller n'a pas de levier pour forger
- Erreur Redis -> propagee (le service R2.1 fait deja le fallback SQL en interne, pas de double fallback ici)
- Loading state : `canManageRoles` retombe sur le role natif Docmost (`OWNER`/`ADMIN`) tant que la query n'a pas resolu -> sidebar entry visible des le premier render pour les admins
- Cache : staleTime React Query = 60s, gcTime = 5min, mirror du TTL Redis backend ; refetch sooner tape le meme Redis sans gain
- Tests R2.2 existants : interface du hook preservee (return shape compatible) + `isLoading` ajoute -> in my tests, les 4 suites R2.2 restent vertes apres ajout du champ dans les 3 mocks affectes

### Hack supprime

L'ancien hook lisait le cookie `authToken` via `js-cookie` puis decodait avec `jwt-decode`. Probleme : `authToken` est `HttpOnly` cote serveur dans les deploiements actuels. Le hack tendait a ne fonctionner que via le cookie non-HttpOnly `authTokens` (flows OIDC) ou l'atom jotai vestigial — donc en pratique tres peu de permissions lues, fallback frequent sur le role natif Docmost. R2.3a remplace ca par la voie propre.

### TODO restants R2.3b (cote bridge formation-hub)

- Le bridge formation-hub continue de lire `acadenice_permissions[]` dans le claim JWT (R2.1) — pas affecte par R2.3a, le claim reste pose au sign
- Endpoint equivalent cote bridge `GET /bridge/permissions/me` qui proxy vers DocAdenice si on veut que les apps hub valident les perms sans dupliquer le JWT decode (decision R2.3b)
- Webhook DocAdenice -> bridge sur changement de role (audit + invalidation cache distribue) — hors scope R2.3a
- Endpoint `GET /api/acadenice/permissions/me/effective?for=<userId>` (admin) pour debug — pas demande, pas implemente

### Verifications skipped

- `pnpm install` / build / Jest run : convention agent fork (Corentin run)

---

---

## Patch 006 — R3.1.c : Extension Tiptap database-view + renderer table + slash `/database`

**Date** : 2026-05-08
**Scope** : extension Tiptap `database-view` (node atomique), renderer table lecture seule, slash command `/database` avec modal 2 etapes, SSE consumer React Query
**Rationale** : R3.1.a/b ont livre les endpoints bridge (views + SSE). R3.1.c branche la couche frontend : un node Tiptap inserrable via slash commande qui affiche une vue Baserow en read-only avec invalidation temps-reel SSE. Pattern "read-only first" : R3.1.d ajoutera edition inline, kanban et calendar.

### Fichiers crees

```
apps/client/src/features/acadenice/database-view/
  types/database-view.types.ts          — types TS (ViewType, DatabaseViewAttrs, BridgeTable/Row/Field/View...)
  services/bridge-client.ts             — axios wrapper bridge (auth cookie + singleton par URL)
  hooks/use-tables.ts                   — React Query : list tables
  hooks/use-views.ts                    — React Query : list views d'une table
  hooks/use-view-data.ts                — React Query : data paginee d'une view
  hooks/use-database-realtime-updates.ts — SSE consumer + invalidation React Query + backoff exp.
  renderers/table-renderer.tsx          — renderer table HTML (TanStack Table v8 migration-ready)
  renderers/table-renderer.module.css
  renderers/placeholder-renderer.tsx    — placeholder pour viewType non supportes (kanban, calendar)
  extension/database-view-extension.ts  — Tiptap Node : attrs, parseHTML, renderHTML, command
  extension/database-view-component.tsx — NodeViewWrapper dispatch viewType -> renderer
  extension/database-view.module.css
  slash-command/database-slash-command.tsx   — slash item descriptor + React root isolee
  slash-command/insert-database-modal.tsx    — modal Mantine 2 etapes (table -> view)
  slash-command/insert-database-modal.module.css
  index.ts                              — exports publics
  __tests__/database-view-extension.test.ts  — schema, attrs, parseHTML/renderHTML, command (7 tests)
  __tests__/database-view-component.test.tsx — NodeViewWrapper dispatch (5 tests)
  __tests__/table-renderer.test.tsx          — loading/error/empty/data/pagination (8 tests)
  __tests__/insert-database-modal.test.tsx   — modal step1/step2/insert/back (8 tests)
  __tests__/use-database-realtime-updates.test.ts — SSE hook (9 tests)
  __tests__/integration.test.tsx             — round-trip Editor schema/parse/serialize (4 tests)
```

### Fichiers modifies (patches upstream minimaux)

| Fichier | Lignes touchees | Modification |
|---------|----------------|--------------|
| `apps/client/src/features/editor/extensions/extensions.ts` | +2 import + +1 entree dans `mainExtensions[]` | Import `DatabaseViewExtension` + push dans l'array |
| `apps/client/src/features/editor/components/slash-menu/menu-items.ts` | +2 import + +3 lignes dans `CommandGroups` | Import `buildDatabaseSlashItem` + groupe `acadenice` en tete de CommandGroups |
| `apps/client/public/locales/en-US/translation.json` | +22 cles i18n | Cles `database_view.*` |
| `apps/client/public/locales/fr-FR/translation.json` | +22 cles i18n | Cles `database_view.*` (traduction FR) |

### Nouvelle dependance a installer (PAS installee — convention fork)

```
@tanstack/react-table@^8.21.0
```

A ajouter dans `apps/client/package.json` dependencies (pas devDeps — c'est du runtime).
Le renderer `table-renderer.tsx` contient un NOTE: expliquant la migration TanStack Table.
En attendant, le rendu est identique fonctionnellement (HTML table + colonnes de BridgeField[]).

### Choix techniques tranches

| Choix | Decision | Pourquoi |
|-------|----------|----------|
| TanStack Table v8 vs Mantine DataTable | TanStack (headless) | Controle total du markup, pas de couplage Mantine opaque |
| SSE EventSource auth | Cookie `withCredentials` natif | Bridge accepte JWT via cookie HttpOnly (R2.3b) ; meme-site en prod via Nginx proxy |
| EventSource polyfill | Non installe (noté) | Si JWT pas en cookie -> `event-source-polyfill` a ajouter (decision R3.1.d) |
| Modal multi-step | Custom stepper 2 etapes Mantine | Stepper Mantine v7 overkill pour 2 etapes ; custom plus light |
| slash command React root | `createRoot` isolee sur `document.body` | Pattern Docmost Excalidraw/Drawio — pas de prop-drilling depuis l'editeur |
| bridgeUrl per-instance | attr optionnel, fallback `VITE_BRIDGE_URL` | Multi-bridge possible, zero breaking change si non fourni |

### Points a debattre avec Corentin

1. **SSE meme-site** : si le bridge n'est pas servi sur le meme domaine que DocAdenice, il faut soit
   (a) un proxy Nginx `/api/bridge/*` -> bridge, soit (b) `event-source-polyfill` pour injecter un
   header `Authorization`. Decision R3.1.d.
2. **TanStack Table v8** : `pnpm add @tanstack/react-table` a faire avant de builder. Le code est
   ecrit pour la migration (voir NOTE: dans `table-renderer.tsx`).
3. **VITE_BRIDGE_URL** : variable d'env a ajouter dans `.env.local` (ex `http://localhost:4000`).
   Non bloquant pour les tests Vitest (hooks mockés).
4. **Slash group "acadenice"** : le groupe apparait en tete du slash menu. Si l'ordre est genant,
   deplacer l'entree dans le groupe `basic` a la position souhaitee.

### Tests

- 41 nouveaux tests Vitest (5 suites)
- Tests existants RBAC R2.x non touches
- Convention : hooks mockés au niveau du module via `vi.mock` — pas de MSW, pas de fetch reel

### Verifications skipped (convention fork)

- `pnpm install` : non execute
- `pnpm typecheck` : non execute (deps manquantes — `@tanstack/react-table` absent)
- `pnpm test` : non execute
- Lint : non execute

---

## Patch 007 — R3.1.d : Kanban + Calendar renderers + Inline edit

**Date** : 2026-05-08
**Scope** : renderers kanban (@dnd-kit) et calendar (@fullcalendar) + edition inline cellules (table) et cartes (kanban) + hook generique useUpdateRow + check permissions
**Commit** : `f3fae2a`

### Fichiers crees

| Fichier | Role |
|---------|------|
| `renderers/kanban-renderer.tsx` + `.module.css` | Renderer kanban avec @dnd-kit drag-drop, group by single_select, optimistic update |
| `renderers/calendar-renderer.tsx` + `.module.css` | Renderer calendar avec @fullcalendar month/week/day, eventDrop -> PATCH date |
| `hooks/use-update-row.ts` | PATCH row generique avec optimistic update + rollback React Query v5 |
| `hooks/use-permissions.ts` | Lecture acadenice_permissions depuis window global ou cookie acadenicePerms |
| `components/inline-editor.tsx` + `.module.css` | Editor polymorphe (text/number/date/single_select/multi_select) |
| `components/row-detail-modal.tsx` | Modal detail row ouverte depuis click event calendar |
| `__tests__/kanban-renderer.test.tsx` | 8 tests kanban |
| `__tests__/calendar-renderer.test.tsx` | 8 tests calendar (FullCalendar mocke) |
| `__tests__/inline-editor.test.tsx` | 7 tests inline editor |
| `__tests__/use-update-row.test.tsx` | 5 tests optimistic/rollback/endpoint/invalidation |
| `__tests__/use-permissions.test.tsx` | 5 tests permissions |

### Fichiers modifies (touches minimales)

| Fichier | Modification |
|---------|--------------|
| `extension/database-view-component.tsx` | switch/case dispatch vers KanbanRenderer + CalendarRenderer (au lieu de PlaceholderRenderer) |
| `renderers/table-renderer.tsx` | Integration InlineEditor sur double-click cellule + useUpdateRow + usePermissions |
| `services/bridge-client.ts` | Ajout helper `patchRow(tableId, rowId, payload, bridgeUrl)` |
| `types/database-view.types.ts` | SUPPORTED_VIEW_TYPES etendu : + "kanban" + "calendar" |
| `__tests__/database-view-component.test.tsx` | Mocks KanbanRenderer + CalendarRenderer, tests kanban/calendar -> real renderers, test unknown -> placeholder |
| `public/locales/en-US/translation.json` | +12 cles i18n (kanban.*, calendar.*, edit.*, row_detail.*) |
| `public/locales/fr-FR/translation.json` | +12 cles i18n traductions FR |

### Nouvelles dependances a installer

```
@dnd-kit/core@^6.3.1
@dnd-kit/sortable@^8.0.0
@dnd-kit/utilities@^3.2.2
@fullcalendar/react@^6.1.15
@fullcalendar/daygrid@^6.1.15
@fullcalendar/timegrid@^6.1.15
@fullcalendar/interaction@^6.1.15
```

### Tests count

- Avant R3.1.d : 41 tests (5 suites R3.1.c) + 22 tests RBAC (R2.x) = 63 tests total
- Apres R3.1.d : 63 + 33 nouveaux = 96 tests total (10 suites)

### Verifications skipped (convention fork)

- pnpm install : non execute (deps a ajouter listees ci-dessus)
- pnpm typecheck : non execute (deps FullCalendar + dnd-kit absentes)
- pnpm test : non execute
- Lint : non execute

### Points a debattre avec Corentin

1. **usePermissions global cache** : le hook lit `window.__acadenice_perms` qui n'est pas set par le code existant. Il faut que le hook RBAC R2.3a set ce global apres resolution. A connecter dans `use-acadenice-permissions.ts` : apres la query resoud, `window.__acadenice_perms = data.permissions`.
2. **KanbanRenderer drag-drop crosscolumn** : le DragEndEvent detecte la colonne cible via `closestCenter`. Si l'utilisateur drop dans une colonne vide (pas de card target), le `over.id` sera le column id (div), pas un row id. L'implementation actuelle cherche la colonne par `col.id === overRowId` — ca couvre ce cas. Mais si les IDs de colonnes collisionnent avec des IDs de rows Baserow (improbable mais possible), il faudrait un prefixe `col:` sur les IDs de colonnes.
3. **FullCalendar CSS** : `@fullcalendar/react` inclut son propre CSS (`@fullcalendar/common/main.css`). Il faut l'importer globalement dans l'app ou dans le composant. Le CSS Mantine-compat dans `calendar-renderer.module.css` override les styles FullCalendar mais ne les importe pas.
4. **@mantine/dates** : `DateInput` dans `inline-editor.tsx` vient de `@mantine/dates` qui necessite une installation separee (`pnpm add @mantine/dates`). A ajouter aux deps.

---

### TODO rebrand complet (futur)

- Logo SVG / favicon DocAdenice (actuellement reutilise `/icons/favicon-32x32.png` upstream)
- Manifest PWA (`apps/client/public/manifest.json`) : name, short_name, icons
- `apps/client/public/icons/` : pack d'icones Acadenice (16, 32, 192, 512, apple-touch)
- Palette couleur design system (theme Mantine custom)
- Eventuellement disable telemetry upstream par defaut (env var ou patch)
- Decider du sort de l'EE branding ("Powered by Docmost" sur les pages partagees publiques)
- Crowdin / i18n : ajouter une cle `appName` au lieu du hardcode et router via `getAppName()`
- Strategie : renommer le package npm `docmost` -> `docadenice` quand on aura un build pipeline custom complet (impacte trop d'imports actuellement)

---

## Patch 008 — R3.1.e : data-testid pour Playwright e2e

**Commit** : (local-only, branche `acadenice/main`)
**Date** : 2026-05-08
**Scope** : ajouts `data-testid` minimaux dans les renderers — aucune logique modifiee

### Fichiers modifies

#### `apps/client/src/features/acadenice/database-view/renderers/table-renderer.tsx`

- `<table>` : ajout `data-testid="table-renderer"` — permet a Playwright de cibler le renderer sans dependre de la structure CSS.
- `<td>` : ajout `data-testid={`cell-${row.id}-${field.name}`}` — permet de cibler une cellule specifique par row ID et nom de champ.

#### `apps/client/src/features/acadenice/database-view/components/inline-editor.tsx`

- Branche `!canWrite` : ajout `data-testid="inline-editor-readonly"` sur le `<span>` — permet aux tests RBAC de verifier que l'editeur est bien en lecture seule.
- Branche `default` (`TextInput`) : ajout `data-testid="inline-editor-input"` — permet de cibler l'input dans les tests d'edition inline.

#### `apps/client/src/features/acadenice/database-view/renderers/kanban-renderer.tsx`

- `KanbanCard` wrapper div : ajout `data-testid={`kanban-card-${row.id}`}` — ciblage par row ID pour les tests de drag.
- `KanbanColumn` wrapper div : ajout `data-testid={`kanban-column-${column.label}`}` — ciblage par label de colonne.
- Board div (DndContext child) : ajout `data-testid="kanban-board"` — detection de la presence du kanban dans la page.

#### `apps/client/src/features/acadenice/database-view/renderers/calendar-renderer.tsx`

- Root div du composant : ajout `data-testid="calendar-renderer"` — detection de la presence du calendrier.

#### `apps/client/Dockerfile.e2e` (nouveau)

- Cree pour docker-compose.e2e.yml : build Vite du client + serve via `serve@14` sur le port 5173.
- Utilise uniquement pour les e2e — pas de changement sur le Dockerfile de production.

### Raison des choix

- `data-testid` sur les elements conteneurs plutot que sur les elements internes — plus stable face aux refactors de structure interne.
- Pas de `data-testid` sur les elements Mantine (Button, Select, etc.) : ces composants ont leurs propres selectors d'accessibilite (role, aria-label) que Playwright prefere nativement.
- Aucun `data-testid` ajoute dans les hooks, services, ou extension Tiptap — non necessaire pour les assertions UI e2e.

### Tests impactes

7 scenarios e2e dans `e2e/tests/` utilisent ces testids :
- `database-view-insert.spec.ts` : `table-renderer`
- `database-view-edit-inline.spec.ts` : `cell-{rowId}-{fieldName}`, `inline-editor-input`
- `database-view-realtime-sse.spec.ts` : `table-renderer`
- `database-view-rbac-denied.spec.ts` : `inline-editor-readonly`
- `database-view-kanban-drag.spec.ts` : `kanban-board`, `kanban-column-{label}`, `kanban-card-{rowId}`
- `database-view-calendar-reschedule.spec.ts` : `calendar-renderer`

---

## Patch 009 — R3.2 : Backlinks bidirectionnels + extension Tiptap wikilinks

**Date** : 2026-05-08
**Commit** : `2fc310a`
**Scope** : Schema DB + module backend backlinks + extension Tiptap wikilinks + panel UI "Linked references"
**Rationale** : R3.2 permet a chaque page DocAdenice de savoir quelles autres pages y font reference. Deux vecteurs : wikilinks `[[Page Title]]` / `[[Page Title|alias]]` (nouveau) et mentions `@page` existantes (deja dans Docmost natif). L'indexation est async (fire-and-forget apres chaque save collaboratif) et idempotente (delete-then-insert par page source).

### Table SQL

`acadenice_backlink` (prefixe acadenice_, zero conflit upstream) :
- `id` UUID PK, `source_page_id` -> `pages.id` CASCADE, `target_page_id` -> `pages.id` CASCADE
- `link_type` VARCHAR(20) CHECK IN ('wikilink', 'mention', 'database_embed')
- `context_excerpt` TEXT (200 chars autour du lien pour preview UI)
- `workspace_id` -> `workspaces.id` CASCADE (scope guard)
- UNIQUE(source_page_id, target_page_id, link_type)
- 3 index: idx_backlink_target, idx_backlink_source, idx_backlink_workspace
- Migration up + down idempotente (ifNotExists)

### Fichiers crees (backend)

| Fichier | Role |
|---------|------|
| `apps/server/src/database/migrations/20260508T100000-create-acadenice-backlink.ts` | Migration Kysely up+down |
| `apps/server/src/core/acadenice/backlinks/backlinks.module.ts` | Module NestJS |
| `apps/server/src/core/acadenice/backlinks/services/backlink-parser.service.ts` | Walke Tiptap JSON, extrait wikilinks/mentions/databaseView, resoud titres via LIKE workspace-scoped |
| `apps/server/src/core/acadenice/backlinks/services/backlink-indexer.service.ts` | Idempotent reindex (delete source -> insert), skip self-refs + null targets |
| `apps/server/src/core/acadenice/backlinks/services/backlink.service.ts` | Lecture permission-aware (space_members / public visibility), groupe par link_type |
| `apps/server/src/core/acadenice/backlinks/controllers/backlinks.controller.ts` | GET /api/acadenice/pages/:pageId/backlinks (JwtAuthGuard) |
| `apps/server/src/core/acadenice/backlinks/events/page-content-updated.listener.ts` | @OnEvent listener -> reindex async, exporte ACADENICE_PAGE_CONTENT_UPDATED_EVENT |
| `apps/server/src/core/acadenice/backlinks/spec/backlink-parser.service.spec.ts` | 10 tests Vitest |
| `apps/server/src/core/acadenice/backlinks/spec/backlink-indexer.service.spec.ts` | 5 tests Vitest |
| `apps/server/src/core/acadenice/backlinks/spec/backlink.service.spec.ts` | 4 tests Vitest |
| `apps/server/src/core/acadenice/backlinks/spec/backlinks.controller.spec.ts` | 4 tests Vitest |

### Fichiers crees (frontend)

| Fichier | Role |
|---------|------|
| `apps/client/src/features/acadenice/wikilinks/extension/wikilink-extension.ts` | Tiptap Node inline+atom, attrs pageId/title/alias, Suggestion plugin [[, insertWikilink command, ReactNodeView (broken-link state) |
| `apps/client/src/features/acadenice/wikilinks/extension/wikilink-suggestion.ts` | Render callbacks floating-ui (pattern mention) |
| `apps/client/src/features/acadenice/wikilinks/extension/wikilink-list.tsx` | Popup suggestions pages via useSearchSuggestionsQuery |
| `apps/client/src/features/acadenice/wikilinks/extension/wikilink-list.module.css` | Styles popup |
| `apps/client/src/features/acadenice/wikilinks/__tests__/wikilink-extension.test.ts` | 9 tests Vitest (schema/attrs/commands/HTML) |
| `apps/client/src/features/acadenice/backlinks/queries/backlinks-query.ts` | useBacklinks(pageId) React Query (staleTime 30s) |
| `apps/client/src/features/acadenice/backlinks/components/linked-references-panel.tsx` | Panel accordeon groupe par link_type, excerpt, navigate source, empty/loading/error states |
| `apps/client/src/features/acadenice/backlinks/components/linked-references-panel.module.css` | Styles panel |
| `apps/client/src/features/acadenice/backlinks/__tests__/linked-references-panel.test.tsx` | 7 tests Vitest (RTL) |

### Fichiers modifies (patches upstream minimaux)

| Fichier | Modification |
|---------|--------------|
| `apps/server/src/collaboration/extensions/persistence.extension.ts` | +import EventEmitter2 + ACADENICE_PAGE_CONTENT_UPDATED_EVENT, +inject eventEmitter, +emit apres enqueuePageHistory |
| `apps/server/src/core/core.module.ts` | +import AcadeniceBacklinksModule, +1 entree imports[] |
| `apps/client/src/features/editor/extensions/extensions.ts` | +import WikilinkExtension, +1 entree mainExtensions[] |
| `apps/client/src/features/editor/full-editor.tsx` | +import LinkedReferencesPanel, +render Divider + panel apres MemoizedPageEditor |
| `apps/client/public/locales/en-US/translation.json` | +11 cles (backlinks.* + wikilink.*) |
| `apps/client/public/locales/fr-FR/translation.json` | +11 cles FR |

### Choix techniques tranches

| Choix | Decision |
|-------|----------|
| Wikilink syntax | [[Title]] et [[Title\|alias]] supportes (Obsidian style) |
| Resolution wikilink | LIKE case-insensitive exact sur `pages.title` dans le workspace. Ambiguite -> null (broken link). Pas de fuzzy. |
| Reindex strategy | Full reindex par page (delete-then-insert), idempotent. OK jusqu'a 10k pages. |
| Excerpt | 100 chars avant + 100 chars apres l'occurrence du titre. |
| Event debounce | Pas de debounce cote listener — Hocuspocus debounce deja la persistance en amont. Fire-and-forget. |
| Placement panel | Sticky bottom de la page (apres l'editeur dans full-editor.tsx), Divider de separation. |

### Tests count

- Backend: +23 tests Vitest (4 suites spec/)
- Frontend: +16 tests Vitest (wikilink-extension + linked-references-panel)
- Total cumule fork: 96 (R3.1.d) + 39 nouveaux = 135 tests

### Nouvelles dependances a installer (PAS installee — convention fork)

Aucune nouvelle dep pour R3.2. Le Tiptap Suggestion est fourni par `@tiptap/suggestion`
(deja en dependance de Docmost pour le systeme mention). floating-ui deja present.

### Verifications skipped (convention fork — Corentin run)

- `pnpm install` : non execute
- `pnpm typecheck` : non execute
- `pnpm test` : non execute
- `pnpm migration:up` : non execute (migration lisible et reversible, a tester)
- Lint : non execute

### Points a debattre avec Corentin

1. **Placement panel backlinks** : actuellement ajoute en bas de `full-editor.tsx` (dans le
   conteneur principal de la page). Alternatives : sidebar dedicee (panneau right) ou drawer
   Mantine. Deplacer sans changer la logique — LinkedReferencesPanel prend juste `pageId`.
2. **Resolution wikilink par pageId** : si l'utilisateur tape `[[Mon titre]]` et que le titre
   change plus tard, le lien devient broken. L'indexer re-resoud au prochain save de la page
   source. Pour stocker l'ID resolus des la saisie, il faudrait que la suggestion popup injecte
   `pageId` directement dans l'attr — c'est deja le cas (la selection dans WikilinkList passe
   `pageId`). Seuls les wikilinks saisis a la main sans passer par la popup auront `pageId=null`.
3. **Wikilink navigation** : le click navigue vers `/page/${pageId}`. DocAdenice utilise des URLs
   `/${spaceSlug}/page/${slugId}`. Il faudra un lookup ou un redirect dans App.tsx pour les pageIds
   sans slugId. Alternative : stocker slugId dans l'attr wikilink au moment de la suggestion.
4. **Index initial** : les pages existantes n'ont pas de backlinks indices. Un script one-shot
   `BacklinkIndexerService.reindexPage(pageId)` sur toutes les pages peut etre declenche
   manuellement ou via un endpoint admin `POST /api/acadenice/admin/backlinks/reindex`.

### TODO non bloquants

- Script/endpoint de reindex massif initial (pages pre-R3.2)
- data-testid sur LinkedReferencesPanel pour e2e Playwright (R3.1.e pattern)
- CSS global pour `.wikilink` et `.wikilink--broken` (actuellement inline dans NodeView)
- Pagination des backlinks si > 100 (rare mais possible)
- Endpoint `DELETE /api/acadenice/admin/backlinks/reindex` pour repartir de zero

---

## Patch 010 — R3.3 : Custom slash commands dynamiques

**Date** : 2026-05-08
**Commit** : `4e2af88`
**Scope** : Workspace admins peuvent declarer leurs propres commandes `/keyword` sans recompile. 5 action types. Nouvelle permission `slash_commands:manage` (catalogue 23 perms).
**Rationale** : Un workspace Notion-like doit permettre l'extensibilite du menu slash sans toucher au code. R3.3 livre un systeme complet admin-UI + runtime editor + securite webhook.

### Table SQL

`acadenice_slash_command` (prefixe acadenice_, zero conflit upstream) :
- `id` UUID PK, `workspace_id` FK CASCADE, `keyword` VARCHAR(50), `label` VARCHAR(100)
- `action_type` VARCHAR(20) CHECK IN ('insert-template','insert-table','embed-url','run-webhook','insert-snippet')
- `action_config` JSONB NOT NULL — payload specifique par action_type
- `is_enabled` BOOLEAN DEFAULT true, `created_by` FK users RESTRICT
- UNIQUE(workspace_id, keyword), INDEX idx_slash_workspace
- Migration up + down idempotente (ifNotExists)

### Fichiers crees (backend)

| Fichier | Role |
|---------|------|
| `apps/server/src/database/migrations/20260508T120000-create-acadenice-slash-command.ts` | Migration Kysely up+down |
| `apps/server/src/core/acadenice/slash-commands/slash-commands.module.ts` | Module NestJS |
| `apps/server/src/core/acadenice/slash-commands/dto/slash-command.dto.ts` | Zod schemas (discriminated union par action_type) + types TS |
| `apps/server/src/core/acadenice/slash-commands/services/action-validator.service.ts` | Validation JSONB config per action_type + webhook allowlist |
| `apps/server/src/core/acadenice/slash-commands/services/slash-command.service.ts` | CRUD : list/get/create/update/delete/toggle |
| `apps/server/src/core/acadenice/slash-commands/controllers/slash-commands.controller.ts` | GET (public auth) + POST/PATCH/DELETE (requires slash_commands:manage) |
| `apps/server/src/core/acadenice/slash-commands/spec/action-validator.service.spec.ts` | 13 tests Vitest (tous action types + allowlist) |
| `apps/server/src/core/acadenice/slash-commands/spec/slash-command.service.spec.ts` | 10 tests Vitest (CRUD + ConflictException + toggle) |
| `apps/server/src/core/acadenice/slash-commands/spec/slash-commands.controller.spec.ts` | 6 tests Vitest (routing + propagation exceptions) |

### Fichiers crees (frontend admin)

| Fichier | Role |
|---------|------|
| `apps/client/src/features/acadenice/slash-commands-admin/services/slash-commands-client.ts` | axios wrapper (list/get/create/update/delete/toggle) |
| `apps/client/src/features/acadenice/slash-commands-admin/queries/slash-commands-query.ts` | React Query hooks (list + CRUD mutations + optimistic toggle) |
| `apps/client/src/features/acadenice/slash-commands-admin/components/slash-command-list.tsx` | Table Mantine + toggle switch + edit/delete buttons |
| `apps/client/src/features/acadenice/slash-commands-admin/components/slash-command-form.tsx` | Modal create/edit polymorphe par action_type (form Mantine + validation) |
| `apps/client/src/features/acadenice/slash-commands-admin/pages/slash-commands-page.tsx` | Page `/settings/slash-commands` (admin seulement) |
| `apps/client/src/features/acadenice/slash-commands-admin/__tests__/slash-command-list.test.tsx` | 5 tests Vitest+RTL |
| `apps/client/src/features/acadenice/slash-commands-admin/__tests__/slash-commands-page.test.tsx` | 3 tests Vitest+RTL (access denied + admin view) |

### Fichiers crees (frontend runtime editor)

| Fichier | Role |
|---------|------|
| `apps/client/src/features/acadenice/slash-commands/hooks/use-custom-slash-commands.ts` | React Query hook (staleTime 2min, degradation graceful) |
| `apps/client/src/features/acadenice/slash-commands/executor/actionExecutor.ts` | Dispatch action_type -> editor command ou webhook fetch |
| `apps/client/src/features/acadenice/slash-commands/executor/buildCustomSlashItems.tsx` | Convertit SlashCommandDto[] -> SlashMenuItemType[] |
| `apps/client/src/features/acadenice/slash-commands/__tests__/use-custom-slash-commands.test.ts` | 4 tests Vitest |
| `apps/client/src/features/acadenice/slash-commands/__tests__/buildCustomSlashItems.test.ts` | 7 tests Vitest |

### Fichiers modifies (patches upstream minimaux)

| Fichier | Modification |
|---------|--------------|
| `apps/server/src/core/acadenice/rbac/permissions-catalog.ts` | +1 permission `slash_commands:manage` (catalogue 23 perms) |
| `apps/server/src/core/acadenice/rbac/services/seed.service.ts` | Admin role seed : +`slash_commands:manage` |
| `apps/server/src/core/core.module.ts` | +import + +1 entree `AcadeniceSlashCommandsModule` |
| `apps/client/src/features/editor/components/slash-menu/menu-items.ts` | `getSuggestionItems` accepte `customSlashItems?` — merged dans groupe 'acadenice' |
| `apps/client/src/components/settings/settings-sidebar.tsx` | +`IconSlash` import + 1 entree "Slash commands" (admin only) |
| `apps/client/src/App.tsx` | +import + +1 route `/settings/slash-commands` |
| `apps/client/public/locales/en-US/translation.json` | +52 cles `slash_commands.*` |
| `apps/client/public/locales/fr-FR/translation.json` | +52 cles `slash_commands.*` (traduction FR) |

### Securite webhook

- URL HTTPS obligatoire (HTTP -> 400)
- `ACADENICE_WEBHOOK_ALLOWLIST` env var : liste de prefixes autorises (optionnel, recommande en prod)
- Timeout 10s via `AbortController`
- `redirect: "error"` — aucun suivi de redirection
- Body cap 1 MB — pas de streaming illimite
- Auth headers (`Authorization`) NON transmis — les secrets doivent aller dans un proxy

### Extensibilite

Le discriminated union Zod (`actionConfigSchema`) permet d'ajouter un nouvel action_type en 3 etapes : 1. Ajouter le schema Zod, 2. Ajouter le case dans `executeAction`, 3. Ajouter le formulaire dans `SlashCommandForm`. Pas de recompile cote base de donnees (JSONB flexible).

### Tests count

- Backend : +29 tests Vitest (3 suites spec/)
- Frontend : +19 tests Vitest (4 suites)
- Total cumule fork : 135 (R3.2) + 48 nouveaux = **183 tests**

### Nouvelles dependances a installer

Aucune. Toutes les dependances sont deja presentes dans le monorepo Docmost :
- Zod (server)
- @mantine/core, @mantine/form, @mantine/notifications (client)
- @tanstack/react-query v5 (client)
- axios (client)

### Variables d'env nouvelles

| Var | Defaut | Role |
|-----|--------|------|
| `ACADENICE_WEBHOOK_ALLOWLIST` | (vide) | Prefixes URL autorises pour run-webhook, comma-separated. Si vide : tout HTTPS est accepte (avec log WARN). |

### Points a debattre avec Corentin

1. **customSlashItems integration dans SlashCommand extension** : `getSuggestionItems` accepte maintenant `customSlashItems?` mais la `SlashCommand` extension (slash-command.ts) appelle `getSuggestionItems` sans ce parametre. Pour que les custom commands apparaissent dans le menu runtime, il faut soit (a) passer `customSlashItems` via `editor.storage` (mis en place par un wrapper React autour de l'editeur qui appelle `useCustomSlashCommands`), soit (b) modifier la config Suggestion dans `extensions.ts` pour injecter le hook. Decision R3.4 au plus tard.
2. **Webhook en prod** : set `ACADENICE_WEBHOOK_ALLOWLIST` avec les prefixes autorises. Sans allowlist, tout HTTPS est accepte (convenable en dev, pas en prod).
3. **Icon resolution** : le champ `icon` stocke un string nom Tabler. Le runtime utilise `IconCommand` comme fallback universel. Une resolution dynamique par map (`{ IconNotes: IconNotes, ... }`) peut etre ajoutee dans `buildCustomSlashItems` si le besoin de customisation visuelle est fort.
4. **Pagination** : aucune pagination cote API (pas de `limit/offset`). Raisonnable jusqu'a ~200 commandes par workspace.

### TODO non bloquants

- Integration complete dans l'extension Suggestion (point 1 ci-dessus)
- Migration idempotente : la contrainte UNIQUE est appliquee via `ALTER TABLE ... ADD CONSTRAINT ... IF NOT EXISTS` avec un catch sur l'erreur si deja existante (certaines versions de Kysely ne supportent pas `ifNotExists` sur les contraintes)
- Audit log creation/modification/suppression de commandes
- data-testid sur les elements cles pour les tests e2e Playwright
