fix(quotas): unifier le décompte IA (BYOK, rollback) et combler les fuites
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 6m55s
CI / Deploy production (on server) (push) Successful in 36s

Centralise la réserve via ai-quota, corrige admin unavailable (-1), brancher les routes sans quota et le host-pays brainstorm, avec usage-meter élargi, noms de clusters, MCP et ajustements dashboard/insights.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Antigravity
2026-07-15 20:42:25 +00:00
parent 30da592ba2
commit 4fe31ebc99
75 changed files with 2949 additions and 785 deletions

View File

@@ -29,6 +29,7 @@ import {
} from '@/components/dashboard-path-widgets'
import type { DashboardPath } from '@/lib/dashboard/path-types'
import { buildFastPathsFromBriefing } from '@/lib/dashboard/paths-fast'
import { emitAiUsageChanged } from '@/lib/ai-usage-sync'
import {
DashboardInboxWidget,
DashboardRevisionWidget,
@@ -192,7 +193,8 @@ function localeForLanguage(language: string): string {
return map[language] || language
}
function stripHtml(html: string): string {
function stripHtml(html: string | null | undefined): string {
if (!html) return ''
return html.replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim()
}
@@ -293,7 +295,10 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
const loadSentiment = useCallback(async () => {
try {
const res = await fetch('/api/briefing/sentiment', { cache: 'no-store' })
if (res.ok) setSentiment(await res.json())
if (res.ok) {
setSentiment(await res.json())
emitAiUsageChanged()
}
} catch {
/* widget sentiment en état vide */
} finally {
@@ -322,15 +327,15 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
fetch('/api/flashcards/stats', { cache: 'no-store' })
.then(r => r.ok ? r.json() : null)
.then(json => {
if (json) {
setFlashcardStats({
retentionRate: json.retentionRate ?? 0,
streak: json.streak ?? 0,
totalCards: json.totalCards ?? 0,
})
}
setFlashcardStats({
retentionRate: json?.retentionRate ?? 0,
streak: json?.streak ?? 0,
totalCards: json?.totalCards ?? 0,
})
})
.catch(() => {
setFlashcardStats({ retentionRate: 0, streak: 0, totalCards: 0 })
})
.catch(() => {})
}, [loadBriefing, loadMindMap])
// Pistes rapides dès le briefing, puis enrichissement en arrière-plan
@@ -482,6 +487,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
if (!res.ok) throw new Error(json.error)
if (json.insight) {
await Promise.all([loadBriefing(), loadPaths()])
emitAiUsageChanged()
toast.success(t('homeDashboard.echoFound'))
} else {
toast.info(t('homeDashboard.echoNone'))
@@ -789,6 +795,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
retentionRate={flashcardStats?.retentionRate ?? 0}
streak={flashcardStats?.streak ?? 0}
totalCards={flashcardStats?.totalCards ?? 0}
dueCount={dueFlashcards}
loading={!flashcardStats}
onOpen={() => router.push('/revision')}
/>,
@@ -799,6 +806,14 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
notes={resumeNotes}
loading={briefingLoading}
onSelect={onNoteSelect}
onCaptureFocus={() => {
document.getElementById('dashboard-widget-capture')?.scrollIntoView({
behavior: prefersReducedMotion ? 'auto' : 'smooth',
block: 'center',
})
const ta = document.querySelector<HTMLTextAreaElement>('#dashboard-widget-capture textarea')
ta?.focus()
}}
formatRelativeTime={relTime}
prefersReducedMotion={!!prefersReducedMotion}
/>,
@@ -1005,7 +1020,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
<div className="h-full w-full bg-[#F9F8F6] dark:bg-[#0D0D0D] overflow-y-auto custom-scrollbar relative">
<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="max-w-6xl mx-auto px-4 sm:px-6 lg:px-10 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">
{/* ── 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">