92 KiB
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) extractsentityType:"user"mention nodes viaextractUserMentions, queuesPAGE_MENTION_NOTIFICATIONjobs.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(nativeNotificationPrefcomponent in/settings/account/preferences).
R3.7 adds:
MentionDetectorService— pure service that walks Tiptap JSON and extracts user mentions (no DB). Used by the emitter and independently testable.NotificationEmitterService— listens toacadenice.page.content.updated(REST API save path, not collab) and queuesPAGE_MENTION_NOTIFICATION. Bridges the gap between the collab-only native detection and pages saved via REST (templates instantiate, import, etc.).AcadeniceNotificationsController— facade over nativeNotificationService, prefix/api/v1/notifications.NotificationPreferencesController— GET/PUT/api/v1/notification-preferences(reads/writes nativeusers.settings.notifications).- Frontend
/notificationspage — full inbox using nativeNotificationItemcomponent. - Frontend
/settings/notificationspreferences 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
- REST path mention dedup :
NotificationEmitterServicepasseoldMentionedUserIds: []— toutes les mentions existantes sont reenvoyees a chaque save REST. La nativeprocessPageMentionfiltre 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 payloadacadenice.page.content.updated. Patch mineur post-R3.7. - Collab path vs REST path : en collab (hocuspocus), la detection est deja faite par
PersistenceExtension. En REST, leNotificationEmitterServiceprend le relais. Les deux peuvent coexister sans conflit car la deduplication native se base suroldMentionedUserIds(diff). Mais si une page est sauvee en collab ET en REST dans la meme session, un doublon est possible. Acceptable pour v1. - SSE optionnel : le bell count est actuellement poll 30s. Si Corentin veut sub-seconde, brancher le bridge SSE (R3.7+ — note dans SESSION-RESUME).
- 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 tableacadenice_notification_preferencesdediee. 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
- 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.
- 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+.
- Permissions row comments : actuellement pas de check explicite
comments:writedans le controller — tout user authentifie avec acces workspace peut commenter. AjouterRequirePermission('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
- Positionnement instantiate dans l'arbre : la methode
getNextPagePositionutilise une approche simple (append suffixe '0'). Pour une integration propre dans le Tiptap tree, il faudra utiliserfractional-indexing-jitteredcomme le faitPageService. Patch mineur post-R3.6. - Instantiate depuis le picker : le custom event
acadenice:open-template-pickerdispatche surdocument. Cela fonctionne si le composant ecoutant (ex. Page.tsx) subscribe au mount. Alternativement, utiliser un atom jotai globaltemplatePickerOpenAtompour plus de robustesse. Bonne option pour R3.7 refactor. - Cover image :
cover_urlest optionnel (URL externe). Pas d'upload prevu dans ce sub-bloc. Si un template a une cover, elle s'affiche en CSSbackground-image— a implementer dans la card si Corentin le souhaite. - 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 :
useNavigatevers/p/{slugId}(slugId map a etendre quand le backend incluraslugIddansGraphNode).
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.
spaceColorCachemodule-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 : ajouterslugId?: stringa GraphNode.
Point a debattre avec Corentin
- slugId dans GraphNode : faut-il enrichir le backend (R3.5.1) pour inclure
slugIddans chaque node ? Permettrait l'activation du bouton "Open page" et la navigation double-click. Patch mineur service + DTO cote backend. - 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). - 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-workspacespaceId: optionnel, filtre les nodes au spacepageId: optionnel, centre le graphe + BFS depth hopsdepth: 1-5, default 2types: filtre par type de lien (defaut: tous les 3)includeOrphans: default false
Reponse
{
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-clientv6 via laConfigurationcachee) - userInfo refetched apres l'echange — on ne fait pas confiance aux claims ID token seuls pour
email - Cookies temporaires
oidc_state/oidc_pkceclear 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
enforceSsocote workspace (actuellement le bouton apparait des queOIDC_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 composeeacadenice_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 parJwtAuthGuard) - Resoud les permissions via
AcadeniceRoleService.getUserPermissions(cache Redis 60s) - Match avec wildcard support :
admin:*couvre tout,<group>:*couvre<group>:<action> - Throw
ForbiddenExceptionsi 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
DELla cle du user. Sur mutation de role/permission, onSCAN+DELtoutes les cles du workspace (pas deKEYS *bloquant) - Service
AcadeniceRoleServicerendu 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 NotFoundvalidation prealable atomique - Assignation duplique du meme role -> idempotent (
ON CONFLICT DO NOTHING) - Permission inconnue dans une payload ->
400 BadRequestavec liste autorisee - Migration re-run sur DB partiellement migree ->
ifNotExistsevite 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.createpour seeder en live (R2.2 ou R2.3) - Endpoint
POST /api/acadenice/permissions/mepour 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
authTokenest en cookie HttpOnly cote serveur (impossible a lire en JS) ; on tente le cookie non-HttpOnlyauthTokenset 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:managemanque 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@/srcapps/client/src/test-setup.ts— stubsmatchMedia+ResizeObserver(Mantine en a besoin)apps/client/package.json— scriptstest+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
Loadercentre dans chaque page - Error state :
Alert+ bouton Retry qui appellerefetch - 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 :
UserRolesPaneln'auto-modifie pas le user (le backend rejette de toute facon — l'UI ne tente pas) - Permission preview : se desactive si
canMutate=falsecar les callsgetRolenecessitentroles:manage - Dirty tracking : boutons Save/Discard se desactivent si les drafts == server state (compare ensembles tries)
- A11y :
aria-labelsur tous les inputs / icon buttons,Helmettitres,aria-live="polite"sur Alerts d'etat
TODO laisses (non bloquants R2.2)
- Endpoint backend
GET /api/acadenice/permissions/mepour 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
WorkspaceMembersTableexistante (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/mearrive
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 :
{
"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
@AuthUseret@AuthWorkspace— anti-spoof : le caller ne peut pas forger un autre user - Delegation a
AcadeniceRoleService.getUserPermissions(cache Redis 60s, court-circuitadmin:*) is_admin_wildcardest 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 :
canManageRolesretombe 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) +
isLoadingajoute -> 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/mequi 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
- 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-polyfillpour injecter un headerAuthorization. Decision R3.1.d. - TanStack Table v8 :
pnpm add @tanstack/react-tablea faire avant de builder. Le code est ecrit pour la migration (voir NOTE: danstable-renderer.tsx). - VITE_BRIDGE_URL : variable d'env a ajouter dans
.env.local(exhttp://localhost:4000). Non bloquant pour les tests Vitest (hooks mockés). - Slash group "acadenice" : le groupe apparait en tete du slash menu. Si l'ordre est genant,
deplacer l'entree dans le groupe
basica 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 executepnpm typecheck: non execute (deps manquantes —@tanstack/react-tableabsent)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
- usePermissions global cache : le hook lit
window.__acadenice_permsqui n'est pas set par le code existant. Il faut que le hook RBAC R2.3a set ce global apres resolution. A connecter dansuse-acadenice-permissions.ts: apres la query resoud,window.__acadenice_perms = data.permissions. - KanbanRenderer drag-drop crosscolumn : le DragEndEvent detecte la colonne cible via
closestCenter. Si l'utilisateur drop dans une colonne vide (pas de card target), leover.idsera le column id (div), pas un row id. L'implementation actuelle cherche la colonne parcol.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 prefixecol:sur les IDs de colonnes. - FullCalendar CSS :
@fullcalendar/reactinclut son propre CSS (@fullcalendar/common/main.css). Il faut l'importer globalement dans l'app ou dans le composant. Le CSS Mantine-compat danscalendar-renderer.module.cssoverride les styles FullCalendar mais ne les importe pas. - @mantine/dates :
DateInputdansinline-editor.tsxvient de@mantine/datesqui 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.pngupstream) - 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
appNameau lieu du hardcode et router viagetAppName() - Strategie : renommer le package npm
docmost->docadenicequand 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>: ajoutdata-testid="table-renderer"— permet a Playwright de cibler le renderer sans dependre de la structure CSS.<td>: ajoutdata-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: ajoutdata-testid="inline-editor-readonly"sur le<span>— permet aux tests RBAC de verifier que l'editeur est bien en lecture seule. - Branche
default(TextInput) : ajoutdata-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
KanbanCardwrapper div : ajoutdata-testid={kanban-card-${row.id}}— ciblage par row ID pour les tests de drag.KanbanColumnwrapper div : ajoutdata-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@14sur le port 5173. - Utilise uniquement pour les e2e — pas de changement sur le Dockerfile de production.
Raison des choix
data-testidsur les elements conteneurs plutot que sur les elements internes — plus stable face aux refactors de structure interne.- Pas de
data-testidsur les elements Mantine (Button, Select, etc.) : ces composants ont leurs propres selectors d'accessibilite (role, aria-label) que Playwright prefere nativement. - Aucun
data-testidajoute 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-rendererdatabase-view-edit-inline.spec.ts:cell-{rowId}-{fieldName},inline-editor-inputdatabase-view-realtime-sse.spec.ts:table-rendererdatabase-view-rbac-denied.spec.ts:inline-editor-readonlydatabase-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) :
idUUID PK,source_page_id->pages.idCASCADE,target_page_id->pages.idCASCADElink_typeVARCHAR(20) CHECK IN ('wikilink', 'mention', 'database_embed')context_excerptTEXT (200 chars autour du lien pour preview UI)workspace_id->workspaces.idCASCADE (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 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 executepnpm typecheck: non executepnpm test: non executepnpm migration:up: non execute (migration lisible et reversible, a tester)- Lint : non execute
Points a debattre avec Corentin
- 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 justepageId. - 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 injectepageIddirectement dans l'attr — c'est deja le cas (la selection dans WikilinkList passepageId). Seuls les wikilinks saisis a la main sans passer par la popup aurontpageId=null. - 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. - 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 adminPOST /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
.wikilinket.wikilink--broken(actuellement inline dans NodeView) - Pagination des backlinks si > 100 (rare mais possible)
- Endpoint
DELETE /api/acadenice/admin/backlinks/reindexpour 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) :
idUUID PK,workspace_idFK CASCADE,keywordVARCHAR(50),labelVARCHAR(100)action_typeVARCHAR(20) CHECK IN ('insert-template','insert-table','embed-url','run-webhook','insert-snippet')action_configJSONB NOT NULL — payload specifique par action_typeis_enabledBOOLEAN DEFAULT true,created_byFK 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_ALLOWLISTenv 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
- customSlashItems integration dans SlashCommand extension :
getSuggestionItemsaccepte maintenantcustomSlashItems?mais laSlashCommandextension (slash-command.ts) appellegetSuggestionItemssans ce parametre. Pour que les custom commands apparaissent dans le menu runtime, il faut soit (a) passercustomSlashItemsviaeditor.storage(mis en place par un wrapper React autour de l'editeur qui appelleuseCustomSlashCommands), soit (b) modifier la config Suggestion dansextensions.tspour injecter le hook. Decision R3.4 au plus tard. - Webhook en prod : set
ACADENICE_WEBHOOK_ALLOWLISTavec les prefixes autorises. Sans allowlist, tout HTTPS est accepte (convenable en dev, pas en prod). - Icon resolution : le champ
iconstocke un string nom Tabler. Le runtime utiliseIconCommandcomme fallback universel. Une resolution dynamique par map ({ IconNotes: IconNotes, ... }) peut etre ajoutee dansbuildCustomSlashItemssi le besoin de customisation visuelle est fort. - 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 EXISTSavec un catch sur l'erreur si deja existante (certaines versions de Kysely ne supportent pasifNotExistssur les contraintes) - Audit log creation/modification/suppression de commandes
- data-testid sur les elements cles pour les tests e2e Playwright
Patch R5.3 — OpenAPI Swagger documentation auto-generee
Date : 2026-05-08
Commit : 21ce2a9
Scope : apps/server/src/main.ts, tous les controllers apps/server/src/core/acadenice/*/controllers/*.controller.ts, docs/api-docs.md
Packages installes
@nestjs/swagger@^11.4.2— SwaggerModule + DocumentBuilder + decoratorsnestjs-zod@^5.3.0—cleanupOpenApiDoc()pour post-processing du doc OpenAPI generee (compatible Zod v4)
Architecture
main.ts: SwaggerModule bootstrappe apres app.enableCors(). Active uniquement siNODE_ENV !== 'production'OUSWAGGER_ENABLED=true. Expose/api/docs(UI) et/api/docs-json(OpenAPI JSON).- 16 controllers decores :
@ApiTags,@ApiBearerAuth,@ApiOperation,@ApiResponse(tous status codes),@ApiParam,@ApiQuery,@ApiBodysur chaque methode. - Tags : templates, sync-blocks, audit-log, api-keys, clipper, graph, rbac, comments, notifications, security, backlinks, slash-commands.
cleanupOpenApiDoc()de nestjs-zod applique le post-processing standard avantSwaggerModule.setup().
Tests
src/core/acadenice/swagger.spec.ts — 8 tests :
- DocumentBuilder produit un objet OpenAPI 3 valide
- Les 12 tags attendus sont tous declares
- BearerAuth + CookieAuth security schemes configures
- @ApiTags present sur RowCommentsController
- @ApiTags present sur SyncBlocksController
- @ApiTags present sur AcadenicePermissionsController
- @ApiTags present sur BacklinksController
- @ApiTags present sur AcadeniceOidcStatusController
Acces
GET /api/docs # Swagger UI (dev/staging)
GET /api/docs-json # OpenAPI 3 JSON spec
Voir docs/api-docs.md pour generation de SDK client.