Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

## [6.2.2] - 2026-04-15

### Added

- **Zentrales Motion-System für Dashboard-Sektionen und Einzel-Visualisierungen** — ein gemeinsamer Section-/Element-Lifecycle steuert jetzt Preload, Reveal und sichtbarkeitsgebundene Aufbauanimationen für Karten, Diagramme und Wertbalken konsistent aus einer Quelle
- **Gezielte Motion-Regressionstests für Dashboard und Diagramme** — neue Frontend-Tests decken Section-Preloading, sichtbarkeitsgebundene Chart-Starts, Histogramm-/Snapshot-Verhalten, responsives Legend-Layout sowie ROI-/Limits-Bar-Aufbau gezielt ab

### Improved

- **Dashboard-Animationen und Ladeverhalten** — Sektionen werden vor dem Eintritt robuster vorbereitet, blenden ruhiger ein und starten Diagramm- sowie Meter-Aufbauten zeitlich harmonischer und deutlich konsistenter als zuvor
- **Diagramm-Aufbau über das gesamte Dashboard** — Linien-, Flächen-, Balken-, Donut-, Scatter- und Heatmap-Visualisierungen nutzen jetzt abgestimmtere Timings, sichtbarkeitsgebundene Starts und ein ruhigeres Motion-Tuning statt gemischter Einzelpfade
- **Heatmap-, KPI- und Meter-Reveals** — Heatmaps bauen sich wochenweise auf, KPI-/Insight-Gruppen folgen einem gleichmäßigeren Reveal, und Dashboard-Bars verwenden denselben orchestrierten Startpfad wie die übrigen Visualisierungen
- **Legendendarstellung in Diagrammen mit vielen Labels** — Chart-Legenden umbrechen jetzt responsiv auf mehrere Zeilen statt horizontal zu scrollen, einschließlich der betreffenden Linien- und Donut-Diagramme
- **Verifizierbarkeit der Motion- und Chart-Änderungen** — zusätzliche gezielte Tests für Anfragequalität, Verteilungen, Cache-Hit-Rate, Legend-Integrationen und Motion-Zeitverhalten sichern die neue Dashboard-Politur gegen Regressionen ab

### Fixed

- **Vorzeitig fertig geladene oder flackernde Diagrammanimationen** — Chart-Aufbauten laufen nicht mehr verdeckt vor dem sichtbaren Reveal ab, sondern starten erst dann, wenn Section und konkretes Diagramm tatsächlich sichtbar sind
- **Inkonsistente Bar- und Vergleichsanimationen in Sekundärflächen** — Anfragequalität, Cache-Ersparnis (ROI), Limits & Abonnements sowie weitere Dashboard-Bars verwenden jetzt keine widersprüchlichen lokalen Sonderpfade oder künstlichen Mindestbreiten mehr
- **Chart-spezifische Darstellungsprobleme in Analyseflächen** — das Verteilungs-Histogramm folgt jetzt dem gemeinsamen Chart-Lifecycle, der Modell-Snapshot der Cache-Hit-Rate ist visuell sauberer gewichtet, und mehrere Legend-/Label-Pfade reagieren auf engem Platz stabiler

## [6.2.1] - 2026-04-15

### Added
Expand Down
24 changes: 19 additions & 5 deletions src/components/cards/MonthMetrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import {
BrainCircuit,
} from 'lucide-react'
import { MetricCard } from './MetricCard'
import { DashboardMotionItem } from '@/components/dashboard/DashboardMotion'
import { FormattedValue } from '@/components/ui/formatted-value'
import { SectionHeader } from '@/components/ui/section-header'
import { FadeIn } from '@/components/features/animations/FadeIn'
import { SECTION_HELP } from '@/lib/help-content'
import { formatCurrency, formatMonthYear, localMonth } from '@/lib/formatters'
import { getCurrentLocale } from '@/lib/i18n'
Expand Down Expand Up @@ -144,8 +144,8 @@ export function MonthMetrics({ daily, metrics }: MonthMetricsProps) {
description={t('metricCards.month.description')}
info={SECTION_HELP.currentMonth}
/>
<FadeIn delay={0.08}>
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4 xl:grid-cols-8">
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4 xl:grid-cols-8">
<DashboardMotionItem order={0}>
<MetricCard
label={t('metricCards.month.costMonth')}
value={<FormattedValue value={agg.totalCost} type="currency" />}
Expand All @@ -159,12 +159,16 @@ export function MonthMetrics({ daily, metrics }: MonthMetricsProps) {
: null
}
/>
</DashboardMotionItem>
<DashboardMotionItem order={1}>
<MetricCard
label={t('metricCards.month.tokensMonth')}
value={<FormattedValue value={agg.totalTokens} type="tokens" />}
icon={<Coins className="h-4 w-4" />}
{...(tokensSubtitle ? { subtitle: tokensSubtitle } : {})}
/>
</DashboardMotionItem>
<DashboardMotionItem order={2}>
<MetricCard
label={t('metricCards.month.activeDays')}
value={`${agg.activeDays} / ${agg.dayOfMonth}`}
Expand All @@ -173,18 +177,24 @@ export function MonthMetrics({ daily, metrics }: MonthMetricsProps) {
})}
icon={<CalendarDays className="h-4 w-4" />}
/>
</DashboardMotionItem>
<DashboardMotionItem order={3}>
<MetricCard
label={t('metricCards.month.models')}
value={String(agg.modelCount)}
icon={<Cpu className="h-4 w-4" />}
{...(modelsSubtitle ? { subtitle: modelsSubtitle } : {})}
/>
</DashboardMotionItem>
<DashboardMotionItem order={4}>
<MetricCard
label={t('metricCards.month.costPerMillion')}
value={<FormattedValue value={agg.costPerMillion} type="currency" />}
icon={<TrendingDown className="h-4 w-4" />}
{...(costPerMillionSubtitle ? { subtitle: costPerMillionSubtitle } : {})}
/>
</DashboardMotionItem>
<DashboardMotionItem order={5}>
<MetricCard
label={t('metricCards.month.cacheHitRate')}
value={<FormattedValue value={agg.cacheHitRate} type="percent" />}
Expand All @@ -194,6 +204,8 @@ export function MonthMetrics({ daily, metrics }: MonthMetricsProps) {
})}
icon={<Database className="h-4 w-4" />}
/>
</DashboardMotionItem>
<DashboardMotionItem order={6}>
<MetricCard
label={t('metricCards.month.requests')}
value={
Expand All @@ -220,14 +232,16 @@ export function MonthMetrics({ daily, metrics }: MonthMetricsProps) {
}
icon={<Activity className="h-4 w-4" />}
/>
</DashboardMotionItem>
<DashboardMotionItem order={7}>
<MetricCard
label={t('metricCards.month.thinking')}
value={<FormattedValue value={agg.thinkingTokens} type="tokens" />}
icon={<BrainCircuit className="h-4 w-4" />}
{...(thinkingSubtitle ? { subtitle: thinkingSubtitle } : {})}
/>
</div>
</FadeIn>
</DashboardMotionItem>
</div>
</div>
)
}
261 changes: 139 additions & 122 deletions src/components/cards/PrimaryMetrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
BrainCircuit,
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { DashboardMotionItem } from '@/components/dashboard/DashboardMotion'
import { MetricCard } from './MetricCard'
import { FormattedValue } from '@/components/ui/formatted-value'
import { formatCurrency, formatPercent, formatTokens, periodUnit } from '@/lib/formatters'
Expand Down Expand Up @@ -68,130 +69,146 @@ export function PrimaryMetrics({

return (
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4 xl:grid-cols-8">
<MetricCard
label={t('metricCards.primary.totalCost')}
value={
<FormattedValue
value={metrics.totalCost}
type="currency"
label={t('metricCards.primary.totalCost')}
insight={t('metricCards.primary.avgPerPeriod', {
value: formatCurrency(metrics.avgDailyCost),
unit: periodUnit(viewMode),
})}
/>
}
subtitle={t('metricCards.primary.totalCostSubtitle', {
average: formatCurrency(metrics.avgDailyCost),
unit: periodUnit(viewMode),
costPerRequest: formatCurrency(metrics.avgCostPerRequest),
})}
icon={<DollarSign className="h-4 w-4" />}
trend={metrics.weekOverWeekChange !== null ? { value: metrics.weekOverWeekChange } : null}
info={METRIC_HELP.totalCost}
/>
<MetricCard
label={t('metricCards.primary.totalTokens')}
value={
<FormattedValue
value={metrics.totalTokens}
type="tokens"
label={t('metricCards.primary.totalTokens')}
insight={t('metricCards.primary.tokensPerRequestAvg', {
value: formatTokens(metrics.avgTokensPerRequest),
})}
/>
}
subtitle={
ioRatio
? t('metricCards.primary.totalTokensSubtitleWithRatio', {
ratio: ioRatio,
tokensPerRequest: formatTokens(metrics.avgTokensPerRequest),
})
: t('metricCards.primary.totalTokensSubtitle', {
tokensPerRequest: formatTokens(metrics.avgTokensPerRequest),
})
}
icon={<Coins className="h-4 w-4" />}
info={METRIC_HELP.totalTokens}
/>
<MetricCard
label={t('metricCards.primary.activeDays')}
value={String(metrics.activeDays)}
subtitle={
coverageRate !== null
? t('metricCards.primary.coverageOfDays', {
coverage: formatPercent(coverageRate, 0),
days: totalCalendarDays,
})
: t('metricCards.primary.providersActive', { count: metrics.providerCount })
}
icon={<Calendar className="h-4 w-4" />}
info={METRIC_HELP.activeDays}
/>
<MetricCard
label={t('metricCards.primary.topModel')}
value={metrics.topModel?.name ?? '–'}
icon={<Cpu className="h-4 w-4" />}
info={METRIC_HELP.topModel}
{...(topModelSubtitle ? { subtitle: topModelSubtitle } : {})}
/>
<MetricCard
label={t('metricCards.primary.cacheHitRate')}
value={<FormattedValue value={metrics.cacheHitRate} type="percent" />}
icon={<Database className="h-4 w-4" />}
info={METRIC_HELP.cacheHitRate}
{...(cacheHitRateSubtitle ? { subtitle: cacheHitRateSubtitle } : {})}
/>
<MetricCard
label={t('metricCards.primary.costPerMillion')}
value={<FormattedValue value={metrics.costPerMillion} type="currency" />}
icon={<TrendingDown className="h-4 w-4" />}
info={METRIC_HELP.costPerMillion}
/>
<MetricCard
label={t('metricCards.primary.requests')}
value={
metrics.hasRequestData ? (
<DashboardMotionItem order={0}>
<MetricCard
label={t('metricCards.primary.totalCost')}
value={
<FormattedValue
value={metrics.totalRequests}
type="number"
label={t('metricCards.primary.requests')}
insight={t('insights.requestEconomy.summary', {
cost: formatCurrency(metrics.avgCostPerRequest),
tokens: formatTokens(metrics.avgTokensPerRequest),
leader: '',
}).trim()}
/>
) : (
t('common.notAvailable')
)
}
subtitle={
metrics.hasRequestData
? t('metricCards.primary.requestsSubtitle', {
requests: metrics.avgRequestsPerDay.toFixed(1),
value={metrics.totalCost}
type="currency"
label={t('metricCards.primary.totalCost')}
insight={t('metricCards.primary.avgPerPeriod', {
value: formatCurrency(metrics.avgDailyCost),
unit: periodUnit(viewMode),
cost: formatCurrency(metrics.avgCostPerRequest),
volatility: Math.round(metrics.requestVolatility),
})
: t('metricCards.primary.requestCountersMissing')
}
icon={<Activity className="h-4 w-4" />}
/>
<MetricCard
label={t('metricCards.primary.thinking')}
value={
<FormattedValue
value={metrics.totalThinking}
type="tokens"
label={t('metricCards.primary.thinking')}
{...(thinkingInsight ? { insight: thinkingInsight } : {})}
/>
}
icon={<BrainCircuit className="h-4 w-4" />}
{...(thinkingSubtitle ? { subtitle: thinkingSubtitle } : {})}
/>
})}
/>
}
subtitle={t('metricCards.primary.totalCostSubtitle', {
average: formatCurrency(metrics.avgDailyCost),
unit: periodUnit(viewMode),
costPerRequest: formatCurrency(metrics.avgCostPerRequest),
})}
icon={<DollarSign className="h-4 w-4" />}
trend={metrics.weekOverWeekChange !== null ? { value: metrics.weekOverWeekChange } : null}
info={METRIC_HELP.totalCost}
/>
</DashboardMotionItem>
<DashboardMotionItem order={1}>
<MetricCard
label={t('metricCards.primary.totalTokens')}
value={
<FormattedValue
value={metrics.totalTokens}
type="tokens"
label={t('metricCards.primary.totalTokens')}
insight={t('metricCards.primary.tokensPerRequestAvg', {
value: formatTokens(metrics.avgTokensPerRequest),
})}
/>
}
subtitle={
ioRatio
? t('metricCards.primary.totalTokensSubtitleWithRatio', {
ratio: ioRatio,
tokensPerRequest: formatTokens(metrics.avgTokensPerRequest),
})
: t('metricCards.primary.totalTokensSubtitle', {
tokensPerRequest: formatTokens(metrics.avgTokensPerRequest),
})
}
icon={<Coins className="h-4 w-4" />}
info={METRIC_HELP.totalTokens}
/>
</DashboardMotionItem>
<DashboardMotionItem order={2}>
<MetricCard
label={t('metricCards.primary.activeDays')}
value={String(metrics.activeDays)}
subtitle={
coverageRate !== null
? t('metricCards.primary.coverageOfDays', {
coverage: formatPercent(coverageRate, 0),
days: totalCalendarDays,
})
: t('metricCards.primary.providersActive', { count: metrics.providerCount })
}
icon={<Calendar className="h-4 w-4" />}
info={METRIC_HELP.activeDays}
/>
</DashboardMotionItem>
<DashboardMotionItem order={3}>
<MetricCard
label={t('metricCards.primary.topModel')}
value={metrics.topModel?.name ?? '–'}
icon={<Cpu className="h-4 w-4" />}
info={METRIC_HELP.topModel}
{...(topModelSubtitle ? { subtitle: topModelSubtitle } : {})}
/>
</DashboardMotionItem>
<DashboardMotionItem order={4}>
<MetricCard
label={t('metricCards.primary.cacheHitRate')}
value={<FormattedValue value={metrics.cacheHitRate} type="percent" />}
icon={<Database className="h-4 w-4" />}
info={METRIC_HELP.cacheHitRate}
{...(cacheHitRateSubtitle ? { subtitle: cacheHitRateSubtitle } : {})}
/>
</DashboardMotionItem>
<DashboardMotionItem order={5}>
<MetricCard
label={t('metricCards.primary.costPerMillion')}
value={<FormattedValue value={metrics.costPerMillion} type="currency" />}
icon={<TrendingDown className="h-4 w-4" />}
info={METRIC_HELP.costPerMillion}
/>
</DashboardMotionItem>
<DashboardMotionItem order={6}>
<MetricCard
label={t('metricCards.primary.requests')}
value={
metrics.hasRequestData ? (
<FormattedValue
value={metrics.totalRequests}
type="number"
label={t('metricCards.primary.requests')}
insight={t('insights.requestEconomy.summary', {
cost: formatCurrency(metrics.avgCostPerRequest),
tokens: formatTokens(metrics.avgTokensPerRequest),
leader: '',
}).trim()}
/>
) : (
t('common.notAvailable')
)
}
subtitle={
metrics.hasRequestData
? t('metricCards.primary.requestsSubtitle', {
requests: metrics.avgRequestsPerDay.toFixed(1),
unit: periodUnit(viewMode),
cost: formatCurrency(metrics.avgCostPerRequest),
volatility: Math.round(metrics.requestVolatility),
})
: t('metricCards.primary.requestCountersMissing')
}
icon={<Activity className="h-4 w-4" />}
/>
</DashboardMotionItem>
<DashboardMotionItem order={7}>
<MetricCard
label={t('metricCards.primary.thinking')}
value={
<FormattedValue
value={metrics.totalThinking}
type="tokens"
label={t('metricCards.primary.thinking')}
{...(thinkingInsight ? { insight: thinkingInsight } : {})}
/>
}
icon={<BrainCircuit className="h-4 w-4" />}
{...(thinkingSubtitle ? { subtitle: thinkingSubtitle } : {})}
/>
</DashboardMotionItem>
</div>
)
}
Loading
Loading