From b2e6250ff50fbc4553c35e15f6404850becdda17 Mon Sep 17 00:00:00 2001 From: Nicolas Cantu Date: Wed, 7 Jan 2026 01:51:26 +0100 Subject: [PATCH] lint fix wip --- .cursor/rules/quality.mdc | 75 +++- components/ArticleEditor.tsx | 3 +- components/ArticleEditorForm.tsx | 6 +- components/ArticleFormButtons.tsx | 31 +- components/KeyManagementManager.tsx | 4 +- components/NotificationBadgeButton.tsx | 9 +- components/NotificationContent.tsx | 2 +- components/NotificationItem.tsx | 7 +- components/NotificationPanel.tsx | 2 +- components/PageHeader.tsx | 2 - components/RelayManager.tsx | 5 + docs/architecture-implementation-status.md | 105 +++++ docs/architecture-principles.md | 84 ++++ docs/service-worker-sync.md | 113 +++++ hooks/useArticlePublishing.ts | 6 + hooks/useArticles.ts | 36 +- hooks/useNotificationCenter.ts | 2 +- hooks/useNotifications.ts | 93 ++-- hooks/useUserArticles.ts | 48 +- lib/articlePublisher.ts | 7 +- lib/articlePublisherHelpersPresentation.ts | 32 +- lib/articlePublisherPublish.ts | 45 +- lib/articlePublisherTypes.ts | 2 + lib/articleQueries.ts | 69 +-- lib/authorQueries.ts | 115 ++--- lib/configStorageTypes.ts | 1 + lib/metadataExtractor.ts | 16 +- lib/nostr.ts | 273 +++++++++--- lib/notificationDetector.ts | 288 ++++++++++++ lib/notificationReader.ts | 82 ++++ lib/notificationService.ts | 345 +++++++++++++++ lib/objectCache.ts | 155 ++++++- lib/platformSync.ts | 73 +++- lib/platformTracking.ts | 126 ++++-- lib/publishLog.ts | 266 +++++++++++ lib/publishResult.ts | 29 ++ lib/publishWorker.ts | 226 ++++++++++ lib/purchaseQueries.ts | 201 +-------- lib/reviewTipQueries.ts | 208 +-------- lib/reviews.ts | 82 +--- lib/seriesQueries.ts | 151 +------ lib/sponsoringQueries.ts | 158 +------ lib/sponsoringTracking.ts | 27 +- lib/swClient.ts | 221 ++++++++++ lib/swSyncHandler.ts | 185 ++++++++ lib/userContentSync.ts | 39 +- lib/websocketService.ts | 277 ++++++++++++ lib/writeOrchestrator.ts | 144 ++++++ lib/writeService.ts | 301 +++++++++++++ pages/_app.tsx | 36 +- pages/api/nip95-upload.ts | 8 +- pages/author/[pubkey].tsx | 9 +- pages/series/[id].tsx | 4 +- public/locales/en.txt | 1 + public/locales/fr.txt | 1 + public/sw.js | 293 +++++++++++++ public/writeWorker.js | 484 +++++++++++++++++++++ update-summary.md | 43 -- 58 files changed, 4439 insertions(+), 1217 deletions(-) create mode 100644 docs/architecture-implementation-status.md create mode 100644 docs/architecture-principles.md create mode 100644 docs/service-worker-sync.md create mode 100644 lib/notificationDetector.ts create mode 100644 lib/notificationReader.ts create mode 100644 lib/notificationService.ts create mode 100644 lib/publishLog.ts create mode 100644 lib/publishResult.ts create mode 100644 lib/publishWorker.ts create mode 100644 lib/swClient.ts create mode 100644 lib/swSyncHandler.ts create mode 100644 lib/websocketService.ts create mode 100644 lib/writeOrchestrator.ts create mode 100644 lib/writeService.ts create mode 100644 public/sw.js create mode 100644 public/writeWorker.js delete mode 100644 update-summary.md diff --git a/.cursor/rules/quality.mdc b/.cursor/rules/quality.mdc index 9723a63..97462b1 100644 --- a/.cursor/rules/quality.mdc +++ b/.cursor/rules/quality.mdc @@ -456,6 +456,78 @@ Aucune bibliothèque ne doit être introduite si une capacité équivalente exis Les noms, interfaces, types et contrats doivent être stables, explicites et orientés intention. +### Architecture logicielle et séparation des responsabilités + +L'architecture du projet suit un modèle strict de séparation des responsabilités et d'isolation des couches. Toute modification doit respecter ces principes architecturaux fondamentaux. + +#### Persistance des données + +* **IndexedDB comme source de vérité locale** : Toutes les données sont persistées dans IndexedDB côté utilisateur. IndexedDB constitue la source de vérité locale pour toutes les données de l'application. + +* **IndexedDB en lecture uniquement pour l'interface** : L'interface utilisateur (composants React, pages) utilise uniquement IndexedDB en lecture. Aucune écriture directe dans IndexedDB ne doit être effectuée depuis l'interface utilisateur. + +* **Écritures via le réseau Nostr** : Toutes les écritures et mises à jour de données passent par le réseau Nostr. L'interface ne doit jamais écrire directement dans IndexedDB. + +* **Statut de publication** : Toute écriture en base de données (IndexedDB) doit inclure un statut `published` qui permet de gérer ultérieurement la publication (ok/ko). Ce statut permet de suivre l'état de synchronisation et de publication des données. + +#### Service Workers et tâches de fond + +* **Service Workers pour les tâches de fond** : Toutes les tâches de fond sur les données sont effectuées avec des Service Workers. Les Service Workers gèrent la synchronisation, la mise à jour et le traitement asynchrone des données. + +* **Service Worker pour les notifications** : Un Service Worker alimente la table des notifications. Il surveille les événements et met à jour IndexedDB avec les nouvelles notifications. + +* **Détection indépendante des changements** : Le Service Worker pour les notifications détecte les changements de manière indépendante. Il n'est pas dépendant du service d'écriture et ne reçoit pas d'événements directs de celui-ci. Il surveille IndexedDB et détecte les nouvelles notifications par lui-même. + +* **Service de lecture des notifications** : Les notifications sont lues par un service dédié qui détecte les nouvelles notifications dans la base de données (IndexedDB). Ce service est séparé du Service Worker qui alimente les notifications. + +#### Web Workers et écritures + +* **Web Worker d'écriture** : Un Web Worker dédié gère toutes les écritures et modifications dans IndexedDB. L'interface utilisateur ne doit jamais écrire directement dans IndexedDB, mais doit passer par ce Web Worker. + +* **Accès aux données** : Le Web Worker d'écriture reçoit les données complètes, pas seulement les modifications. Les données partielles ou différentielles ne doivent pas être utilisées. + +* **Transactions multi-tables** : Le Web Worker gère les transactions multi-tables via plusieurs transactions. La logique de découpage des transactions est gérée côté Web Worker, pas côté appelant. + +* **Synchronisation et gestion des conflits** : Le Web Worker est conçu pour gérer une pile d'écritures. Il traite les écritures de manière séquentielle pour éviter les conflits lorsque plusieurs écritures arrivent simultanément. Aucune écriture ne doit contourner cette pile. + +* **Service de gestion des écritures** : Un service gère les écritures/mises à jour vers les WebSockets/API et vers le Web Worker d'écriture. Ce service orchestre la communication entre l'interface, le réseau (Nostr/WebSockets) et le Web Worker d'écriture. + +* **Orchestration** : Le service de gestion des écritures coordonne le flux WebSockets → Web Worker → IndexedDB. Il orchestre les écritures réseau et locales de manière indépendante. + +* **Ordre des opérations** : Les écritures réseau et locales sont effectuées en parallèle et de manière indépendante. Il n'y a pas d'ordre imposé entre l'écriture réseau et l'écriture locale. + +* **Gestion des erreurs** : Si le réseau échoue mais que l'écriture locale réussit, aucune action immédiate n'est requise. Un autre Service Worker réessaiera la publication ultérieurement en se basant sur le statut `published` des données en IndexedDB. + +#### WebSockets et communication + +* **Service de gestion des WebSockets** : Les WebSockets sont gérés par un service dédié qui communique avec les Service Workers. Ce service isole la logique de communication WebSocket et permet une gestion centralisée des connexions. + +* **Rôle principal et secondaires** : Le service WebSocket a pour rôle principal la gestion des connexions. Ses rôles secondaires incluent le routage des messages et la gestion de l'état de connexion. + +* **Communication avec les Service Workers** : La communication entre le service WebSocket et les Service Workers s'effectue via `postMessage`. Aucun autre mécanisme de communication ne doit être utilisé. + +* **Gestion des reconnexions** : Le service WebSocket doit gérer les reconnexions automatiques et maintenir l'état de connexion. Les reconnexions doivent être transparentes pour les Service Workers. + +* **Séparation des responsabilités** : Le service WebSocket ne doit pas écrire directement dans IndexedDB. Il communique avec les Service Workers qui gèrent la persistance. + +#### Principes de respect de l'architecture + +Toute modification doit respecter ces principes : + +* **Pas d'écriture directe dans IndexedDB depuis l'interface** : L'interface utilisateur ne doit jamais écrire directement dans IndexedDB. Toutes les écritures passent par le réseau Nostr et le Web Worker d'écriture. + +* **Séparation stricte des couches** : Les couches doivent rester isolées : + + * Interface utilisateur (React) : Lecture IndexedDB uniquement + * Service de gestion des écritures : Orchestration des écritures réseau et Web Worker + * Web Worker d'écriture : Écritures dans IndexedDB + * Service Workers : Tâches de fond, synchronisation, notifications + * Service WebSocket : Communication réseau avec Service Workers + +* **Statut de publication obligatoire** : Toute écriture en base doit inclure un statut `published` pour le suivi de la publication. + +* **Pas de contournement** : Aucun contournement de cette architecture ne doit être introduit, même pour des cas particuliers. Les exceptions doivent être gérées dans le cadre de cette architecture. + ### Validation et précaution obligatoires Avant toute implémentation ou modification, certaines décisions critiques doivent être validées explicitement par l'utilisateur. @@ -487,7 +559,8 @@ Sont concernés : * **Contrats d'API** : Modifications des structures de données échangées via API * **Formats de stockage** : Changements de format de fichiers, structures JSON, schémas de validation -Toute modification du modèle de données doit être documentée avec : +**Toute modification du modèle de données doit être documentée avec :** + * L'impact sur les données existantes * Les migrations nécessaires * Les risques de régression diff --git a/components/ArticleEditor.tsx b/components/ArticleEditor.tsx index 9ebb2aa..49ad3a5 100644 --- a/components/ArticleEditor.tsx +++ b/components/ArticleEditor.tsx @@ -24,7 +24,7 @@ function SuccessMessage(): React.ReactElement { export function ArticleEditor({ onPublishSuccess, onCancel, seriesOptions, onSelectSeries, defaultSeriesId }: ArticleEditorProps): React.ReactElement { const { connected, pubkey, connect } = useNostrAuth() - const { loading, error, success, publishArticle } = useArticlePublishing(pubkey ?? null) + const { loading, error, success, relayStatuses, publishArticle } = useArticlePublishing(pubkey ?? null) const [draft, setDraft] = useState({ title: '', preview: '', @@ -50,6 +50,7 @@ export function ArticleEditor({ onPublishSuccess, onCancel, seriesOptions, onSel }} loading={loading} error={error} + relayStatuses={relayStatuses} {...(onCancel ? { onCancel } : {})} {...(seriesOptions ? { seriesOptions } : {})} {...(onSelectSeries ? { onSelectSeries } : {})} diff --git a/components/ArticleEditorForm.tsx b/components/ArticleEditorForm.tsx index 8644b42..ed0d943 100644 --- a/components/ArticleEditorForm.tsx +++ b/components/ArticleEditorForm.tsx @@ -9,12 +9,15 @@ import { MarkdownEditorTwoColumns } from './MarkdownEditorTwoColumns' import type { MediaRef } from '@/types/nostr' import { t } from '@/lib/i18n' +import type { RelayPublishStatus } from '@/lib/publishResult' + interface ArticleEditorFormProps { draft: ArticleDraft onDraftChange: (draft: ArticleDraft) => void onSubmit: (e: React.FormEvent) => void loading: boolean error: string | null + relayStatuses?: RelayPublishStatus[] onCancel?: () => void seriesOptions?: { id: string; title: string }[] | undefined onSelectSeries?: ((seriesId: string | undefined) => void) | undefined @@ -249,6 +252,7 @@ export function ArticleEditorForm({ onSubmit, loading, error, + relayStatuses, onCancel, seriesOptions, onSelectSeries, @@ -266,7 +270,7 @@ export function ArticleEditorForm({ - + ) } diff --git a/components/ArticleFormButtons.tsx b/components/ArticleFormButtons.tsx index d786d70..434e968 100644 --- a/components/ArticleFormButtons.tsx +++ b/components/ArticleFormButtons.tsx @@ -2,28 +2,31 @@ import { t } from '@/lib/i18n' interface ArticleFormButtonsProps { loading: boolean + relayStatuses?: unknown // Kept for backward compatibility but not displayed onCancel?: () => void } export function ArticleFormButtons({ loading, onCancel }: ArticleFormButtonsProps): React.ReactElement { return ( -
- - {onCancel && ( +
+
- )} + {onCancel && ( + + )} +
) } diff --git a/components/KeyManagementManager.tsx b/components/KeyManagementManager.tsx index c349346..ed58218 100644 --- a/components/KeyManagementManager.tsx +++ b/components/KeyManagementManager.tsx @@ -59,7 +59,7 @@ export function KeyManagementManager(): React.ReactElement { const urlObj = new URL(url) // Check if it's a nostr:// URL with nsec if (urlObj.protocol === 'nostr:' || urlObj.protocol === 'nostr://') { - const path = urlObj.pathname || urlObj.href.replace(/^nostr:?\/\//, '') + const path = urlObj.pathname ?? urlObj.href.replace(/^nostr:?\/\//, '') if (path.startsWith('nsec')) { return path } @@ -329,7 +329,7 @@ export function KeyManagementManager(): React.ReactElement { ) diff --git a/components/NotificationContent.tsx b/components/NotificationContent.tsx index f5e7a40..27bfe4a 100644 --- a/components/NotificationContent.tsx +++ b/components/NotificationContent.tsx @@ -1,5 +1,5 @@ import Link from 'next/link' -import type { Notification } from '@/types/notifications' +import type { Notification } from '@/lib/notificationService' interface NotificationContentProps { notification: Notification diff --git a/components/NotificationItem.tsx b/components/NotificationItem.tsx index adda796..c4d5079 100644 --- a/components/NotificationItem.tsx +++ b/components/NotificationItem.tsx @@ -1,4 +1,4 @@ -import type { Notification } from '@/types/notifications' +import type { Notification } from '@/lib/notificationService' import { NotificationContent } from './NotificationContent' import { NotificationActions } from './NotificationActions' @@ -19,11 +19,14 @@ export function NotificationItem({ return (
onNotificationClick(notification)} > + {!notification.read && ( +
+ )}
diff --git a/components/NotificationPanel.tsx b/components/NotificationPanel.tsx index 2db4d8c..11ced1c 100644 --- a/components/NotificationPanel.tsx +++ b/components/NotificationPanel.tsx @@ -1,4 +1,4 @@ -import type { Notification } from '@/types/notifications' +import type { Notification } from '@/lib/notificationService' import { NotificationItem } from './NotificationItem' import { NotificationPanelHeader } from './NotificationPanelHeader' import { t } from '@/lib/i18n' diff --git a/components/PageHeader.tsx b/components/PageHeader.tsx index 2fa64eb..5ad0805 100644 --- a/components/PageHeader.tsx +++ b/components/PageHeader.tsx @@ -3,7 +3,6 @@ import { ConditionalPublishButton } from './ConditionalPublishButton' import { LanguageSelector } from './LanguageSelector' import { t } from '@/lib/i18n' import { KeyIndicator } from './KeyIndicator' -import { SyncStatus } from './GlobalSyncProgressBar' function GitIcon(): React.ReactElement { return ( @@ -90,7 +89,6 @@ export function PageHeader(): React.ReactElement { -
diff --git a/components/RelayManager.tsx b/components/RelayManager.tsx index 35468e3..89eb6ae 100644 --- a/components/RelayManager.tsx +++ b/components/RelayManager.tsx @@ -377,6 +377,11 @@ export function RelayManager({ onConfigChange }: RelayManagerProps): React.React
)}
+ {relay.lastSyncDate && ( +
+ {t('settings.relay.list.lastSync')}: {new Date(relay.lastSyncDate).toLocaleString()} +
+ )}