fix(dashboard): audit Second Brain — pistes, erreur briefing, dark, crédits
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m16s
CI / Deploy production (on server) (push) Has been skipped

Corrige loadPaths sans argument après Echo/IA, bannière si briefing en échec,
checklist connexion inversée, skeletons dark, libellé usage = solde unique.
This commit is contained in:
Antigravity
2026-07-16 21:08:48 +00:00
parent a77f3ffb6e
commit e8ee53c815
6 changed files with 99 additions and 50 deletions

View File

@@ -58,7 +58,7 @@ export function DashboardInboxWidget({
compact compact
> >
{loading ? ( {loading ? (
<div className="h-10 rounded-lg bg-stone-50 animate-pulse" /> <div className="h-10 rounded-lg bg-stone-50 dark:bg-zinc-950/40 animate-pulse" />
) : ( ) : (
<button <button
type="button" type="button"
@@ -94,7 +94,7 @@ export function DashboardRevisionWidget({
compact compact
> >
{loading ? ( {loading ? (
<div className="h-10 rounded-lg bg-stone-50 animate-pulse" /> <div className="h-10 rounded-lg bg-stone-50 dark:bg-zinc-950/40 animate-pulse" />
) : ( ) : (
<button <button
type="button" type="button"
@@ -135,7 +135,7 @@ export function DashboardStatsWidget({
> >
{loading ? ( {loading ? (
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2">
{[0, 1, 2].map(i => <div key={i} className="h-12 rounded-lg bg-stone-50 animate-pulse" />)} {[0, 1, 2].map(i => <div key={i} className="h-12 rounded-lg bg-stone-50 dark:bg-zinc-950/40 animate-pulse" />)}
</div> </div>
) : ( ) : (
<button type="button" onClick={onOpen} className="w-full text-start"> <button type="button" onClick={onOpen} className="w-full text-start">
@@ -176,8 +176,8 @@ export function DashboardAgentActivityWidget({
> >
{loading ? ( {loading ? (
<div className="space-y-2"> <div className="space-y-2">
<div className="h-8 rounded-lg bg-stone-50 animate-pulse" /> <div className="h-8 rounded-lg bg-stone-50 dark:bg-zinc-950/40 animate-pulse" />
<div className="h-8 rounded-lg bg-stone-50 animate-pulse" /> <div className="h-8 rounded-lg bg-stone-50 dark:bg-zinc-950/40 animate-pulse" />
</div> </div>
) : actions.length === 0 ? ( ) : actions.length === 0 ? (
<p className="text-[10px] text-concrete italic py-2">{t('homeDashboard.widgetAgentActivityEmpty')}</p> <p className="text-[10px] text-concrete italic py-2">{t('homeDashboard.widgetAgentActivityEmpty')}</p>
@@ -222,7 +222,7 @@ export function DashboardGmailWidget({
compact compact
> >
{loading ? ( {loading ? (
<div className="h-10 rounded-lg bg-stone-50 animate-pulse" /> <div className="h-10 rounded-lg bg-stone-50 dark:bg-zinc-950/40 animate-pulse" />
) : connected ? ( ) : connected ? (
<button <button
type="button" type="button"
@@ -264,7 +264,7 @@ export function DashboardPinnedWidget({
> >
{loading ? ( {loading ? (
<div className="space-y-2"> <div className="space-y-2">
<div className="h-8 rounded-lg bg-stone-50 animate-pulse" /> <div className="h-8 rounded-lg bg-stone-50 dark:bg-zinc-950/40 animate-pulse" />
</div> </div>
) : notes.length === 0 ? ( ) : notes.length === 0 ? (
<p className="text-[10px] text-concrete italic py-2">{t('homeDashboard.widgetPinnedEmpty')}</p> <p className="text-[10px] text-concrete italic py-2">{t('homeDashboard.widgetPinnedEmpty')}</p>

View File

@@ -30,7 +30,7 @@ export function DashboardDailyReview({
> >
{loading ? ( {loading ? (
<div className="space-y-2"> <div className="space-y-2">
{[0, 1, 2].map(i => <div key={i} className="h-8 rounded-lg bg-stone-50 animate-pulse" />)} {[0, 1, 2].map(i => <div key={i} className="h-8 rounded-lg bg-stone-50 dark:bg-zinc-950/40 animate-pulse" />)}
</div> </div>
) : ( ) : (
<ul className="space-y-1.5"> <ul className="space-y-1.5">
@@ -79,7 +79,7 @@ export function DashboardOpenLoops({
compact compact
> >
{loading ? ( {loading ? (
<div className="h-16 rounded-lg bg-stone-50 animate-pulse" /> <div className="h-16 rounded-lg bg-stone-50 dark:bg-zinc-950/40 animate-pulse" />
) : loops.length === 0 ? ( ) : loops.length === 0 ? (
<p className="text-[10px] text-concrete italic py-2">{t('homeDashboard.openLoopsEmpty')}</p> <p className="text-[10px] text-concrete italic py-2">{t('homeDashboard.openLoopsEmpty')}</p>
) : ( ) : (
@@ -122,7 +122,7 @@ export function DashboardDailyNoteWidget({
compact compact
> >
{loading ? ( {loading ? (
<div className="h-10 rounded-lg bg-stone-50 animate-pulse" /> <div className="h-10 rounded-lg bg-stone-50 dark:bg-zinc-950/40 animate-pulse" />
) : ( ) : (
<button <button
type="button" type="button"
@@ -158,7 +158,7 @@ export function DashboardLinkSuggestions({
> >
{loading ? ( {loading ? (
<div className="space-y-2"> <div className="space-y-2">
<div className="h-12 rounded-lg bg-stone-50 animate-pulse" /> <div className="h-12 rounded-lg bg-stone-50 dark:bg-zinc-950/40 animate-pulse" />
</div> </div>
) : paths.length === 0 ? ( ) : paths.length === 0 ? (
<p className="text-[10px] text-concrete italic">{t('homeDashboard.linkSuggestionsEmpty')}</p> <p className="text-[10px] text-concrete italic">{t('homeDashboard.linkSuggestionsEmpty')}</p>
@@ -216,7 +216,7 @@ export function DashboardBridgesWidget({
title={t('homeDashboard.widgets.bridges')} title={t('homeDashboard.widgets.bridges')}
> >
{loading ? ( {loading ? (
<div className="h-20 rounded-lg bg-stone-50 animate-pulse" /> <div className="h-20 rounded-lg bg-stone-50 dark:bg-zinc-950/40 animate-pulse" />
) : suggestions.length === 0 ? ( ) : suggestions.length === 0 ? (
<p className="text-[10px] text-concrete italic">{t('homeDashboard.bridgesEmpty')}</p> <p className="text-[10px] text-concrete italic">{t('homeDashboard.bridgesEmpty')}</p>
) : ( ) : (

View File

@@ -246,13 +246,19 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
const [echoRefreshing, setEchoRefreshing] = useState(false) const [echoRefreshing, setEchoRefreshing] = useState(false)
const [dismissingInsightId, setDismissingInsightId] = useState<string | null>(null) const [dismissingInsightId, setDismissingInsightId] = useState<string | null>(null)
const [actingBridgeSuggestionKey, setActingBridgeSuggestionKey] = useState<string | null>(null) const [actingBridgeSuggestionKey, setActingBridgeSuggestionKey] = useState<string | null>(null)
const [briefingError, setBriefingError] = useState(false)
const loadBriefing = useCallback(async () => { const loadBriefing = useCallback(async () => {
try { try {
const res = await fetch('/api/briefing', { cache: 'no-store' }) const res = await fetch('/api/briefing', { cache: 'no-store' })
if (res.ok) setData(await res.json()) if (res.ok) {
setData(await res.json())
setBriefingError(false)
} else {
setBriefingError(true)
}
} catch { } catch {
/* état dégradé */ setBriefingError(true)
} }
}, []) }, [])
@@ -292,6 +298,36 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
} }
}, []) }, [])
/** Recharge briefing puis enrichit les pistes (évite loadPaths() sans argument). */
const reloadBriefingAndPaths = useCallback(async () => {
try {
const res = await fetch('/api/briefing', { cache: 'no-store' })
if (!res.ok) {
setBriefingError(true)
return
}
const briefing = await res.json()
setData(briefing)
setBriefingError(false)
setPaths(buildFastPathsFromBriefing({
recentNotes: (briefing.recentNotes ?? []).map((n: BriefingNote) => ({
id: n.id,
title: n.title,
content: n.content,
notebookId: n.notebookId,
})),
inboxCount: briefing.inboxCount ?? 0,
dueFlashcards: briefing.dueFlashcards ?? 0,
insights: briefing.insights ?? [],
bridgeSuggestions: briefing.bridgeSuggestions ?? [],
agentSuggestions: briefing.agentSuggestions ?? [],
}))
await loadPaths(briefing)
} catch {
setBriefingError(true)
}
}, [loadPaths])
const loadSentiment = useCallback(async () => { const loadSentiment = useCallback(async () => {
try { try {
const res = await fetch('/api/briefing/sentiment', { cache: 'no-store' }) const res = await fetch('/api/briefing/sentiment', { cache: 'no-store' })
@@ -486,7 +522,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
if (res.status === 403 && json.error === 'ai_consent_required') { await requestAiConsent(); return } if (res.status === 403 && json.error === 'ai_consent_required') { await requestAiConsent(); return }
if (!res.ok) throw new Error(json.error) if (!res.ok) throw new Error(json.error)
if (json.insight) { if (json.insight) {
await Promise.all([loadBriefing(), loadPaths()]) await reloadBriefingAndPaths()
emitAiUsageChanged() emitAiUsageChanged()
toast.success(t('homeDashboard.echoFound')) toast.success(t('homeDashboard.echoFound'))
} else { } else {
@@ -497,12 +533,12 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
} finally { } finally {
setEchoRefreshing(false) setEchoRefreshing(false)
} }
}, [requestAiConsent, loadBriefing, loadPaths, t]) }, [requestAiConsent, reloadBriefingAndPaths, t])
const handleEnableAi = useCallback(async () => { const handleEnableAi = useCallback(async () => {
await requestAiConsent() await requestAiConsent()
await Promise.all([loadBriefing(), loadPaths()]) await reloadBriefingAndPaths()
}, [requestAiConsent, loadBriefing, loadPaths]) }, [requestAiConsent, reloadBriefingAndPaths])
const handleDismissBridgeSuggestion = useCallback(async (s: BridgeSuggestionItem) => { const handleDismissBridgeSuggestion = useCallback(async (s: BridgeSuggestionItem) => {
const key = `${s.clusterAId}-${s.clusterBId}` const key = `${s.clusterAId}-${s.clusterBId}`
@@ -672,7 +708,8 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
{ {
key: 'connection', key: 'connection',
label: t('homeDashboard.dailyReviewConnection'), label: t('homeDashboard.dailyReviewConnection'),
done: pathsList.some(p => p.type === 'connect' || p.type === 'resurface'), // Coché quand il n'y a plus de piste de connexion à traiter
done: !pathsList.some(p => p.type === 'connect' || p.type === 'resurface'),
onClick: () => { onClick: () => {
const p = pathsList.find(x => x.type === 'connect' || x.type === 'resurface') const p = pathsList.find(x => x.type === 'connect' || x.type === 'resurface')
if (p) handlePathAction(p) if (p) handlePathAction(p)
@@ -853,8 +890,8 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
/> />
{briefingLoading ? ( {briefingLoading ? (
<div className="space-y-2"> <div className="space-y-2">
<div className="h-10 rounded-lg bg-stone-50 animate-pulse" /> <div className="h-10 rounded-lg bg-stone-50 dark:bg-zinc-950/40 animate-pulse" />
<div className="h-10 rounded-lg bg-stone-50 animate-pulse" /> <div className="h-10 rounded-lg bg-stone-50 dark:bg-zinc-950/40 animate-pulse" />
</div> </div>
) : reminders.length === 0 ? ( ) : reminders.length === 0 ? (
<p className="text-[11px] text-concrete italic py-1">{t('homeDashboard.allCaughtUp')}</p> <p className="text-[11px] text-concrete italic py-1">{t('homeDashboard.allCaughtUp')}</p>
@@ -1021,6 +1058,20 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808005_1px,transparent_1px),linear-gradient(to_bottom,#80808005_1px,transparent_1px)] bg-[size:28px_28px] pointer-events-none z-0" /> <div className="absolute inset-0 bg-[linear-gradient(to_right,#80808005_1px,transparent_1px),linear-gradient(to_bottom,#80808005_1px,transparent_1px)] bg-[size:28px_28px] pointer-events-none z-0" />
<div className="w-full px-4 sm:px-6 lg:px-8 xl:px-10 2xl:px-12 py-6 sm:py-8 relative z-10"> <div className="w-full px-4 sm:px-6 lg:px-8 xl:px-10 2xl:px-12 py-6 sm:py-8 relative z-10">
{briefingError && !data && (
<div className="mb-4 rounded-xl border border-rose-500/30 bg-rose-500/5 px-4 py-3 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<p className="text-sm text-ink dark:text-dark-ink">
{t('homeDashboard.briefingLoadError')}
</p>
<button
type="button"
onClick={() => { void reloadBriefingAndPaths() }}
className="shrink-0 text-[10px] font-mono font-bold uppercase tracking-wider px-3 py-2 rounded-lg bg-ink text-white dark:bg-white dark:text-black"
>
{t('homeDashboard.briefingRetry')}
</button>
</div>
)}
{/* ── En-tête : orientation en 2 secondes ── */} {/* ── En-tête : orientation en 2 secondes ── */}
<header className="mb-5"> <header className="mb-5">
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-2 pb-4 border-b border-border/20"> <div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-2 pb-4 border-b border-border/20">

View File

@@ -1,36 +1,30 @@
# Audit dashboard Second Brain (`/home`) — 2026-07-16 # Audit dashboard Second Brain (`/home`) — 2026-07-16
## Portée ## Demande dorigine
Revue code de `dashboard-view.tsx` + widgets catalogue / paths / briefing. « Audit analytique : réaliser un audit complet du tableau de bord. »
## Points sains ## Correctifs appliqués (code)
| Zone | Constat | | # | Problème | Fix |
|------|---------| |---|----------|-----|
| Layout v5 configurable | Catalogue widgets + `/api/dashboard/layout` | | 1 | Après refresh Memory Echo / activation IA, `loadPaths()` appelé **sans argument** → pistes non rechargées | `reloadBriefingAndPaths()` charge briefing puis paths |
| Prochaines pistes | Fast paths + enrich async `/api/briefing/paths` | | 2 | Si `/api/briefing` échoue : spinner infini, aucune erreur | Bannière + bouton Réessayer |
| Revue quotidienne | Checklist interactive (pas revue auto) | | 3 | Checklist « connexion » cochée quand il restait des pistes | `done` = plus aucune piste connect/resurface |
| Quotas | `emitAiUsageChanged` + consent IA sur actions | | 4 | Skeletons `bg-stone-50` clairs en dark mode | Variante `dark:bg-zinc-950/40` |
| Aide « ? » | `DashboardWidgetHelp` i18n | | 5 | Texte widget usage « par fonctionnalité » (seaux) | Solde unique de crédits |
| Empty states | Widgets catalogue gèrent listes vides |
## Risques / écarts ## Déjà OK (revue code)
| # | Problème | Sévérité | Action | - Layout v5 + grille widgets
|---|----------|----------|--------| - Fast paths + enrich async
| 1 | Briefing dépend du cron / première visite : widgets peuvent rester vides temporairement | Moyenne | UX « chargement / aucune donnée » déjà partielle — dogfood | - Revue matinale interactive
| 2 | Bridges / clusters sémantiques absurdes cross-domaine | Moyenne | Produit insights (hors dashboard pur) | - Quotas via `UsageMeter` (balance crédits)
| 3 | Widget usage encore mentalité « seaux » vs solde crédits | Basse | `DashboardUsageWidget` lit `/api/usage/current` (balance multi) — OK si API à jour | - Aide « ? » widgets
| 4 | Agent suggestions vides si cron agent-suggestions off | Basse | Ops CRON_SECRET | - Empty states flashcards / activité
## Verdict ## Reste hors code (ops / produit)
Dashboard **utilisable** et aligné prototype sur larchitecture. Pas de bug bloquant code identifié dans cette passe. - Cron agent-suggestions pour peupler le carrousel agents
**Reste dogfood** : première visite, dark mode, clics paths, quotas 402. - Pertinence sémantique des ponts (logique clusters /insights)
- Dogfood manuel sur compte réel après deploy
## Suite recommandée
1. Dogfood 10 min sur compte réel
2. Si widget usage affiche encore danciens seaux → forcer refresh API (déjà multi-feature dans usage-current)
3. Prioriser pertinence bridges (insights) plutôt que refonte dashboard

View File

@@ -4218,7 +4218,9 @@
"widgetAgentActivityEmpty": "No agent activity in the last 48 hours.", "widgetAgentActivityEmpty": "No agent activity in the last 48 hours.",
"widgetPinnedEmpty": "No pinned notes yet.", "widgetPinnedEmpty": "No pinned notes yet.",
"widgetActivityHint": "Notes edited over the last 90 days.", "widgetActivityHint": "Notes edited over the last 90 days.",
"widgetUsageHint": "Monthly AI usage by feature.", "widgetUsageHint": "Single AI credit balance (all features).",
"briefingLoadError": "Could not load the dashboard.",
"briefingRetry": "Retry",
"widgetCategories": { "widgetCategories": {
"essential": "Essentials", "essential": "Essentials",
"paths": "Paths & review", "paths": "Paths & review",

View File

@@ -4224,7 +4224,9 @@
"widgetAgentActivityEmpty": "Aucune activité d'agent sur les 48 dernières heures.", "widgetAgentActivityEmpty": "Aucune activité d'agent sur les 48 dernières heures.",
"widgetPinnedEmpty": "Aucune note épinglée pour l'instant.", "widgetPinnedEmpty": "Aucune note épinglée pour l'instant.",
"widgetActivityHint": "Notes modifiées sur les 90 derniers jours.", "widgetActivityHint": "Notes modifiées sur les 90 derniers jours.",
"widgetUsageHint": "Consommation IA mensuelle par fonctionnalité.", "widgetUsageHint": "Solde unique de crédits IA (toutes les fonctions).",
"briefingLoadError": "Impossible de charger le tableau de bord.",
"briefingRetry": "Réessayer",
"widgetCategories": { "widgetCategories": {
"essential": "Essentiels", "essential": "Essentiels",
"paths": "Pistes & revue", "paths": "Pistes & revue",