diff --git a/.cursor/rules/quality.mdc b/.cursor/rules/quality.mdc index 8a91dd8..9723a63 100644 --- a/.cursor/rules/quality.mdc +++ b/.cursor/rules/quality.mdc @@ -456,6 +456,97 @@ 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. +### Validation et précaution obligatoires + +Avant toute implémentation ou modification, certaines décisions critiques doivent être validées explicitement par l'utilisateur. + +#### Validation des choix de design + +Tous les choix de design architecturaux, d'interface utilisateur ou d'expérience utilisateur doivent être validés avant implémentation. + +Sont concernés : + +* **Architecture** : Choix de patterns, structure de modules, organisation des couches +* **Interface utilisateur** : Disposition des composants, flux de navigation, interactions utilisateur +* **Expérience utilisateur** : Parcours utilisateur, workflows, processus métier +* **Intégrations** : Choix d'API externes, protocoles de communication, formats d'échange +* **Performance** : Stratégies d'optimisation, mécanismes de cache, techniques de chargement + +Aucune implémentation ne doit être commencée sans validation explicite des choix de design proposés. + +#### Validation des modifications du modèle de données + +Toute modification affectant le modèle de données, la structure de la base de données ou les schémas de données doit être validée avant implémentation. + +Sont concernés : + +* **Schémas de base de données** : Ajout, modification ou suppression de tables, colonnes, index, contraintes +* **Types de données** : Modifications de types, ajout de champs, changements de structure +* **Relations** : Ajout ou modification de relations entre entités, clés étrangères +* **Migrations** : Scripts de migration de base de données, transformations de données +* **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 : +* L'impact sur les données existantes +* Les migrations nécessaires +* Les risques de régression +* Les stratégies de rollback + +Aucune modification du modèle de données ne doit être implémentée sans validation explicite. + +#### Validation des commits et publications + +Tous les commits et publications doivent être validés avant exécution. + +**Commits :** + +* Tous les commits doivent être présentés avec leur message structuré avant validation +* Aucun commit ne doit être effectué sans validation explicite +* Les commits doivent être atomiques et logiques +* Les commits doivent respecter le format structuré défini dans les règles de commits + +**Publications :** + +* Toutes les publications (push, merge, release) doivent être validées avant exécution +* Les publications vers les branches principales (main, master, production) nécessitent une validation explicite +* Les déploiements doivent être validés avant exécution + +Aucun commit ou publication ne doit être effectué automatiquement sans validation. + +#### Validation des modifications des contrôles qualité et consignes + +Toute modification des règles de qualité, des contrôles de linting, des configurations ESLint, des règles TypeScript ou des consignes à l'IA doit être validée avant application. + +Sont concernés : + +* **Règles de linting** : Ajout, modification ou suppression de règles ESLint +* **Configuration TypeScript** : Modifications de `tsconfig.json`, règles de compilation +* **Règles de qualité** : Modifications des fichiers de règles (`.cursor/rules/`, `quality.mdc`, etc.) +* **Consignes à l'IA** : Modifications des instructions pour l'IA, changements de workflow +* **Tests** : Modifications des stratégies de test, ajout ou suppression de frameworks de test +* **CI/CD** : Modifications des pipelines, des scripts de déploiement, des vérifications automatiques + +Ces modifications ont un impact systémique sur tout le projet et doivent être validées explicitement avant application. + +#### Processus de validation + +Le processus de validation doit suivre ces étapes : + +1. **Proposition** : Présenter clairement les choix ou modifications proposés avec : + * Le contexte et les motivations + * Les alternatives considérées + * Les impacts attendus + * Les risques identifiés + +2. **Attente de validation** : Attendre explicitement la validation de l'utilisateur avant de procéder + +3. **Documentation** : Documenter les choix validés dans la documentation appropriée (`docs/`, `features/`, `fixKnowledge/`) + +4. **Implémentation** : Procéder à l'implémentation uniquement après validation explicite + +Aucune action ne doit être entreprise en supposant une validation implicite ou en anticipant une réponse. + ### Documentation et contrats Les fonctions publiques, classes et modules introduits doivent être auto-descriptifs par leur nommage et leur typage. diff --git a/components/GlobalSyncProgressBar.tsx b/components/GlobalSyncProgressBar.tsx index f4ed5c5..78763a3 100644 --- a/components/GlobalSyncProgressBar.tsx +++ b/components/GlobalSyncProgressBar.tsx @@ -3,39 +3,23 @@ import { syncProgressManager } from '@/lib/syncProgressManager' import type { SyncProgress } from '@/lib/userContentSync' import { t } from '@/lib/i18n' -export function GlobalSyncProgressBar(): React.ReactElement | null { - const [progress, setProgress] = useState(null) - - useEffect(() => { - const unsubscribe = syncProgressManager.subscribe((newProgress) => { - setProgress(newProgress) - }) - - return () => { - unsubscribe() - } - }, []) - - if (!progress || progress.completed) { - return null +// Extract relay name from URL (remove wss:// and truncate if too long) +function getRelayDisplayName(relayUrl?: string): string { + if (!relayUrl) { + return '' } - - // Extract relay name from URL (remove wss:// and truncate if too long) - const getRelayDisplayName = (relayUrl?: string): string => { - if (!relayUrl) { - return 'Connecting...' - } - const cleaned = relayUrl.replace(/^wss?:\/\//, '').replace(/\/$/, '') - if (cleaned.length > 50) { - return `${cleaned.substring(0, 47)}...` - } - return cleaned + const cleaned = relayUrl.replace(/^wss?:\/\//, '').replace(/\/$/, '') + if (cleaned.length > 50) { + return `${cleaned.substring(0, 47)}...` } + return cleaned +} - // Spinning sync icon - const SyncIcon = (): React.ReactElement => ( +// Spinning sync icon +function SyncIcon(): React.ReactElement { + return ( ) +} + +/** + * Sync status indicator component for display in header next to key icon + * Shows sync icon + relay name when syncing + */ +export function SyncStatus(): React.ReactElement | null { + const [progress, setProgress] = useState(null) + + useEffect(() => { + const unsubscribe = syncProgressManager.subscribe((newProgress) => { + setProgress(newProgress) + }) + + return () => { + unsubscribe() + } + }, []) + + if (!progress || progress.completed) { + return null + } return ( -
-
-
- - {progress.currentRelay && ( - - {getRelayDisplayName(progress.currentRelay)} - - )} - {!progress.currentRelay && ( - - {t('settings.sync.connecting')} - - )} -
-
+
+ + {progress.currentRelay ? ( + + {getRelayDisplayName(progress.currentRelay)} + + ) : ( + + {t('settings.sync.connecting')} + + )}
) } + +/** + * Global sync progress bar (deprecated - kept for backward compatibility) + * @deprecated Use SyncStatus component in KeyManagementManager instead + */ +export function GlobalSyncProgressBar(): React.ReactElement | null { + return null +} diff --git a/components/PageHeader.tsx b/components/PageHeader.tsx index 5ad0805..2fa64eb 100644 --- a/components/PageHeader.tsx +++ b/components/PageHeader.tsx @@ -3,6 +3,7 @@ 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 ( @@ -89,6 +90,7 @@ export function PageHeader(): React.ReactElement { +
diff --git a/lib/articlePublisherHelpersPresentation.ts b/lib/articlePublisherHelpersPresentation.ts index 9723e59..cb24cb6 100644 --- a/lib/articlePublisherHelpersPresentation.ts +++ b/lib/articlePublisherHelpersPresentation.ts @@ -288,19 +288,22 @@ export async function fetchAuthorPresentationFromPool( } }) - sub.on('eose', async (): Promise => { - // Get the latest version from all collected events - const latestEvent = getLatestVersion(events) - if (latestEvent) { - const parsed = await parsePresentationEvent(latestEvent) - if (parsed) { - await finalize(parsed) - return + sub.on('eose', (): void => { + void (async (): Promise => { + // Get the latest version from all collected events + const latestEvent = getLatestVersion(events) + if (latestEvent) { + const parsed = await parsePresentationEvent(latestEvent) + if (parsed) { + await finalize(parsed) + return + } } - } - await finalize(null) + await finalize(null) + })() }) - setTimeout(async (): Promise => { + setTimeout((): void => { + void (async (): Promise => { // Get the latest version from all collected events const latestEvent = getLatestVersion(events) if (latestEvent) { @@ -311,6 +314,7 @@ export async function fetchAuthorPresentationFromPool( } } await finalize(null) + })() }, 5000).unref?.() }) } diff --git a/lib/authorQueries.ts b/lib/authorQueries.ts index d41e55d..27b112e 100644 --- a/lib/authorQueries.ts +++ b/lib/authorQueries.ts @@ -89,19 +89,22 @@ export async function fetchAuthorByHashId( } }) - sub.on('eose', async (): Promise => { - // Get the latest version from all collected events - const latestEvent = getLatestVersion(events) - if (latestEvent) { - const parsed = await parsePresentationEvent(latestEvent) - if (parsed) { - await finalize(parsed) - return + sub.on('eose', (): void => { + void (async (): Promise => { + // Get the latest version from all collected events + const latestEvent = getLatestVersion(events) + if (latestEvent) { + const parsed = await parsePresentationEvent(latestEvent) + if (parsed) { + await finalize(parsed) + return + } } - } - await finalize(null) + await finalize(null) + })() }) - setTimeout(async (): Promise => { + setTimeout((): void => { + void (async (): Promise => { // Get the latest version from all collected events const timeoutLatestEvent = getLatestVersion(events) if (timeoutLatestEvent) { @@ -112,6 +115,7 @@ export async function fetchAuthorByHashId( } } await finalize(null) + })() }, 5000).unref?.() }) } diff --git a/lib/platformSync.ts b/lib/platformSync.ts index 49a5a1d..ad8f5df 100644 --- a/lib/platformSync.ts +++ b/lib/platformSync.ts @@ -93,13 +93,17 @@ class PlatformSyncService { } }) - sub.on('eose', async (): Promise => { - await finalize() + sub.on('eose', (): void => { + void (async (): Promise => { + await finalize() + })() }) // Timeout after SYNC_TIMEOUT_MS - setTimeout(async (): Promise => { - await finalize() + setTimeout((): void => { + void (async (): Promise => { + await finalize() + })() }, this.SYNC_TIMEOUT_MS).unref?.() this.syncSubscription = sub diff --git a/lib/purchaseQueries.ts b/lib/purchaseQueries.ts index f95eb14..131845b 100644 --- a/lib/purchaseQueries.ts +++ b/lib/purchaseQueries.ts @@ -101,7 +101,9 @@ export async function getPurchaseById(purchaseId: string, timeoutMs: number = 50 sub.on('eose', (): void => { void done(null) }) - setTimeout(() => done(null), timeoutMs).unref?.() + setTimeout((): void => { + void done(null) + }, timeoutMs).unref?.() }) } @@ -142,8 +144,12 @@ export function getPurchasesForArticle(articleId: string, timeoutMs: number = 50 })() }) - sub.on('eose', () => done()) - setTimeout(() => done(), timeoutMs).unref?.() + sub.on('eose', (): void => { + void done() + }) + setTimeout((): void => { + void done() + }, timeoutMs).unref?.() }) } diff --git a/lib/reviewTipQueries.ts b/lib/reviewTipQueries.ts index 719e031..0e1d32f 100644 --- a/lib/reviewTipQueries.ts +++ b/lib/reviewTipQueries.ts @@ -108,7 +108,9 @@ export async function getReviewTipById(reviewTipId: string, timeoutMs: number = sub.on('eose', (): void => { void done(null) }) - setTimeout(() => done(null), timeoutMs).unref?.() + setTimeout((): void => { + void done(null) + }, timeoutMs).unref?.() }) } diff --git a/lib/seriesQueries.ts b/lib/seriesQueries.ts index 07b1aae..066d0ad 100644 --- a/lib/seriesQueries.ts +++ b/lib/seriesQueries.ts @@ -128,21 +128,25 @@ export async function getSeriesById(seriesId: string, timeoutMs: number = 5000): resolve(value) } - sub.on('event', async (event: Event): Promise => { - const parsed = await parseSeriesFromEvent(event) - if (parsed) { - // Cache the parsed series - const tags = extractTagsFromEvent(event) - if (parsed.hash) { - await objectCache.set('series', parsed.hash, event, parsed, tags.version ?? 0, tags.hidden ?? false, parsed.index) + sub.on('event', (event: Event): void => { + void (async (): Promise => { + const parsed = await parseSeriesFromEvent(event) + if (parsed) { + // Cache the parsed series + const tags = extractTagsFromEvent(event) + if (parsed.hash) { + await objectCache.set('series', parsed.hash, event, parsed, tags.version ?? 0, tags.hidden ?? false, parsed.index) + } + await done(parsed) } - await done(parsed) - } + })() }) sub.on('eose', (): void => { void done(null) }) - setTimeout(() => done(null), timeoutMs).unref?.() + setTimeout((): void => { + void done(null) + }, timeoutMs).unref?.() }) } diff --git a/lib/sponsoringQueries.ts b/lib/sponsoringQueries.ts index f17f585..fed9874 100644 --- a/lib/sponsoringQueries.ts +++ b/lib/sponsoringQueries.ts @@ -108,7 +108,9 @@ export async function getSponsoringById(sponsoringId: string, timeoutMs: number sub.on('eose', (): void => { void done(null) }) - setTimeout(() => done(null), timeoutMs).unref?.() + setTimeout((): void => { + void done(null) + }, timeoutMs).unref?.() }) } diff --git a/pages/_app.tsx b/pages/_app.tsx index 404db5d..8175324 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -7,7 +7,6 @@ import { nostrAuthService } from '@/lib/nostrAuth' import { syncUserContentToCache } from '@/lib/userContentSync' import { getLastSyncDate, getCurrentTimestamp } from '@/lib/syncStorage' import { syncProgressManager } from '@/lib/syncProgressManager' -import { GlobalSyncProgressBar } from '@/components/GlobalSyncProgressBar' import { relaySessionManager } from '@/lib/relaySessionManager' function I18nProvider({ children }: { children: React.ReactNode }): React.ReactElement { @@ -136,7 +135,6 @@ export default function App({ Component, pageProps }: AppProps): React.ReactElem return ( - ) diff --git a/public/locales/en.txt b/public/locales/en.txt index 8a0e157..b4c2eb0 100644 --- a/public/locales/en.txt +++ b/public/locales/en.txt @@ -249,6 +249,7 @@ settings.sync.completed=Everything is synchronized settings.sync.ready=Ready to synchronize settings.sync.syncing=Synchronizing settings.sync.connecting=Connecting... +settings.sync.status=Synchronizing settings.nip95.title=NIP-95 Upload Endpoints settings.nip95.loading=Loading... settings.nip95.error.loadFailed=Failed to load NIP-95 APIs diff --git a/public/locales/fr.txt b/public/locales/fr.txt index 19ccb6b..e4c07bc 100644 --- a/public/locales/fr.txt +++ b/public/locales/fr.txt @@ -249,6 +249,7 @@ settings.sync.completed=Tout est synchronisé settings.sync.ready=Prêt à synchroniser settings.sync.syncing=Synchronisation en cours settings.sync.connecting=Connexion... +settings.sync.status=Synchronisation settings.language.title=Langue de préférence settings.language.description=Choisissez votre langue préférée pour l'interface settings.language.loading=Chargement...