fix(dashboard): audit Second Brain — pistes, erreur briefing, dark, crédits
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:
@@ -58,7 +58,7 @@ export function DashboardInboxWidget({
|
||||
compact
|
||||
>
|
||||
{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
|
||||
type="button"
|
||||
@@ -94,7 +94,7 @@ export function DashboardRevisionWidget({
|
||||
compact
|
||||
>
|
||||
{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
|
||||
type="button"
|
||||
@@ -135,7 +135,7 @@ export function DashboardStatsWidget({
|
||||
>
|
||||
{loading ? (
|
||||
<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>
|
||||
) : (
|
||||
<button type="button" onClick={onOpen} className="w-full text-start">
|
||||
@@ -176,8 +176,8 @@ export function DashboardAgentActivityWidget({
|
||||
>
|
||||
{loading ? (
|
||||
<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 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 dark:bg-zinc-950/40 animate-pulse" />
|
||||
</div>
|
||||
) : actions.length === 0 ? (
|
||||
<p className="text-[10px] text-concrete italic py-2">{t('homeDashboard.widgetAgentActivityEmpty')}</p>
|
||||
@@ -222,7 +222,7 @@ export function DashboardGmailWidget({
|
||||
compact
|
||||
>
|
||||
{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 ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -264,7 +264,7 @@ export function DashboardPinnedWidget({
|
||||
>
|
||||
{loading ? (
|
||||
<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>
|
||||
) : notes.length === 0 ? (
|
||||
<p className="text-[10px] text-concrete italic py-2">{t('homeDashboard.widgetPinnedEmpty')}</p>
|
||||
|
||||
@@ -30,7 +30,7 @@ export function DashboardDailyReview({
|
||||
>
|
||||
{loading ? (
|
||||
<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>
|
||||
) : (
|
||||
<ul className="space-y-1.5">
|
||||
@@ -79,7 +79,7 @@ export function DashboardOpenLoops({
|
||||
compact
|
||||
>
|
||||
{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 ? (
|
||||
<p className="text-[10px] text-concrete italic py-2">{t('homeDashboard.openLoopsEmpty')}</p>
|
||||
) : (
|
||||
@@ -122,7 +122,7 @@ export function DashboardDailyNoteWidget({
|
||||
compact
|
||||
>
|
||||
{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
|
||||
type="button"
|
||||
@@ -158,7 +158,7 @@ export function DashboardLinkSuggestions({
|
||||
>
|
||||
{loading ? (
|
||||
<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>
|
||||
) : paths.length === 0 ? (
|
||||
<p className="text-[10px] text-concrete italic">{t('homeDashboard.linkSuggestionsEmpty')}</p>
|
||||
@@ -216,7 +216,7 @@ export function DashboardBridgesWidget({
|
||||
title={t('homeDashboard.widgets.bridges')}
|
||||
>
|
||||
{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 ? (
|
||||
<p className="text-[10px] text-concrete italic">{t('homeDashboard.bridgesEmpty')}</p>
|
||||
) : (
|
||||
|
||||
@@ -246,13 +246,19 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
const [echoRefreshing, setEchoRefreshing] = useState(false)
|
||||
const [dismissingInsightId, setDismissingInsightId] = useState<string | null>(null)
|
||||
const [actingBridgeSuggestionKey, setActingBridgeSuggestionKey] = useState<string | null>(null)
|
||||
const [briefingError, setBriefingError] = useState(false)
|
||||
|
||||
const loadBriefing = useCallback(async () => {
|
||||
try {
|
||||
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 {
|
||||
/* é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 () => {
|
||||
try {
|
||||
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.ok) throw new Error(json.error)
|
||||
if (json.insight) {
|
||||
await Promise.all([loadBriefing(), loadPaths()])
|
||||
await reloadBriefingAndPaths()
|
||||
emitAiUsageChanged()
|
||||
toast.success(t('homeDashboard.echoFound'))
|
||||
} else {
|
||||
@@ -497,12 +533,12 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
} finally {
|
||||
setEchoRefreshing(false)
|
||||
}
|
||||
}, [requestAiConsent, loadBriefing, loadPaths, t])
|
||||
}, [requestAiConsent, reloadBriefingAndPaths, t])
|
||||
|
||||
const handleEnableAi = useCallback(async () => {
|
||||
await requestAiConsent()
|
||||
await Promise.all([loadBriefing(), loadPaths()])
|
||||
}, [requestAiConsent, loadBriefing, loadPaths])
|
||||
await reloadBriefingAndPaths()
|
||||
}, [requestAiConsent, reloadBriefingAndPaths])
|
||||
|
||||
const handleDismissBridgeSuggestion = useCallback(async (s: BridgeSuggestionItem) => {
|
||||
const key = `${s.clusterAId}-${s.clusterBId}`
|
||||
@@ -672,7 +708,8 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
{
|
||||
key: 'connection',
|
||||
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: () => {
|
||||
const p = pathsList.find(x => x.type === 'connect' || x.type === 'resurface')
|
||||
if (p) handlePathAction(p)
|
||||
@@ -853,8 +890,8 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
/>
|
||||
{briefingLoading ? (
|
||||
<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 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 dark:bg-zinc-950/40 animate-pulse" />
|
||||
</div>
|
||||
) : reminders.length === 0 ? (
|
||||
<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="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 ── */}
|
||||
<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">
|
||||
|
||||
@@ -1,36 +1,30 @@
|
||||
# Audit dashboard Second Brain (`/home`) — 2026-07-16
|
||||
|
||||
## Portée
|
||||
## Demande d’origine
|
||||
|
||||
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 |
|
||||
|------|---------|
|
||||
| Layout v5 configurable | Catalogue widgets + `/api/dashboard/layout` |
|
||||
| Prochaines pistes | Fast paths + enrich async `/api/briefing/paths` |
|
||||
| Revue quotidienne | Checklist interactive (pas revue auto) |
|
||||
| Quotas | `emitAiUsageChanged` + consent IA sur actions |
|
||||
| Aide « ? » | `DashboardWidgetHelp` i18n |
|
||||
| Empty states | Widgets catalogue gèrent listes vides |
|
||||
| # | Problème | Fix |
|
||||
|---|----------|-----|
|
||||
| 1 | Après refresh Memory Echo / activation IA, `loadPaths()` appelé **sans argument** → pistes non rechargées | `reloadBriefingAndPaths()` charge briefing puis paths |
|
||||
| 2 | Si `/api/briefing` échoue : spinner infini, aucune erreur | Bannière + bouton Réessayer |
|
||||
| 3 | Checklist « connexion » cochée quand il restait des pistes | `done` = plus aucune piste connect/resurface |
|
||||
| 4 | Skeletons `bg-stone-50` clairs en dark mode | Variante `dark:bg-zinc-950/40` |
|
||||
| 5 | Texte widget usage « par fonctionnalité » (seaux) | Solde unique de crédits |
|
||||
|
||||
## Risques / écarts
|
||||
## Déjà OK (revue code)
|
||||
|
||||
| # | Problème | Sévérité | Action |
|
||||
|---|----------|----------|--------|
|
||||
| 1 | Briefing dépend du cron / première visite : widgets peuvent rester vides temporairement | Moyenne | UX « chargement / aucune donnée » déjà partielle — dogfood |
|
||||
| 2 | Bridges / clusters sémantiques absurdes cross-domaine | Moyenne | Produit insights (hors dashboard pur) |
|
||||
| 3 | Widget usage encore mentalité « seaux » vs solde crédits | Basse | `DashboardUsageWidget` lit `/api/usage/current` (balance multi) — OK si API à jour |
|
||||
| 4 | Agent suggestions vides si cron agent-suggestions off | Basse | Ops CRON_SECRET |
|
||||
- Layout v5 + grille widgets
|
||||
- Fast paths + enrich async
|
||||
- Revue matinale interactive
|
||||
- Quotas via `UsageMeter` (balance crédits)
|
||||
- Aide « ? » widgets
|
||||
- Empty states flashcards / activité
|
||||
|
||||
## Verdict
|
||||
## Reste hors code (ops / produit)
|
||||
|
||||
Dashboard **utilisable** et aligné prototype sur l’architecture. Pas de bug bloquant code identifié dans cette passe.
|
||||
**Reste dogfood** : première visite, dark mode, clics paths, quotas 402.
|
||||
|
||||
## Suite recommandée
|
||||
|
||||
1. Dogfood 10 min sur compte réel
|
||||
2. Si widget usage affiche encore d’anciens seaux → forcer refresh API (déjà multi-feature dans usage-current)
|
||||
3. Prioriser pertinence bridges (insights) plutôt que refonte dashboard
|
||||
- Cron agent-suggestions pour peupler le carrousel agents
|
||||
- Pertinence sémantique des ponts (logique clusters /insights)
|
||||
- Dogfood manuel sur compte réel après deploy
|
||||
|
||||
@@ -4218,7 +4218,9 @@
|
||||
"widgetAgentActivityEmpty": "No agent activity in the last 48 hours.",
|
||||
"widgetPinnedEmpty": "No pinned notes yet.",
|
||||
"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": {
|
||||
"essential": "Essentials",
|
||||
"paths": "Paths & review",
|
||||
|
||||
@@ -4224,7 +4224,9 @@
|
||||
"widgetAgentActivityEmpty": "Aucune activité d'agent sur les 48 dernières heures.",
|
||||
"widgetPinnedEmpty": "Aucune note épinglée pour l'instant.",
|
||||
"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": {
|
||||
"essential": "Essentiels",
|
||||
"paths": "Pistes & revue",
|
||||
|
||||
Reference in New Issue
Block a user