Replace all @Controller('acadenice/...') decorators with 'v1/...' on 16 NestJS controllers. Update all client services, hooks, tests, extension-clipper, and doc comments to match. DB table names (acadenice_*) and folder structure untouched.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1411 lines
90 KiB
Markdown
1411 lines
90 KiB
Markdown
# 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
|