From 2c93e83e6b04eb28415fffa15003b09a80cd2339 Mon Sep 17 00:00:00 2001 From: matthieu Date: Sun, 15 Mar 2026 08:45:30 +0100 Subject: [PATCH] docs : add BookStack connector design spec Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-03-15-bookstack-connector-design.md | 295 ++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-15-bookstack-connector-design.md diff --git a/docs/superpowers/specs/2026-03-15-bookstack-connector-design.md b/docs/superpowers/specs/2026-03-15-bookstack-connector-design.md new file mode 100644 index 0000000..501eba1 --- /dev/null +++ b/docs/superpowers/specs/2026-03-15-bookstack-connector-design.md @@ -0,0 +1,295 @@ +# BookStack Connector — Design Spec + +**Date:** 2026-03-15 +**BookStack version:** v25.12.8 +**Pattern:** Mirror of Gitea connector + +## Overview + +Connecteur BookStack permettant de lier des documents (pages et livres) du wiki à des tâches Lesstime. Chaque projet peut être associé à une étagère (shelf) BookStack, et les utilisateurs peuvent rechercher et lier des pages/livres de cette étagère à leurs tâches. + +## Périmètre + +- Types liés : **pages** et **livres** (books) +- Niveau projet : liaison à une **étagère** (shelf) +- Niveau tâche : liaison à une ou plusieurs **pages/livres** de l'étagère du projet +- Recherche : filtrée dans l'étagère du projet uniquement +- Stockage : **référence** (titre + URL), pas d'aperçu du contenu +- Auth BookStack : Token ID + Token Secret (header `Authorization: Token {id}:{secret}`) + +## Backend + +### Entités + +#### BookStackConfiguration (singleton) + +```php +// src/Entity/BookStackConfiguration.php +class BookStackConfiguration +{ + private ?int $id; + private ?string $url = null; + private ?string $encryptedTokenId = null; + private ?string $encryptedTokenSecret = null; + + public function hasToken(): bool; // vérifie que les deux sont présents +} +``` + +- Chiffrement via `TokenEncryptor` existant (même pattern que Gitea) +- Repository avec `findSingleton()` + +#### TaskBookStackLink + +```php +// src/Entity/TaskBookStackLink.php +class TaskBookStackLink +{ + private ?int $id; + private Task $task; // ManyToOne, CASCADE on delete + private int $bookstackId; // ID dans BookStack + private string $bookstackType; // 'page' | 'book' + private string $title; // titre au moment du lien (cache) + private string $url; // URL complète + private \DateTimeImmutable $createdAt; +} +``` + +#### Project (extension) + +Ajout de deux champs : +- `bookstackShelfId` (nullable int) +- `bookstackShelfName` (nullable string) — cache du nom pour affichage + +### Service + +#### BookStackApiService + +```php +// src/Service/BookStackApiService.php +class BookStackApiService +{ + public function testConnection(): bool; + public function listShelves(): array; + public function searchInShelf(int $shelfId, string $query): array; + public function getPage(int $id): array; + public function getBook(int $id): array; +} +``` + +- Utilise `HttpClientInterface` (Symfony HttpClient) +- Auth : header `Authorization: Token {tokenId}:{tokenSecret}` +- Timeout : 10 secondes +- `testConnection()` : GET `/api/docs.json` +- `listShelves()` : GET `/api/shelves` (paginé, récupère toutes les pages) +- `searchInShelf()` : GET `/api/search?query={query}+{type:page|book}` puis filtre côté serveur par shelf ID +- `getPage()` : GET `/api/pages/{id}` +- `getBook()` : GET `/api/books/{id}` + +#### BookStackApiException + +```php +// src/Exception/BookStackApiException.php +class BookStackApiException extends \RuntimeException {} +``` + +### API Resources & Endpoints + +#### Admin + +| Méthode | Route | Ressource API Platform | Sécurité | +|---------|-------|----------------------|----------| +| GET | `/api/settings/bookstack` | BookStackSettings | ROLE_ADMIN | +| PUT | `/api/settings/bookstack` | BookStackSettings | ROLE_ADMIN | +| POST | `/api/settings/bookstack/test` | BookStackTestConnection | ROLE_ADMIN | + +**BookStackSettings** (DTO) : +- Read : `url`, `hasToken` +- Write : `url`, `tokenId`, `tokenSecret` + +**BookStackTestConnection** (DTO) : +- Read : `success` + +#### Projet + +| Méthode | Route | Ressource API Platform | Sécurité | +|---------|-------|----------------------|----------| +| GET | `/api/bookstack/shelves` | BookStackShelf | ROLE_ADMIN | + +**BookStackShelf** (DTO) : +- Read : `id`, `name` + +L'étagère sélectionnée est sauvée via le PATCH existant de Project (`bookstackShelfId`, `bookstackShelfName`). + +#### Tâche + +| Méthode | Route | Ressource API Platform | Sécurité | +|---------|-------|----------------------|----------| +| GET | `/api/tasks/{taskId}/bookstack/links` | BookStackLink | Authenticated | +| POST | `/api/tasks/{taskId}/bookstack/links` | BookStackLink | Authenticated | +| DELETE | `/api/tasks/{taskId}/bookstack/links/{id}` | BookStackLink | Authenticated | +| GET | `/api/tasks/{taskId}/bookstack/search?q=` | BookStackSearchResult | Authenticated | + +**BookStackLink** (DTO) : +- Read : `id`, `bookstackId`, `bookstackType`, `title`, `url`, `createdAt` +- Write : `bookstackId`, `bookstackType`, `title`, `url` + +**BookStackSearchResult** (DTO) : +- Read : `id`, `type`, `name`, `url` + +### State Providers / Processors + +| Classe | Rôle | +|--------|------| +| `BookStackSettingsProvider` | Lit config singleton, retourne DTO masqué | +| `BookStackSettingsProcessor` | Persiste config, chiffre tokens | +| `BookStackTestConnectionProvider` | Appelle `testConnection()` | +| `BookStackShelfProvider` | Appelle `listShelves()`, mappe en DTOs | +| `BookStackLinkProvider` | Lit `TaskBookStackLink` par task ID | +| `BookStackLinkProcessor` | POST : crée lien en DB / DELETE : supprime | +| `BookStackSearchResultProvider` | Appelle `searchInShelf()`, mappe en DTOs | + +### Migration + +```sql +CREATE TABLE bookstack_configuration ( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + url VARCHAR(255) DEFAULT NULL, + encrypted_token_id TEXT DEFAULT NULL, + encrypted_token_secret TEXT DEFAULT NULL +); + +CREATE TABLE task_bookstack_link ( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + task_id INT NOT NULL REFERENCES task(id) ON DELETE CASCADE, + bookstack_id INT NOT NULL, + bookstack_type VARCHAR(10) NOT NULL, + title VARCHAR(255) NOT NULL, + url VARCHAR(500) NOT NULL, + created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL +); + +CREATE INDEX IDX_task_bookstack_link_task_id ON task_bookstack_link (task_id); + +ALTER TABLE project ADD bookstack_shelf_id INT DEFAULT NULL; +ALTER TABLE project ADD bookstack_shelf_name VARCHAR(255) DEFAULT NULL; +``` + +### Variable d'environnement + +Réutilise la même clé de chiffrement que Gitea (`TokenEncryptor` existant) — pas besoin d'une nouvelle clé. + +## Frontend + +### Service + +```typescript +// frontend/services/bookstack.ts +export function useBookStackService() { + // Admin + async function getSettings(): Promise + async function saveSettings(payload: BookStackSettingsWrite): Promise + async function testConnection(): Promise + + // Projet + async function listShelves(): Promise + + // Tâche + async function getLinks(taskId: number): Promise + async function addLink(taskId: number, payload: BookStackLinkCreate): Promise + async function removeLink(taskId: number, linkId: number): Promise + async function search(taskId: number, query: string): Promise +} +``` + +### DTOs + +```typescript +// frontend/services/dto/bookstack.ts +type BookStackSettings = { url: string | null; hasToken: boolean } +type BookStackSettingsWrite = { url: string | null; tokenId: string | null; tokenSecret: string | null } +type BookStackTestResult = { success: boolean } +type BookStackShelf = { id: number; name: string } +type BookStackLink = { id: number; bookstackId: number; bookstackType: 'page' | 'book'; title: string; url: string; createdAt: string } +type BookStackLinkCreate = { bookstackId: number; bookstackType: 'page' | 'book'; title: string; url: string } +type BookStackSearchResult = { id: number; type: 'page' | 'book'; name: string; url: string } +``` + +### Composants + +#### AdminBookStackTab.vue + +Onglet admin (même pattern que `AdminGiteaTab.vue`) : +- Champs : URL, Token ID, Token Secret +- Bouton "Tester la connexion" avec indicateur résultat +- Indicateur "Token configuré" (ne montre jamais le token) +- Sauvegarde via `saveSettings()` + +#### ProjectDrawer.vue (extension) + +- Si BookStack est configuré : select pour choisir une étagère +- Charge `listShelves()` à l'ouverture +- Sauvegarde `bookstackShelfId` + `bookstackShelfName` sur le projet via PATCH + +#### TaskBookStackLinks.vue + +Petit composant intégré dans `TaskModal.vue`, visible directement : +- **Input de recherche** avec debounce (~300ms) → appel `search(taskId, query)` → dropdown résultats +- Chaque résultat : icône (page 📄 / livre 📕) + titre — clic pour ajouter +- **Liste des liens** sous le champ recherche : icône type + titre cliquable (ouvre BookStack dans nouvel onglet) + bouton × supprimer +- Affiché uniquement si le projet de la tâche a une shelf BookStack configurée +- Charge les liens existants au mount via `getLinks(taskId)` + +#### TaskModal.vue (extension) + +- Ajoute `` dans le modal, conditionné par `project.bookstackShelfId` +- Passe `taskId` et `projectId` en props + +## Fichiers à créer/modifier + +### Backend — Nouveaux fichiers + +``` +src/Entity/BookStackConfiguration.php +src/Entity/TaskBookStackLink.php +src/Repository/BookStackConfigurationRepository.php +src/Repository/TaskBookStackLinkRepository.php +src/Service/BookStackApiService.php +src/Exception/BookStackApiException.php +src/ApiResource/BookStackSettings.php +src/ApiResource/BookStackTestConnection.php +src/ApiResource/BookStackShelf.php +src/ApiResource/BookStackLink.php +src/ApiResource/BookStackSearchResult.php +src/State/BookStackSettingsProvider.php +src/State/BookStackSettingsProcessor.php +src/State/BookStackTestConnectionProvider.php +src/State/BookStackShelfProvider.php +src/State/BookStackLinkProvider.php +src/State/BookStackLinkProcessor.php +src/State/BookStackSearchResultProvider.php +migrations/VersionXXXX.php +``` + +### Backend — Fichiers modifiés + +``` +src/Entity/Project.php (ajout bookstackShelfId, bookstackShelfName) +``` + +### Frontend — Nouveaux fichiers + +``` +frontend/services/bookstack.ts +frontend/services/dto/bookstack.ts +frontend/components/admin/AdminBookStackTab.vue +frontend/components/task/TaskBookStackLinks.vue +``` + +### Frontend — Fichiers modifiés + +``` +frontend/components/task/TaskModal.vue (ajout TaskBookStackLinks) +frontend/components/project/ProjectDrawer.vue (ajout select étagère) +frontend/components/admin/ (ajout onglet BookStack dans la page admin) +```