lint fix wip

This commit is contained in:
Nicolas Cantu 2026-01-06 21:16:28 +01:00
parent cdd923e981
commit b7d65a55c7
13 changed files with 216 additions and 88 deletions

View File

@ -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. 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 ### Documentation et contrats
Les fonctions publiques, classes et modules introduits doivent être auto-descriptifs par leur nommage et leur typage. Les fonctions publiques, classes et modules introduits doivent être auto-descriptifs par leur nommage et leur typage.

View File

@ -3,39 +3,23 @@ import { syncProgressManager } from '@/lib/syncProgressManager'
import type { SyncProgress } from '@/lib/userContentSync' import type { SyncProgress } from '@/lib/userContentSync'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
export function GlobalSyncProgressBar(): React.ReactElement | null { // Extract relay name from URL (remove wss:// and truncate if too long)
const [progress, setProgress] = useState<SyncProgress | null>(null) function getRelayDisplayName(relayUrl?: string): string {
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)
const getRelayDisplayName = (relayUrl?: string): string => {
if (!relayUrl) { if (!relayUrl) {
return 'Connecting...' return ''
} }
const cleaned = relayUrl.replace(/^wss?:\/\//, '').replace(/\/$/, '') const cleaned = relayUrl.replace(/^wss?:\/\//, '').replace(/\/$/, '')
if (cleaned.length > 50) { if (cleaned.length > 50) {
return `${cleaned.substring(0, 47)}...` return `${cleaned.substring(0, 47)}...`
} }
return cleaned return cleaned
} }
// Spinning sync icon // Spinning sync icon
const SyncIcon = (): React.ReactElement => ( function SyncIcon(): React.ReactElement {
return (
<svg <svg
className="animate-spin h-5 w-5 text-neon-cyan" className="animate-spin h-4 w-4 text-neon-cyan"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -55,24 +39,49 @@ export function GlobalSyncProgressBar(): React.ReactElement | null {
/> />
</svg> </svg>
) )
}
/**
* 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<SyncProgress | null>(null)
useEffect(() => {
const unsubscribe = syncProgressManager.subscribe((newProgress) => {
setProgress(newProgress)
})
return () => {
unsubscribe()
}
}, [])
if (!progress || progress.completed) {
return null
}
return ( return (
<div className="fixed top-0 left-0 right-0 z-50 bg-cyber-darker border-b border-neon-cyan/30 shadow-lg"> <div className="flex items-center gap-2 ml-2" title={progress.currentRelay ? getRelayDisplayName(progress.currentRelay) : t('settings.sync.connecting')}>
<div className="container mx-auto px-4 py-3">
<div className="flex items-center gap-3">
<SyncIcon /> <SyncIcon />
{progress.currentRelay && ( {progress.currentRelay ? (
<span className="text-neon-cyan text-sm font-mono"> <span className="text-neon-cyan text-xs font-mono max-w-[200px] truncate">
{getRelayDisplayName(progress.currentRelay)} {getRelayDisplayName(progress.currentRelay)}
</span> </span>
)} ) : (
{!progress.currentRelay && ( <span className="text-cyber-accent/70 text-xs italic">
<span className="text-cyber-accent/70 text-sm italic">
{t('settings.sync.connecting')} {t('settings.sync.connecting')}
</span> </span>
)} )}
</div> </div>
</div>
</div>
) )
} }
/**
* Global sync progress bar (deprecated - kept for backward compatibility)
* @deprecated Use SyncStatus component in KeyManagementManager instead
*/
export function GlobalSyncProgressBar(): React.ReactElement | null {
return null
}

View File

@ -3,6 +3,7 @@ import { ConditionalPublishButton } from './ConditionalPublishButton'
import { LanguageSelector } from './LanguageSelector' import { LanguageSelector } from './LanguageSelector'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import { KeyIndicator } from './KeyIndicator' import { KeyIndicator } from './KeyIndicator'
import { SyncStatus } from './GlobalSyncProgressBar'
function GitIcon(): React.ReactElement { function GitIcon(): React.ReactElement {
return ( return (
@ -89,6 +90,7 @@ export function PageHeader(): React.ReactElement {
<GitIcon /> <GitIcon />
</a> </a>
<KeyIndicator /> <KeyIndicator />
<SyncStatus />
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<LanguageSelector /> <LanguageSelector />

View File

@ -288,7 +288,8 @@ export async function fetchAuthorPresentationFromPool(
} }
}) })
sub.on('eose', async (): Promise<void> => { sub.on('eose', (): void => {
void (async (): Promise<void> => {
// Get the latest version from all collected events // Get the latest version from all collected events
const latestEvent = getLatestVersion(events) const latestEvent = getLatestVersion(events)
if (latestEvent) { if (latestEvent) {
@ -299,8 +300,10 @@ export async function fetchAuthorPresentationFromPool(
} }
} }
await finalize(null) await finalize(null)
})()
}) })
setTimeout(async (): Promise<void> => { setTimeout((): void => {
void (async (): Promise<void> => {
// Get the latest version from all collected events // Get the latest version from all collected events
const latestEvent = getLatestVersion(events) const latestEvent = getLatestVersion(events)
if (latestEvent) { if (latestEvent) {
@ -311,6 +314,7 @@ export async function fetchAuthorPresentationFromPool(
} }
} }
await finalize(null) await finalize(null)
})()
}, 5000).unref?.() }, 5000).unref?.()
}) })
} }

View File

@ -89,7 +89,8 @@ export async function fetchAuthorByHashId(
} }
}) })
sub.on('eose', async (): Promise<void> => { sub.on('eose', (): void => {
void (async (): Promise<void> => {
// Get the latest version from all collected events // Get the latest version from all collected events
const latestEvent = getLatestVersion(events) const latestEvent = getLatestVersion(events)
if (latestEvent) { if (latestEvent) {
@ -100,8 +101,10 @@ export async function fetchAuthorByHashId(
} }
} }
await finalize(null) await finalize(null)
})()
}) })
setTimeout(async (): Promise<void> => { setTimeout((): void => {
void (async (): Promise<void> => {
// Get the latest version from all collected events // Get the latest version from all collected events
const timeoutLatestEvent = getLatestVersion(events) const timeoutLatestEvent = getLatestVersion(events)
if (timeoutLatestEvent) { if (timeoutLatestEvent) {
@ -112,6 +115,7 @@ export async function fetchAuthorByHashId(
} }
} }
await finalize(null) await finalize(null)
})()
}, 5000).unref?.() }, 5000).unref?.()
}) })
} }

View File

@ -93,13 +93,17 @@ class PlatformSyncService {
} }
}) })
sub.on('eose', async (): Promise<void> => { sub.on('eose', (): void => {
void (async (): Promise<void> => {
await finalize() await finalize()
})()
}) })
// Timeout after SYNC_TIMEOUT_MS // Timeout after SYNC_TIMEOUT_MS
setTimeout(async (): Promise<void> => { setTimeout((): void => {
void (async (): Promise<void> => {
await finalize() await finalize()
})()
}, this.SYNC_TIMEOUT_MS).unref?.() }, this.SYNC_TIMEOUT_MS).unref?.()
this.syncSubscription = sub this.syncSubscription = sub

View File

@ -101,7 +101,9 @@ export async function getPurchaseById(purchaseId: string, timeoutMs: number = 50
sub.on('eose', (): void => { sub.on('eose', (): void => {
void done(null) 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()) sub.on('eose', (): void => {
setTimeout(() => done(), timeoutMs).unref?.() void done()
})
setTimeout((): void => {
void done()
}, timeoutMs).unref?.()
}) })
} }

View File

@ -108,7 +108,9 @@ export async function getReviewTipById(reviewTipId: string, timeoutMs: number =
sub.on('eose', (): void => { sub.on('eose', (): void => {
void done(null) void done(null)
}) })
setTimeout(() => done(null), timeoutMs).unref?.() setTimeout((): void => {
void done(null)
}, timeoutMs).unref?.()
}) })
} }

View File

@ -128,7 +128,8 @@ export async function getSeriesById(seriesId: string, timeoutMs: number = 5000):
resolve(value) resolve(value)
} }
sub.on('event', async (event: Event): Promise<void> => { sub.on('event', (event: Event): void => {
void (async (): Promise<void> => {
const parsed = await parseSeriesFromEvent(event) const parsed = await parseSeriesFromEvent(event)
if (parsed) { if (parsed) {
// Cache the parsed series // Cache the parsed series
@ -138,11 +139,14 @@ export async function getSeriesById(seriesId: string, timeoutMs: number = 5000):
} }
await done(parsed) await done(parsed)
} }
})()
}) })
sub.on('eose', (): void => { sub.on('eose', (): void => {
void done(null) void done(null)
}) })
setTimeout(() => done(null), timeoutMs).unref?.() setTimeout((): void => {
void done(null)
}, timeoutMs).unref?.()
}) })
} }

View File

@ -108,7 +108,9 @@ export async function getSponsoringById(sponsoringId: string, timeoutMs: number
sub.on('eose', (): void => { sub.on('eose', (): void => {
void done(null) void done(null)
}) })
setTimeout(() => done(null), timeoutMs).unref?.() setTimeout((): void => {
void done(null)
}, timeoutMs).unref?.()
}) })
} }

View File

@ -7,7 +7,6 @@ import { nostrAuthService } from '@/lib/nostrAuth'
import { syncUserContentToCache } from '@/lib/userContentSync' import { syncUserContentToCache } from '@/lib/userContentSync'
import { getLastSyncDate, getCurrentTimestamp } from '@/lib/syncStorage' import { getLastSyncDate, getCurrentTimestamp } from '@/lib/syncStorage'
import { syncProgressManager } from '@/lib/syncProgressManager' import { syncProgressManager } from '@/lib/syncProgressManager'
import { GlobalSyncProgressBar } from '@/components/GlobalSyncProgressBar'
import { relaySessionManager } from '@/lib/relaySessionManager' import { relaySessionManager } from '@/lib/relaySessionManager'
function I18nProvider({ children }: { children: React.ReactNode }): React.ReactElement { function I18nProvider({ children }: { children: React.ReactNode }): React.ReactElement {
@ -136,7 +135,6 @@ export default function App({ Component, pageProps }: AppProps): React.ReactElem
return ( return (
<I18nProvider> <I18nProvider>
<GlobalSyncProgressBar />
<Component {...pageProps} /> <Component {...pageProps} />
</I18nProvider> </I18nProvider>
) )

View File

@ -249,6 +249,7 @@ settings.sync.completed=Everything is synchronized
settings.sync.ready=Ready to synchronize settings.sync.ready=Ready to synchronize
settings.sync.syncing=Synchronizing settings.sync.syncing=Synchronizing
settings.sync.connecting=Connecting... settings.sync.connecting=Connecting...
settings.sync.status=Synchronizing
settings.nip95.title=NIP-95 Upload Endpoints settings.nip95.title=NIP-95 Upload Endpoints
settings.nip95.loading=Loading... settings.nip95.loading=Loading...
settings.nip95.error.loadFailed=Failed to load NIP-95 APIs settings.nip95.error.loadFailed=Failed to load NIP-95 APIs

View File

@ -249,6 +249,7 @@ settings.sync.completed=Tout est synchronisé
settings.sync.ready=Prêt à synchroniser settings.sync.ready=Prêt à synchroniser
settings.sync.syncing=Synchronisation en cours settings.sync.syncing=Synchronisation en cours
settings.sync.connecting=Connexion... settings.sync.connecting=Connexion...
settings.sync.status=Synchronisation
settings.language.title=Langue de préférence settings.language.title=Langue de préférence
settings.language.description=Choisissez votre langue préférée pour l'interface settings.language.description=Choisissez votre langue préférée pour l'interface
settings.language.loading=Chargement... settings.language.loading=Chargement...