Compare commits
1 Commits
fix/bugs-b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8ee53c815 |
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -1,36 +1,30 @@
|
|||||||
# Audit dashboard Second Brain (`/home`) — 2026-07-16
|
# 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 |
|
| # | 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 l’architecture. 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 d’anciens seaux → forcer refresh API (déjà multi-feature dans usage-current)
|
|
||||||
3. Prioriser pertinence bridges (insights) plutôt que refonte dashboard
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user