Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (10)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds editor/Prettier/ESLint configs, tightens CI and release workflows with format/lint/tsc gates and pinned actions, introduces spec-driven model normalization, refactors server CLI/port/exports and many client components/hooks for defensive checks, and updates scripts, types, and documentation to use new verify/build scripts. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 9
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/components/tables/ProviderEfficiency.tsx (1)
91-104:⚠️ Potential issue | 🟠 MajorSortable headers need keyboard and ARIA support.
<th onClick>is mouse-only. Use a button inside the header and addaria-sortto the<th>for assistive tech.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/tables/ProviderEfficiency.tsx` around lines 91 - 104, The SortHeader <th> currently uses onClick (mouse-only); move the interactive handler to a <button> inside SortHeader (call handleSort(field) from the button) and remove onClick from the <th>; add aria-sort on the <th> so assistive tech knows the column state (e.g., aria-sort={sortKey === field ? 'ascending' : 'none'}), ensure the button has an accessible label/focus styles and that ArrowUpDown remains inside the button; keep references to SortHeader, handleSort, sortKey and field when making the changes.src/components/features/settings/SettingsModal.tsx (1)
117-127:⚠️ Potential issue | 🟠 MajorDragging downward reorders to the wrong slot.
targetIndexis captured before the source item is removed. WhensourceIndex < targetIndex, droppingAontoCinserts it afterCinstead of atC's position.Suggested fix
function reorderSections( order: DashboardSectionOrder, sourceId: DashboardSectionOrder[number], targetId: DashboardSectionOrder[number], ) { if (sourceId === targetId) return order const sourceIndex = order.indexOf(sourceId) const targetIndex = order.indexOf(targetId) if (sourceIndex < 0 || targetIndex < 0) { return order } const next = [...order] const [moved] = next.splice(sourceIndex, 1) if (!moved) return order - next.splice(targetIndex, 0, moved) + const insertionIndex = sourceIndex < targetIndex ? targetIndex - 1 : targetIndex + next.splice(insertionIndex, 0, moved) return next }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/features/settings/SettingsModal.tsx` around lines 117 - 127, The bug is that targetIndex is calculated from order before removing the source, so when sourceIndex < targetIndex the subsequent splice inserts the moved item after the intended slot; fix by recalculating or adjusting targetIndex after removing the source: after creating next and doing next.splice(sourceIndex, 1) (the [moved] removal), either set targetIndex = next.indexOf(targetId) or if you prefer minimal change, decrement targetIndex by 1 when sourceIndex < targetIndex, then call next.splice(targetIndex, 0, moved); update the logic around sourceIndex, targetIndex, next, and moved accordingly.
🟡 Minor comments (16)
src/components/charts/CorrelationAnalysis.tsx-88-90 (1)
88-90:⚠️ Potential issue | 🟡 MinorFix inconsistent undefined check for tokens.
Line 89 uses a truthy check (
point.tokens ?) which will incorrectly display '–' whenpoint.tokensis0. This is inconsistent with the explicit undefined checks used forpoint.requests(lines 79, 110) andpoint.cacheRate(line 98).🐛 Proposed fix to use explicit undefined check
- <span className="font-mono font-medium"> - {point.tokens ? formatTokens(point.tokens) : '–'} - </span> + <span className="font-mono font-medium"> + {point.tokens !== undefined ? formatTokens(point.tokens) : '–'} + </span>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/charts/CorrelationAnalysis.tsx` around lines 88 - 90, Change the truthy check for tokens to an explicit undefined check so zero values render correctly: in the CorrelationAnalysis component replace the conditional on point.tokens (currently using point.tokens ? ...) with an explicit typeof or !== undefined check (matching how point.requests and point.cacheRate are handled) so formatTokens(point.tokens) is called for 0 and only '–' is shown when tokens is actually undefined.src/components/cards/SecondaryMetrics.tsx-63-66 (1)
63-66:⚠️ Potential issue | 🟡 MinorLocalize the hardcoded
σ Reqsuffix in subtitle text.This introduces untranslated UI text in localized flows.
Suggested fix
- const medianSubtitle = - median !== null && metrics.avgDailyCost > 0 - ? `${t('metricCards.secondary.vsAverage', { direction: median < metrics.avgDailyCost ? '↓' : '↑', value: Math.abs(((median - metrics.avgDailyCost) / metrics.avgDailyCost) * 100).toFixed(0) })} · σ Req ${Math.round(metrics.requestVolatility)}` - : null + const medianSubtitle = + median !== null && metrics.avgDailyCost > 0 + ? t('metricCards.secondary.medianSubtitle', { + direction: median < metrics.avgDailyCost ? '↓' : '↑', + value: Math.abs(((median - metrics.avgDailyCost) / metrics.avgDailyCost) * 100).toFixed(0), + volatility: Math.round(metrics.requestVolatility), + }) + : null🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/cards/SecondaryMetrics.tsx` around lines 63 - 66, The subtitle construction in medianSubtitle currently appends a hardcoded "· σ Req" suffix (using metrics.requestVolatility), which is not localized; update the translation usage to include that suffix (e.g., add a new i18n key such as 'metricCards.secondary.vsAverageWithStd' or include a placeholder in 'metricCards.secondary.vsAverage') and pass the rounded volatility value as a parameter instead of concatenating "· σ Req" in code — modify the medianSubtitle expression to call t(...) with the new key/placeholder and provide { direction, value, requestVolatility: Math.round(metrics.requestVolatility) } so the entire subtitle is translatable.src/components/cards/MonthMetrics.tsx-180-187 (1)
180-187:⚠️ Potential issue | 🟡 MinorLocalize the hardcoded “/ Request” suffix.
This user-facing English fragment bypasses i18n and can leak untranslated text.
Suggested fix
- insight={`${formatCurrency(agg.totalCost / agg.requestCount)} / Request`} + insight={t('metricCards.month.costPerRequest', { + value: formatCurrency(agg.totalCost / agg.requestCount), + })}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/cards/MonthMetrics.tsx` around lines 180 - 187, The hardcoded " / Request" suffix should be localized: in MonthMetrics.tsx (where FormattedValue is used) replace the string interpolation that appends " / Request" with a call to the i18n translator (t) and concatenate the formatted currency with a new translation key (e.g. metricCards.month.perRequest) so the suffix is translatable; add the new key to your locale files with appropriate translations and ensure you use the existing t function in the component when building the insight prop for FormattedValue.src/components/tables/ModelEfficiency.tsx-310-313 (1)
310-313:⚠️ Potential issue | 🟡 MinorRemove
cursor-pointerfrom non-clickable rows.The row styling suggests click behavior, but no row action exists.
💡 Suggested fix
- className="border-b border-border/50 even:bg-muted/5 hover:bg-muted/10 transition-colors cursor-pointer" + className="border-b border-border/50 even:bg-muted/5 hover:bg-muted/10 transition-colors"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/tables/ModelEfficiency.tsx` around lines 310 - 313, The table row in the ModelEfficiency component is styled with "cursor-pointer" but has no click behavior; remove "cursor-pointer" from the className on the <tr> element (the row rendered with key={model.name}) in ModelEfficiency.tsx so non-clickable rows do not imply interactivity.src/components/charts/ChartCard.tsx-247-257 (1)
247-257:⚠️ Potential issue | 🟡 MinorHardcoded labels introduce mixed-language UI in an otherwise translated component.
GesamtandDatenpunkteshould go through i18n keys like the rest of the card labels to keep locale consistency.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/charts/ChartCard.tsx` around lines 247 - 257, The ChartCard component contains hardcoded German labels "Gesamt" and "Datenpunkte" which breaks localization; replace these literals with the app's i18n calls (the same translation helper used elsewhere in this component, e.g., the local t/useTranslations) so the labels use keys like "charts.total" and "charts.datapoints" (or your project's existing keys), keeping the surrounding markup (the divs, classes, and fmt(stats.total)) intact; also add the new keys to the translation files for supported locales so strings render correctly.RELEASING.md-61-64 (1)
61-64:⚠️ Potential issue | 🟡 MinorIndent the verification command bullets under step 12.
These bullets should be nested under the numbered item to avoid markdown rendering them as a separate top-level list.
💡 Suggested doc fix
-12. verifies: - -- `npx --yes `@roastcodes/ttdash`@<version> --help` -- `bunx `@roastcodes/ttdash`@<version> --help` +12. verifies: + - `npx --yes `@roastcodes/ttdash`@<version> --help` + - `bunx `@roastcodes/ttdash`@<version> --help`🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@RELEASING.md` around lines 61 - 64, Indent the two verification lines so they are nested under the numbered item "12. verifies:" by adding a consistent list indent (e.g., four spaces or a tab) before each bullet (`- `npx --yes `@roastcodes/ttdash`@<version> --help`` and `- `bunx `@roastcodes/ttdash`@<version> --help``) so they render as sub-bullets under step 12 rather than a separate top-level list.src/components/features/forecast/CostForecast.tsx-186-190 (1)
186-190:⚠️ Potential issue | 🟡 MinorRemove accidental
~text node from forecast value rendering.Line 188 currently includes a leading
~, which will render in the UI before the currency value.Proposed fix
- value={ - <> - ~<FormattedValue value={forecastTotal} type="currency" /> - </> - } + value={<FormattedValue value={forecastTotal} type="currency" />}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/features/forecast/CostForecast.tsx` around lines 186 - 190, The CostForecast component is rendering an accidental literal tilde text node in the value prop (the fragment containing "~<FormattedValue .../>"); remove the leading "~" text so the value prop only renders the FormattedValue (and you can also drop the unnecessary fragment if present) in the JSX where value is set in CostForecast.CHANGELOG.md-5-5 (1)
5-5:⚠️ Potential issue | 🟡 MinorUse
Unreleasedor the actual release date here.This entry is dated 2026-04-13, but the PR is still open on 2026-04-12. Future-dating the release makes the changelog inaccurate until the tag is actually cut.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CHANGELOG.md` at line 5, The changelog header "## [6.1.6] - 2026-04-13" is future-dated; update that header to either use "Unreleased" or the actual release date (e.g., "## [6.1.6] - Unreleased") so the entry reflects the PR's current state and is not dated ahead of the tag being cut.src/components/features/drill-down/DrillDownModal.tsx-149-152 (1)
149-152:⚠️ Potential issue | 🟡 MinorGuard zero-token days before dividing by
day.totalTokens.If a day has
0tokens, the$ /1Mcard rendersInfinity/NaN, and the legend percentages renderNaN%even though the stacked bar is hidden. Please short-circuit these displays to0or–.Also applies to: 253-288
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/features/drill-down/DrillDownModal.tsx` around lines 149 - 152, In DrillDownModal, guard all divisions by day.totalTokens (e.g., the FormattedValue calculation value={day.totalCost / (day.totalTokens / 1_000_000)}) and any legend percentage computations so they short-circuit when day.totalTokens is 0 or falsy; replace the resulting value with 0 or a placeholder like '–' (or pass null/undefined to FormattedValue) instead of performing the division. Locate usages of day.totalCost and day.totalTokens in the component (including the $/1M card and the legend percentage calculations) and wrap them with a conditional such as day.totalTokens ? (day.totalCost / (day.totalTokens / 1_000_000)) : fallback, and do the same for each percent formula so Infinity/NaN are never passed to rendering. Ensure the fallback type matches what FormattedValue and the legend expect (number or placeholder string).src/lib/formatters.ts-124-127 (1)
124-127:⚠️ Potential issue | 🟡 MinorAvoid defaulting malformed month strings to January.
This change makes inputs like
'2026'format as January 2026, which hides an invalid period key behind a plausible label. It would be safer to validateYYYY-MMexplicitly and return an empty string or the original input when parsing fails.🩹 Proposed fix
export function formatMonthYear(dateStr: string): string { - const [year = '0', month = '1'] = dateStr.split('-') + if (!/^\d{4}-\d{2}$/.test(dateStr)) return '' + const [year, month] = dateStr.split('-') const date = new Date(parseInt(year, 10), parseInt(month, 10) - 1) return date.toLocaleDateString(getCurrentLocale(), { month: 'long', year: 'numeric' }) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/formatters.ts` around lines 124 - 127, The formatMonthYear function currently defaults a missing month to January (making "2026" format as Jan 2026); instead, validate the input explicitly (e.g., with a YYYY-MM regex) before parsing and if the input doesn't match return an empty string or the original input as desired; update formatMonthYear to check the pattern for year and month, avoid using default values when splitting, and only construct a Date and call toLocaleDateString when the validation passes.src/components/tables/RecentDays.tsx-176-180 (1)
176-180:⚠️ Potential issue | 🟡 MinorKeep the peak cost on the shared currency formatter.
This hardcodes
USDand bypasses the locale-aware formatting used everywhere else in the table, so the peak row can drift from the rest of the UI. ReuseformatCurrency(...)orFormattedValuehere as well.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/tables/RecentDays.tsx` around lines 176 - 180, The peak row currently hardcodes "USD" and bypasses the app's locale-aware formatting; replace the literal `${summary.top.totalCost.toFixed(2)} USD` with the shared currency formatter (e.g., call formatCurrency(summary.top.totalCost, summary.top.currency?) or render <FormattedValue value={summary.top.totalCost} currency={...}>) so the peak cost uses the same locale/currency logic as the rest of the table; locate this in the RecentDays component where summary.top is accessed (near formatDate(summary.top.date)) and use the existing formatCurrency or FormattedValue helper consistent with other rows.src/components/layout/FilterBar.tsx-239-257 (1)
239-257:⚠️ Potential issue | 🟡 MinorLocalize the calendar navigation labels.
aria-label="Previous month"/"Next month"stay English even when the rest of the picker is translated, so screen-reader users get a mixed-language control. These should come fromt(...)like the other date-picker copy.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/layout/FilterBar.tsx` around lines 239 - 257, Replace the hard-coded English aria labels for the month navigation in FilterBar.tsx so they use the i18n translation function (same one used elsewhere in this component) instead of static strings; update the left button aria-label and the right button aria-label to call t(...) (e.g., t('previousMonth') / t('nextMonth') or your existing calendar keys) where the buttons are defined alongside the setDisplayMonth callbacks and monthLabel so screen readers receive localized labels.src/components/features/limits/ProviderLimitsSection.tsx-71-80 (1)
71-80:⚠️ Potential issue | 🟡 MinorKeep the badge localized and preserve subscription ratios above 100%.
The new helper hardcodes
Limit/Sub/Offen, and the caller now clampssubscriptionProgressto100before passing it in. That means providers above break-even all render as100% Sub, even though the helper clearly expects a higher cap. Use translated badge labels and pass the raw ratio for the badge while keeping a separately clamped value for the progress bar width.Also applies to: 299-302, 333-333
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/features/limits/ProviderLimitsSection.tsx` around lines 71 - 80, formatLimitBadge currently hardcodes English/German strings and assumes subscriptionProgress is clamped; update it to use the app i18n/localization instead of literal "Limit"/"Sub"/"Offen" and stop clamping the subscription ratio inside the helper so values >100% are preserved (accept the raw subscriptionProgress number and render Math.min(...) only where the progress bar width is computed). Locate the function formatLimitBadge and change badge labels to use the translation keys (e.g., t('limits.badge.limit'), t('limits.badge.subscription'), t('limits.badge.off')) and ensure any clamping to 100 for UI width happens in the progress bar rendering code (the caller code around the progress bar, not formatLimitBadge); also apply the same change pattern for the other occurrences referenced (around the other similar badge lines).src/components/features/limits/ProviderLimitsSection.tsx-923-939 (1)
923-939:⚠️ Potential issue | 🟡 MinorAvoid pluralizing translated legend labels with string concatenation.
${t(...)}sonly works in English. In other locales it will produce awkward or incorrect legend text. These legend names should come from dedicated translation keys instead of appending's'in code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/features/limits/ProviderLimitsSection.tsx` around lines 923 - 939, The legend labels in ProviderLimitsSection.tsx currently build plurals by appending "s" to t('limits.tracks.limit') and t('limits.tracks.subscription') inside the Line components (dataKey "totalLimit" and "totalSubscriptions"), which breaks i18n; replace these string-concatenated names with proper translated strings—either add dedicated keys like t('limits.tracks.limits') and t('limits.tracks.subscriptions') or use the i18n pluralization form t('limits.tracks.limit', { count: n }) (or analogous plural key for subscription) and pass an appropriate count/variable so the legend names come from translation keys instead of using `${t(...)}s`. Ensure you update both Line name props referenced above.src/lib/help-content.ts-245-255 (1)
245-255:⚠️ Potential issue | 🟡 MinorReturn
undefinedfor missing keys in the proxy descriptor trap.
getOwnPropertyDescriptorcurrently reports every queried key as an own property, even when it does not exist in the selected help map. That breaks normalObject.hasOwn()/hasOwnProperty()semantics on these exported proxies.Suggested fix
function dynamicMap<const T extends Record<string, string>>(selector: () => T): T { return new Proxy({} as T, { get: (_, key) => Reflect.get(selector(), key), has: (_, key) => key in selector(), ownKeys: () => Reflect.ownKeys(selector()), - getOwnPropertyDescriptor: (_, key) => ({ - value: Reflect.get(selector(), key), - enumerable: true, - configurable: true, - }), + getOwnPropertyDescriptor: (_, key) => + key in selector() + ? { + value: Reflect.get(selector(), key), + enumerable: true, + configurable: true, + } + : undefined, }) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/help-content.ts` around lines 245 - 255, The getOwnPropertyDescriptor trap in dynamicMap incorrectly returns a descriptor for every key; modify it to first fetch the current map (const map = selector()) and check ownership with Object.prototype.hasOwnProperty.call(map, key) (or return undefined for non-own keys), returning undefined if the property does not exist, otherwise return the existing descriptor ({ value: Reflect.get(map, key), enumerable: true, configurable: true }); keep the other traps as-is and use the same selector() result to avoid calling selector() multiple times.src/lib/data-transforms.ts-35-47 (1)
35-47:⚠️ Potential issue | 🟡 Minor
modelsUsedloses set semantics after filtering.This rebuild copies every breakdown name into
modelsUsed, so duplicate/aliased breakdown rows inflatemodelsUsed.length.computeMetricsuses that length foravgModelsPerEntry, so filtered views can overreport the metric.Suggested fix
return { ...day, totalCost, totalTokens: inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens + thinkingTokens, @@ thinkingTokens, requestCount, modelBreakdowns: filteredBreakdowns, - modelsUsed: filteredBreakdowns.map((mb) => mb.modelName), + modelsUsed: [...new Set(filteredBreakdowns.map((mb) => normalizeModelName(mb.modelName)))], } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/data-transforms.ts` around lines 35 - 47, The modelsUsed array returned from the transform currently maps filteredBreakdowns to a plain array allowing duplicates, which inflates counts used by computeMetrics (avgModelsPerEntry); change the return so modelsUsed is deduplicated (preserve order if needed) from filteredBreakdowns — e.g., derive modelsUsed from filteredBreakdowns.map(mb => mb.modelName) and convert to a unique collection (Set or Array.from(new Set(...))) before returning, updating the symbol modelsUsed in this function and ensuring computeMetrics still consumes it as an array.
🧹 Nitpick comments (8)
src/index.css (1)
3-3: Note: Static analysis false positive.The stylelint error flagging
@themeas an unknown at-rule is a false positive.@themeis a valid Tailwind CSS v4 directive and was present before this PR. Consider updating the stylelint configuration to recognize Tailwind v4 syntax if this warning causes confusion.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/index.css` at line 3, The stylelint warning about the `@theme` at-rule is a false positive for Tailwind CSS v4; update the stylelint configuration (e.g., the rule that validates unknown at-rules) to recognize Tailwind v4 directives so `@theme` is allowed. Modify the stylelint config (the rule like "at-rules/no-unknown" or add the tailwind plugin/ignore at-rules list) to include "theme" or enable the official Tailwind parser/plugin so the `@theme` at-rule used in src/index.css is not flagged.src/components/charts/CorrelationAnalysis.tsx (2)
285-291: Consider extracting nested ternaries to helper functions.The three-level nested ternaries for correlation interpretation are logically correct but could be more readable as helper functions or simple if-else chains.
♻️ Example refactor using helper function
function getCorrelationInterpretation( correlation: number, type: 'requestCost' | 'cacheEfficiency' ): string { const { t } = useTranslation() if (type === 'requestCost') { if (correlation >= 0.6) return t('charts.correlation.strongRequestCost') if (correlation >= 0.3) return t('charts.correlation.mediumRequestCost') return t('charts.correlation.weakRequestCost') } else { if (correlation <= -0.3) return t('charts.correlation.negativeCache') if (correlation < 0.2) return t('charts.correlation.neutralCache') return t('charts.correlation.positiveCache') } }Then use:
footer={getCorrelationInterpretation(requestCostCorrelation, 'requestCost')}Also applies to: 305-311
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/charts/CorrelationAnalysis.tsx` around lines 285 - 291, Extract the nested ternary logic used in the footer props into a small helper function (e.g., getCorrelationInterpretation) that takes the correlation number and a type flag ('requestCost' | 'cacheEfficiency') and returns the appropriate translated string using the existing useTranslation hook or by accepting t as an argument; replace the inline ternaries that compute footer for requestCostCorrelation and the one for cacheEfficiencyCorrelation with calls to this helper (reference requestCostCorrelation, cacheEfficiencyCorrelation, and the component CorrelationAnalysis or its rendering of footer).
170-177: Consider simplifying the conditional tickFormatter spread.React and Recharts handle
undefinedprops gracefully, so the conditional spread{...(xTickFormatter ? { tickFormatter: xTickFormatter } : {})}adds unnecessary complexity. A direct prop assignment would be more idiomatic.♻️ Proposed simplification
<XAxis type="number" dataKey="x" stroke={CHART_COLORS.axis} fontSize={10} tickLine={false} name={xAxisName} - {...(xTickFormatter ? { tickFormatter: xTickFormatter } : {})} + tickFormatter={xTickFormatter} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/charts/CorrelationAnalysis.tsx` around lines 170 - 177, The XAxis prop uses an unnecessary conditional spread to set tickFormatter; simplify by assigning tickFormatter directly (replace the {...(xTickFormatter ? { tickFormatter: xTickFormatter } : {})} pattern with a direct tickFormatter prop) so XAxis receives tickFormatter={xTickFormatter} and React/Recharts can handle undefined values; locate the XAxis component in CorrelationAnalysis.tsx (symbols: XAxis, xTickFormatter, xAxisName) and update accordingly.src/components/features/anomaly/AnomalyDetection.tsx (1)
65-67: Prefer non-mutating sort in render path.
anomalies.sort(...)mutates the memoized array. Use a copied array before sorting to keep render data flow immutable.Suggested diff
- {anomalies - .sort((a, b) => b.totalCost - a.totalCost) + {[...anomalies] + .sort((a, b) => b.totalCost - a.totalCost) .map((day) => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/features/anomaly/AnomalyDetection.tsx` around lines 65 - 67, The render currently calls anomalies.sort(...), which mutates the memoized anomalies array; instead create a non-mutating copy before sorting (e.g., using slice() or spread) and call sort on that copy so the original anomalies value from the hook/memo isn't mutated; update the expression in AnomalyDetection where anomalies.sort((a,b) => b.totalCost - a.totalCost).map(...) is used to use a copied array (e.g., [...anomalies] or anomalies.slice()) before .sort and then .map.src/components/ui/card.tsx (1)
5-20: Prevent accidental override of Card motion defaults.Line 18 spreads
{...props}after the motion.div animation props, allowing consumers to unintentionally overrideinitial,whileInView,viewport, ortransition. While no current callers do this, the safer pattern is to spread consumer props before the built-in animation props to preserve the animation contract.Suggested fix
const Card = React.forwardRef<HTMLDivElement, CardProps>(({ className, ...props }, ref) => ( <motion.div ref={ref} + {...props} initial={{ opacity: 0, y: 14 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, amount: 0.15 }} transition={{ duration: 0.35, ease: 'easeOut' }} className={cn( 'relative rounded-xl border border-border/50 bg-card/80 backdrop-blur-xl text-card-foreground shadow-[var(--shadow-card)] transition-all duration-300 hover:shadow-[var(--shadow-card-hover)] hover:border-border/80', className, )} - {...props} /> ))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/card.tsx` around lines 5 - 20, The Card component currently spreads {...props} after the motion.div animation props which allows consumers to override built-in animation settings; move the spread of consumer props so they are applied before the animation props (i.e., spread {...props} earlier on the motion.div and then set initial, whileInView, viewport, transition, className, ref explicitly) to ensure Card's initial/whileInView/viewport/transition cannot be overridden by callers while still forwarding ref and merging className..github/workflows/release.yml (1)
145-147: Pin Bun to a fixed version for reproducible releases.
bun-version: latestcan cause release results to drift over time and introduce non-deterministic failures. Use a specific version instead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/release.yml around lines 145 - 147, The workflow currently uses the setup-bun action with bun-version: latest which makes releases non-reproducible; change the bun-version value to a specific fixed Bun release (e.g., a concrete semver like 1.x.y) and pin the setup action if desired (the current uses: oven-sh/setup-bun@0c5077... may remain or be updated to a stable tag), so update the bun-version field from "latest" to a specific version string to ensure deterministic builds.src/components/charts/ModelMix.tsx (1)
126-143: Avoid repeated lookup in series animation delay.You can use the map index directly instead of
models.indexOf(model)for cleaner and slightly cheaper rendering logic.♻️ Suggested refactor
- {models.map((model) => { + {models.map((model, index) => { const color = getModelColor(model) const id = `mix-grad-${model.replace(/[\s.]/g, '-')}` return ( <Area @@ - animationBegin={CHART_ANIMATION.stagger * (models.indexOf(model) % 5)} + animationBegin={CHART_ANIMATION.stagger * (index % 5)}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/charts/ModelMix.tsx` around lines 126 - 143, The animation delay currently recomputes the model index with models.indexOf(model) inside the models.map loop; update the mapping to use the map callback index (e.g., change models.map((model) => { ... }) to models.map((model, idx) => { ... }) and replace models.indexOf(model) with idx when computing animationBegin) so the Area rendering (animationBegin property) uses the direct index and avoids repeated lookups; keep the rest of the Area props (type, dataKey, stackId, stroke, fill, name, isAnimationActive, animationDuration, animationEasing) unchanged.src/components/charts/CostByModelOverTime.tsx (1)
69-84: Use map index for animation delay in both model loops.Same as other chart files: this avoids repeated
indexOflookups and keeps intent clearer.♻️ Suggested refactor
- {models.map((model) => ( + {models.map((model, index) => ( <Line @@ - animationBegin={CHART_ANIMATION.stagger * (models.indexOf(model) % 5)} + animationBegin={CHART_ANIMATION.stagger * (index % 5)} @@ - {models.map((model) => ( + {models.map((model, index) => ( <Line @@ - animationBegin={CHART_ANIMATION.stagger * (models.indexOf(model) % 5)} + animationBegin={CHART_ANIMATION.stagger * (index % 5)}Also applies to: 136-150
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/charts/CostByModelOverTime.tsx` around lines 69 - 84, The animation delay currently computes animationBegin using models.indexOf(model) inside the models.map, which is inefficient and brittle; update both model iteration sites (the map that renders <Line key={`${model}_ma7`} ... /> and the other models.map around lines 136-150) to use the map callback index parameter (e.g., (model, idx) => ...) and replace models.indexOf(model) with that idx when calculating animationBegin (still using CHART_ANIMATION.stagger * (idx % 5)); keep all other props (dataKey, key, stroke, etc.) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@package.json`:
- Around line 36-39: The npm scripts "verify" and "verify:release" currently
invoke tsc using an explicit ./node_modules/.bin/tsc path; remove the hardcoded
path and call tsc directly in both script definitions so npm's PATH resolution
is used (update the "verify" script and the "verify:release" script to replace
"./node_modules/.bin/tsc --noEmit" with "tsc --noEmit").
In `@server/model-normalization.json`:
- Around line 3-10: The alias regexes are consuming the trailing hyphen so
suffixed variants (e.g., gpt-5-4-codex) get flattened; update each pattern to
assert the token boundary with a lookahead instead of consuming the hyphen:
replace occurrences of ($|-) with (?=$|-) (and for the gpt-5 pattern keep the
existing negative-digit check as (?=$|-(?!\d))). Apply this change to the listed
patterns (e.g., the patterns for "gpt-5-4", "gpt-5", "opus-4-6", "opus-4-5",
"sonnet-4-6", "sonnet-4-5", "haiku-4-5", "gemini-3-flash-preview") so aliases
match the token but do not remove subsequent suffixes.
In `@src/components/cards/TodayMetrics.tsx`:
- Around line 63-67: The branch in requestsSubtitle reads
today.modelsUsed.length and divides by it, which can throw if modelsUsed is
null/undefined; update the logic in the requestsSubtitle computation
(referencing today and requestsSubtitle) to use a null-safe count (e.g., compute
const modelsCount = Array.isArray(today.modelsUsed) ? today.modelsUsed.length :
0 or use today.modelsUsed?.length ?? 0) and guard the division with modelsCount
> 0 before computing the average and before formatting to avoid NaN/throwing;
ensure the ternary condition uses modelsCount instead of today.modelsUsed.length
and handle zero counts to keep the existing fallback behavior.
In `@src/components/charts/ChartCard.tsx`:
- Around line 33-49: The CSV export breaks when cell text contains commas,
quotes or newlines because stringifyCsvCell returns raw strings that are later
joined with commas; update stringifyCsvCell to CSV-escape and quote values by
converting the value to a string (as currently done), then wrap it in double
quotes and replace any " with "" (double them) so internal quotes are escaped
and commas/newlines are preserved inside the quoted field; apply the same
quoting/escaping logic to the other export path referenced around the join(',')
usage (the block at 155-158) so all CSV rows use consistent CSV-quoted cells.
In `@src/components/features/settings/SettingsModal.tsx`:
- Around line 200-203: The code builds nextProviderLimits from the existing
limits, which preserves providers that are no longer in limitProviders; change
the construction so nextProviderLimits only contains entries for the current
limitProviders (e.g., initialize nextProviderLimits as an empty object and then
add for each provider in limitProviders: nextProviderLimits[provider] =
limitDraft[provider] ?? { ...DEFAULT_PROVIDER_LIMIT_CONFIG }). Update the logic
in SettingsModal where nextProviderLimits, limits, limitProviders, and
limitDraft are used so a save or reset replaces the provider map entirely (and
thus clears stale entries) rather than merging onto the old limits.
In `@src/components/tables/ModelEfficiency.tsx`:
- Around line 138-150: The SortHeader component currently attaches onClick to
the <th>, which is not keyboard-accessible and does not expose sort state;
update SortHeader to render a <th> containing a <button> that receives the
onClick handler (handleSort) and visual classes (instead of the <th>), make the
button focusable, and set aria-sort on the <th> (or the button if you prefer)
based on sortKey and the passed field (use 'none' when sortKey !== field, and
'ascending' or 'descending' when it matches — map your existing sort direction
state if you have one, or add one alongside sortKey). Ensure unique symbols
referenced: SortHeader, SortKey, sortKey, handleSort, and ArrowUpDown.
In `@src/components/tables/RecentDays.tsx`:
- Around line 289-358: The sortable column headers (those using sortKey and
handleSort and showing ArrowUpDown) must use a real button inside the th for
keyboard focus and activation; move the onClick from the th into a <button>
placed around the label/ArrowUpDown (preserve the cn classes for styling/focus)
and call handleSort('date'|'cost'|'tokens'|'costPerM') from the button; also add
aria-sort on the th (values 'none' or 'ascending'/'descending' derived from
sortKey and your sortDirection state) so screen readers announce sort state, and
ensure the button has an accessible name (use the translated label or
aria-label) while keeping ArrowUpDown as a decorative icon.
In `@src/lib/auto-import.ts`:
- Around line 24-31: parseEventData currently throws on malformed JSON and
accepts primitives/arrays which break downstream handlers; update parseEventData
to catch JSON.parse errors and return null on parse failure, then validate the
parsed value is a non-null plain object (reject primitives and arrays using
typeof === 'object' && value !== null && !Array.isArray(value)) before casting
to T; keep the MessageEvent/type checks, and only return the cast value when
these guards pass to ensure handlers can safely access properties like message
or line.
In `@src/lib/formatters.ts`:
- Around line 18-29: coerceNumber currently maps invalid inputs (undefined, NaN,
±Infinity) to 0 which causes bad data to display as legitimate zero values;
change coerceNumber to return a nullable type (e.g., number | null) and return
null for any non-finite or non-parsable inputs instead of 0, then update the
axis/value formatter call sites that consume coerceNumber to treat null as "no
value" (render blank or skip the label) rather than formatting it as $0.00/0%;
reference the coerceNumber function and the axis/value formatter call sites so
they consistently handle nulls.
---
Outside diff comments:
In `@src/components/features/settings/SettingsModal.tsx`:
- Around line 117-127: The bug is that targetIndex is calculated from order
before removing the source, so when sourceIndex < targetIndex the subsequent
splice inserts the moved item after the intended slot; fix by recalculating or
adjusting targetIndex after removing the source: after creating next and doing
next.splice(sourceIndex, 1) (the [moved] removal), either set targetIndex =
next.indexOf(targetId) or if you prefer minimal change, decrement targetIndex by
1 when sourceIndex < targetIndex, then call next.splice(targetIndex, 0, moved);
update the logic around sourceIndex, targetIndex, next, and moved accordingly.
In `@src/components/tables/ProviderEfficiency.tsx`:
- Around line 91-104: The SortHeader <th> currently uses onClick (mouse-only);
move the interactive handler to a <button> inside SortHeader (call
handleSort(field) from the button) and remove onClick from the <th>; add
aria-sort on the <th> so assistive tech knows the column state (e.g.,
aria-sort={sortKey === field ? 'ascending' : 'none'}), ensure the button has an
accessible label/focus styles and that ArrowUpDown remains inside the button;
keep references to SortHeader, handleSort, sortKey and field when making the
changes.
---
Minor comments:
In `@CHANGELOG.md`:
- Line 5: The changelog header "## [6.1.6] - 2026-04-13" is future-dated; update
that header to either use "Unreleased" or the actual release date (e.g., "##
[6.1.6] - Unreleased") so the entry reflects the PR's current state and is not
dated ahead of the tag being cut.
In `@RELEASING.md`:
- Around line 61-64: Indent the two verification lines so they are nested under
the numbered item "12. verifies:" by adding a consistent list indent (e.g., four
spaces or a tab) before each bullet (`- `npx --yes `@roastcodes/ttdash`@<version>
--help`` and `- `bunx `@roastcodes/ttdash`@<version> --help``) so they render as
sub-bullets under step 12 rather than a separate top-level list.
In `@src/components/cards/MonthMetrics.tsx`:
- Around line 180-187: The hardcoded " / Request" suffix should be localized: in
MonthMetrics.tsx (where FormattedValue is used) replace the string interpolation
that appends " / Request" with a call to the i18n translator (t) and concatenate
the formatted currency with a new translation key (e.g.
metricCards.month.perRequest) so the suffix is translatable; add the new key to
your locale files with appropriate translations and ensure you use the existing
t function in the component when building the insight prop for FormattedValue.
In `@src/components/cards/SecondaryMetrics.tsx`:
- Around line 63-66: The subtitle construction in medianSubtitle currently
appends a hardcoded "· σ Req" suffix (using metrics.requestVolatility), which is
not localized; update the translation usage to include that suffix (e.g., add a
new i18n key such as 'metricCards.secondary.vsAverageWithStd' or include a
placeholder in 'metricCards.secondary.vsAverage') and pass the rounded
volatility value as a parameter instead of concatenating "· σ Req" in code —
modify the medianSubtitle expression to call t(...) with the new key/placeholder
and provide { direction, value, requestVolatility:
Math.round(metrics.requestVolatility) } so the entire subtitle is translatable.
In `@src/components/charts/ChartCard.tsx`:
- Around line 247-257: The ChartCard component contains hardcoded German labels
"Gesamt" and "Datenpunkte" which breaks localization; replace these literals
with the app's i18n calls (the same translation helper used elsewhere in this
component, e.g., the local t/useTranslations) so the labels use keys like
"charts.total" and "charts.datapoints" (or your project's existing keys),
keeping the surrounding markup (the divs, classes, and fmt(stats.total)) intact;
also add the new keys to the translation files for supported locales so strings
render correctly.
In `@src/components/charts/CorrelationAnalysis.tsx`:
- Around line 88-90: Change the truthy check for tokens to an explicit undefined
check so zero values render correctly: in the CorrelationAnalysis component
replace the conditional on point.tokens (currently using point.tokens ? ...)
with an explicit typeof or !== undefined check (matching how point.requests and
point.cacheRate are handled) so formatTokens(point.tokens) is called for 0 and
only '–' is shown when tokens is actually undefined.
In `@src/components/features/drill-down/DrillDownModal.tsx`:
- Around line 149-152: In DrillDownModal, guard all divisions by day.totalTokens
(e.g., the FormattedValue calculation value={day.totalCost / (day.totalTokens /
1_000_000)}) and any legend percentage computations so they short-circuit when
day.totalTokens is 0 or falsy; replace the resulting value with 0 or a
placeholder like '–' (or pass null/undefined to FormattedValue) instead of
performing the division. Locate usages of day.totalCost and day.totalTokens in
the component (including the $/1M card and the legend percentage calculations)
and wrap them with a conditional such as day.totalTokens ? (day.totalCost /
(day.totalTokens / 1_000_000)) : fallback, and do the same for each percent
formula so Infinity/NaN are never passed to rendering. Ensure the fallback type
matches what FormattedValue and the legend expect (number or placeholder
string).
In `@src/components/features/forecast/CostForecast.tsx`:
- Around line 186-190: The CostForecast component is rendering an accidental
literal tilde text node in the value prop (the fragment containing
"~<FormattedValue .../>"); remove the leading "~" text so the value prop only
renders the FormattedValue (and you can also drop the unnecessary fragment if
present) in the JSX where value is set in CostForecast.
In `@src/components/features/limits/ProviderLimitsSection.tsx`:
- Around line 71-80: formatLimitBadge currently hardcodes English/German strings
and assumes subscriptionProgress is clamped; update it to use the app
i18n/localization instead of literal "Limit"/"Sub"/"Offen" and stop clamping the
subscription ratio inside the helper so values >100% are preserved (accept the
raw subscriptionProgress number and render Math.min(...) only where the progress
bar width is computed). Locate the function formatLimitBadge and change badge
labels to use the translation keys (e.g., t('limits.badge.limit'),
t('limits.badge.subscription'), t('limits.badge.off')) and ensure any clamping
to 100 for UI width happens in the progress bar rendering code (the caller code
around the progress bar, not formatLimitBadge); also apply the same change
pattern for the other occurrences referenced (around the other similar badge
lines).
- Around line 923-939: The legend labels in ProviderLimitsSection.tsx currently
build plurals by appending "s" to t('limits.tracks.limit') and
t('limits.tracks.subscription') inside the Line components (dataKey "totalLimit"
and "totalSubscriptions"), which breaks i18n; replace these string-concatenated
names with proper translated strings—either add dedicated keys like
t('limits.tracks.limits') and t('limits.tracks.subscriptions') or use the i18n
pluralization form t('limits.tracks.limit', { count: n }) (or analogous plural
key for subscription) and pass an appropriate count/variable so the legend names
come from translation keys instead of using `${t(...)}s`. Ensure you update both
Line name props referenced above.
In `@src/components/layout/FilterBar.tsx`:
- Around line 239-257: Replace the hard-coded English aria labels for the month
navigation in FilterBar.tsx so they use the i18n translation function (same one
used elsewhere in this component) instead of static strings; update the left
button aria-label and the right button aria-label to call t(...) (e.g.,
t('previousMonth') / t('nextMonth') or your existing calendar keys) where the
buttons are defined alongside the setDisplayMonth callbacks and monthLabel so
screen readers receive localized labels.
In `@src/components/tables/ModelEfficiency.tsx`:
- Around line 310-313: The table row in the ModelEfficiency component is styled
with "cursor-pointer" but has no click behavior; remove "cursor-pointer" from
the className on the <tr> element (the row rendered with key={model.name}) in
ModelEfficiency.tsx so non-clickable rows do not imply interactivity.
In `@src/components/tables/RecentDays.tsx`:
- Around line 176-180: The peak row currently hardcodes "USD" and bypasses the
app's locale-aware formatting; replace the literal
`${summary.top.totalCost.toFixed(2)} USD` with the shared currency formatter
(e.g., call formatCurrency(summary.top.totalCost, summary.top.currency?) or
render <FormattedValue value={summary.top.totalCost} currency={...}>) so the
peak cost uses the same locale/currency logic as the rest of the table; locate
this in the RecentDays component where summary.top is accessed (near
formatDate(summary.top.date)) and use the existing formatCurrency or
FormattedValue helper consistent with other rows.
In `@src/lib/data-transforms.ts`:
- Around line 35-47: The modelsUsed array returned from the transform currently
maps filteredBreakdowns to a plain array allowing duplicates, which inflates
counts used by computeMetrics (avgModelsPerEntry); change the return so
modelsUsed is deduplicated (preserve order if needed) from filteredBreakdowns —
e.g., derive modelsUsed from filteredBreakdowns.map(mb => mb.modelName) and
convert to a unique collection (Set or Array.from(new Set(...))) before
returning, updating the symbol modelsUsed in this function and ensuring
computeMetrics still consumes it as an array.
In `@src/lib/formatters.ts`:
- Around line 124-127: The formatMonthYear function currently defaults a missing
month to January (making "2026" format as Jan 2026); instead, validate the input
explicitly (e.g., with a YYYY-MM regex) before parsing and if the input doesn't
match return an empty string or the original input as desired; update
formatMonthYear to check the pattern for year and month, avoid using default
values when splitting, and only construct a Date and call toLocaleDateString
when the validation passes.
In `@src/lib/help-content.ts`:
- Around line 245-255: The getOwnPropertyDescriptor trap in dynamicMap
incorrectly returns a descriptor for every key; modify it to first fetch the
current map (const map = selector()) and check ownership with
Object.prototype.hasOwnProperty.call(map, key) (or return undefined for non-own
keys), returning undefined if the property does not exist, otherwise return the
existing descriptor ({ value: Reflect.get(map, key), enumerable: true,
configurable: true }); keep the other traps as-is and use the same selector()
result to avoid calling selector() multiple times.
---
Nitpick comments:
In @.github/workflows/release.yml:
- Around line 145-147: The workflow currently uses the setup-bun action with
bun-version: latest which makes releases non-reproducible; change the
bun-version value to a specific fixed Bun release (e.g., a concrete semver like
1.x.y) and pin the setup action if desired (the current uses:
oven-sh/setup-bun@0c5077... may remain or be updated to a stable tag), so update
the bun-version field from "latest" to a specific version string to ensure
deterministic builds.
In `@src/components/charts/CorrelationAnalysis.tsx`:
- Around line 285-291: Extract the nested ternary logic used in the footer props
into a small helper function (e.g., getCorrelationInterpretation) that takes the
correlation number and a type flag ('requestCost' | 'cacheEfficiency') and
returns the appropriate translated string using the existing useTranslation hook
or by accepting t as an argument; replace the inline ternaries that compute
footer for requestCostCorrelation and the one for cacheEfficiencyCorrelation
with calls to this helper (reference requestCostCorrelation,
cacheEfficiencyCorrelation, and the component CorrelationAnalysis or its
rendering of footer).
- Around line 170-177: The XAxis prop uses an unnecessary conditional spread to
set tickFormatter; simplify by assigning tickFormatter directly (replace the
{...(xTickFormatter ? { tickFormatter: xTickFormatter } : {})} pattern with a
direct tickFormatter prop) so XAxis receives tickFormatter={xTickFormatter} and
React/Recharts can handle undefined values; locate the XAxis component in
CorrelationAnalysis.tsx (symbols: XAxis, xTickFormatter, xAxisName) and update
accordingly.
In `@src/components/charts/CostByModelOverTime.tsx`:
- Around line 69-84: The animation delay currently computes animationBegin using
models.indexOf(model) inside the models.map, which is inefficient and brittle;
update both model iteration sites (the map that renders <Line
key={`${model}_ma7`} ... /> and the other models.map around lines 136-150) to
use the map callback index parameter (e.g., (model, idx) => ...) and replace
models.indexOf(model) with that idx when calculating animationBegin (still using
CHART_ANIMATION.stagger * (idx % 5)); keep all other props (dataKey, key,
stroke, etc.) unchanged.
In `@src/components/charts/ModelMix.tsx`:
- Around line 126-143: The animation delay currently recomputes the model index
with models.indexOf(model) inside the models.map loop; update the mapping to use
the map callback index (e.g., change models.map((model) => { ... }) to
models.map((model, idx) => { ... }) and replace models.indexOf(model) with idx
when computing animationBegin) so the Area rendering (animationBegin property)
uses the direct index and avoids repeated lookups; keep the rest of the Area
props (type, dataKey, stackId, stroke, fill, name, isAnimationActive,
animationDuration, animationEasing) unchanged.
In `@src/components/features/anomaly/AnomalyDetection.tsx`:
- Around line 65-67: The render currently calls anomalies.sort(...), which
mutates the memoized anomalies array; instead create a non-mutating copy before
sorting (e.g., using slice() or spread) and call sort on that copy so the
original anomalies value from the hook/memo isn't mutated; update the expression
in AnomalyDetection where anomalies.sort((a,b) => b.totalCost -
a.totalCost).map(...) is used to use a copied array (e.g., [...anomalies] or
anomalies.slice()) before .sort and then .map.
In `@src/components/ui/card.tsx`:
- Around line 5-20: The Card component currently spreads {...props} after the
motion.div animation props which allows consumers to override built-in animation
settings; move the spread of consumer props so they are applied before the
animation props (i.e., spread {...props} earlier on the motion.div and then set
initial, whileInView, viewport, transition, className, ref explicitly) to ensure
Card's initial/whileInView/viewport/transition cannot be overridden by callers
while still forwarding ref and merging className.
In `@src/index.css`:
- Line 3: The stylelint warning about the `@theme` at-rule is a false positive for
Tailwind CSS v4; update the stylelint configuration (e.g., the rule that
validates unknown at-rules) to recognize Tailwind v4 directives so `@theme` is
allowed. Modify the stylelint config (the rule like "at-rules/no-unknown" or add
the tailwind plugin/ignore at-rules list) to include "theme" or enable the
official Tailwind parser/plugin so the `@theme` at-rule used in src/index.css is
not flagged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 89a45369-2956-410e-91b2-94bf6449329d
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (111)
.editorconfig.github/workflows/ci.yml.github/workflows/release.yml.prettierignore.prettierrc.jsonAGENTS.mdCHANGELOG.mdCONTRIBUTING.mdREADME.mdRELEASING.mdeslint.config.mjsexamples/sample-usage.jsonindex.htmlpackage.jsonscripts/report-smoke.jsscripts/start-test-server.jsscripts/verify-main-ci.jsscripts/verify-package.jsscripts/verify-registry-install.jsserver.jsserver/model-normalization.jsonserver/report/charts.jsserver/report/index.jsserver/report/utils.jssrc/components/Dashboard.tsxsrc/components/EmptyState.tsxsrc/components/cards/MetricCard.tsxsrc/components/cards/MonthMetrics.tsxsrc/components/cards/PrimaryMetrics.tsxsrc/components/cards/SecondaryMetrics.tsxsrc/components/cards/TodayMetrics.tsxsrc/components/charts/ChartCard.tsxsrc/components/charts/CorrelationAnalysis.tsxsrc/components/charts/CostByModel.tsxsrc/components/charts/CostByModelOverTime.tsxsrc/components/charts/CostByWeekday.tsxsrc/components/charts/CostOverTime.tsxsrc/components/charts/CumulativeCost.tsxsrc/components/charts/CustomTooltip.tsxsrc/components/charts/DistributionAnalysis.tsxsrc/components/charts/ModelMix.tsxsrc/components/charts/RequestCacheHitRateByModel.tsxsrc/components/charts/RequestsOverTime.tsxsrc/components/charts/TokenEfficiency.tsxsrc/components/charts/TokenTypes.tsxsrc/components/charts/TokensOverTime.tsxsrc/components/features/animations/FadeIn.tsxsrc/components/features/anomaly/AnomalyDetection.tsxsrc/components/features/auto-import/AutoImportModal.tsxsrc/components/features/cache-roi/CacheROI.tsxsrc/components/features/command-palette/CommandPalette.tsxsrc/components/features/comparison/PeriodComparison.tsxsrc/components/features/drill-down/DrillDownModal.tsxsrc/components/features/forecast/CostForecast.tsxsrc/components/features/heatmap/HeatmapCalendar.tsxsrc/components/features/help/HelpPanel.tsxsrc/components/features/help/InfoButton.tsxsrc/components/features/insights/UsageInsights.tsxsrc/components/features/limits/ProviderLimitsSection.tsxsrc/components/features/pdf-report/PDFReport.tsxsrc/components/features/request-quality/RequestQuality.tsxsrc/components/features/risk/ConcentrationRisk.tsxsrc/components/features/settings/SettingsModal.tsxsrc/components/layout/FilterBar.tsxsrc/components/layout/Header.tsxsrc/components/tables/ModelEfficiency.tsxsrc/components/tables/ProviderEfficiency.tsxsrc/components/tables/RecentDays.tsxsrc/components/ui/badge.tsxsrc/components/ui/button.tsxsrc/components/ui/card.tsxsrc/components/ui/dialog.tsxsrc/components/ui/expandable-card.tsxsrc/components/ui/formatted-value.tsxsrc/components/ui/select.tsxsrc/components/ui/skeleton.tsxsrc/components/ui/toast.tsxsrc/components/ui/tooltip.tsxsrc/hooks/use-app-settings.tssrc/hooks/use-computed-metrics.tssrc/hooks/use-dashboard-filters.tssrc/hooks/use-provider-limits.tssrc/hooks/use-theme.tssrc/hooks/use-usage-data.tssrc/index.csssrc/lib/api.tssrc/lib/app-settings.tssrc/lib/auto-import.tssrc/lib/calculations.tssrc/lib/constants.tssrc/lib/csv-export.tssrc/lib/dashboard-preferences.tssrc/lib/data-transforms.tssrc/lib/formatters.tssrc/lib/help-content.tssrc/lib/i18n.tssrc/lib/model-utils.tssrc/lib/provider-limits.tssrc/types/index.tstests/e2e/dashboard.spec.tstests/frontend/provider-limits-section.test.tsxtests/frontend/use-dashboard-filters.test.tsxtests/integration/server.test.tstests/unit/analytics.test.tstests/unit/model-normalization.test.tstests/unit/report-charts.test.tstests/unit/report-utils.test.tstests/unit/server-helpers.test.tstsconfig.jsonusage-normalizer.jsvitest.config.ts
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/lib/data-transforms.ts (1)
292-297:⚠️ Potential issue | 🟠 MajorWeekday labels can be shifted by timezone, causing mislabeled chart buckets.
On Line 293–294, you create UTC dates but format them in the user's local timezone. In many US timezones this can shift labels by one day (e.g., Monday bucket labeled as Sunday), while bucket indexing uses Monday=0 logic.
💡 Suggested fix
- const weekdayLabels = Array.from({ length: 7 }, (_, index) => - new Intl.DateTimeFormat(getCurrentLocale(), { weekday: 'short' }) - .format(new Date(Date.UTC(2024, 0, 1 + index))) - .replace('.', '') - .slice(0, 2), - ) + const weekdayFormatter = new Intl.DateTimeFormat(getCurrentLocale(), { + weekday: 'short', + timeZone: 'UTC', + }) + const weekdayLabels = Array.from({ length: 7 }, (_, index) => + weekdayFormatter.format(new Date(Date.UTC(2024, 0, 1 + index))).replace('.', ''), + )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/data-transforms.ts` around lines 292 - 297, The weekdayLabels generation (variable weekdayLabels) formats UTC-built dates with Intl.DateTimeFormat using the user's local timezone, which can shift the day and mislabel chart buckets; fix it by forcing UTC in the formatter options (add timeZone: 'UTC' to the options passed to Intl.DateTimeFormat in the weekdayLabels code that calls getCurrentLocale()), so the formatter interprets the Date.UTC values in UTC and labels align with the Monday=0 bucket logic.
🧹 Nitpick comments (3)
src/components/charts/CostOverTime.tsx (1)
31-33: Avoid sorting to compute the peak point.
Line 32does an O(n log n) sort + array clone just to get max. A single-pass max is cheaper and clearer for larger datasets.♻️ Suggested refactor
- const peak = [...data].sort((a, b) => b.cost - a.cost)[0] + let peak = data[0] + for (let i = 1; i < data.length; i += 1) { + if (data[i].cost > peak.cost) peak = data[i] + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/charts/CostOverTime.tsx` around lines 31 - 33, The code computes peak by cloning and sorting data (const peak = [...data].sort((a, b) => b.cost - a.cost)[0]) which is O(n log n); replace that with a single-pass max scan (e.g., use Array.prototype.reduce or a simple loop) to find the item with the largest cost without cloning data, keep the existing latest = data[data.length - 1] logic and the same peak variable name so subsequent code is unchanged, and ensure the function still returns null when either latest or peak is falsy.src/lib/data-transforms.ts (1)
13-408: Please run the full verification gates for this transform-heavy change before merge.Given this file drives filtering/aggregation/chart data paths, run verify, unit coverage, e2e, and manual flow checks (dashboard load, auto-import, JSON upload, filtering, export).
Based on learnings: "Run
npm run verifybefore opening a PR to validate code quality", "Runnpm run test:e2ebefore opening a PR for end-to-end testing", "Runnpm run test:unit:coverageto match the gate used by the release workflow", and "Manually verify the main flows affected by the change: dashboard load, auto-import, JSON upload, filtering, and export actions".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/data-transforms.ts` around lines 13 - 408, This change touches many transforms (recalculateDayFromBreakdowns, filterByModels, filterByProviders, aggregateToDailyFormat, toModelCostChartData, toTokenChartData, toRequestChartData, toWeekdayData and related helpers), so before merging run the full verification gates and manual flows: run npm run verify, npm run test:unit:coverage and fix any failing unit tests/coverage gaps, run npm run test:e2e and resolve failing e2e tests, then manually exercise the dashboard load, auto-import, JSON upload, filtering (by date/model/provider/month), export and aggregation paths to confirm outputs (charts, weekday aggregation, monthly/yearly aggregation) are correct; iterate on the affected functions above until all gates and manual checks pass.src/lib/help-content.ts (1)
1-1: Consider renaming this utility file to match kebab-free naming guidance.
src/lib/help-content.tsconflicts with the utility filename convention;src/lib/helpContent.ts(orsrc/lib/HelpContent.ts) would align better.As per coding guidelines "
src/lib/**/*.ts: Use PascalCase or descriptive kebab-free names for utility filenames (e.g.,formatters.ts)".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/charts/CostByModelOverTime.tsx`:
- Around line 31-33: The aggregation for the `total` in the `data.reduce`
(summing `point[model]`) currently checks `typeof point[model] === 'number'`
which allows NaN/Infinity; update the reducer to use the existing `coerceNumber`
helper (or call it on `point[model]`) and only add the returned finite numeric
value (e.g., treat non-finite as 0) so `total` is never polluted by
NaN/Infinity; change the `total` computation in `CostByModelOverTime` to reuse
`coerceNumber(point[model])` when summing.
In `@src/components/features/drill-down/DrillDownModal.tsx`:
- Around line 79-100: Compute a single tokensTotal value from
day.cacheReadTokens + day.cacheCreationTokens + day.inputTokens +
day.outputTokens + day.thinkingTokens and use that single tokensTotal everywhere
instead of mixing day.totalTokens and ad-hoc sums: replace the hasTokens,
cacheRate denominator, costPerMillion calculation and any other places (e.g.,
avgTokensPerRequest / avgCostPerRequest usage points) to reference tokensTotal
(and base hasTokens on tokensTotal > 0) so all percentage/cost derivations use
the same canonical token total.
In `@src/lib/data-transforms.ts`:
- Around line 108-118: getDateRange currently returns null if data[0] is falsy,
which drops valid later entries; change the logic in getDateRange to scan for
the first non-null DailyUsage entry before deciding to return null and
initialize start/end from that found entry (or return null only if no non-null
entries exist), e.g., use a loop to find first valid entry and then continue
iterating to compute min/max dates using the existing loop logic; update
references in getDateRange to use that firstValidEntry instead of assuming
data[0].
In `@src/lib/help-content.ts`:
- Around line 247-248: The Proxy traps for get and has in the selector wrapper
are exposing prototype properties (e.g., toString) because they call
Reflect.get(selector(), key) and use key in selector(), which walk the prototype
chain; update both traps in the Proxy returned by selector() to first check
ownership using Object.prototype.hasOwnProperty.call(selector(), key) (or
selector().hasOwnProperty guard) and only then return Reflect.get(selector(),
key) for get and true for has, otherwise return undefined (or Reflect.get
default) / false so inherited properties are not treated as map keys.
---
Outside diff comments:
In `@src/lib/data-transforms.ts`:
- Around line 292-297: The weekdayLabels generation (variable weekdayLabels)
formats UTC-built dates with Intl.DateTimeFormat using the user's local
timezone, which can shift the day and mislabel chart buckets; fix it by forcing
UTC in the formatter options (add timeZone: 'UTC' to the options passed to
Intl.DateTimeFormat in the weekdayLabels code that calls getCurrentLocale()), so
the formatter interprets the Date.UTC values in UTC and labels align with the
Monday=0 bucket logic.
---
Nitpick comments:
In `@src/components/charts/CostOverTime.tsx`:
- Around line 31-33: The code computes peak by cloning and sorting data (const
peak = [...data].sort((a, b) => b.cost - a.cost)[0]) which is O(n log n);
replace that with a single-pass max scan (e.g., use Array.prototype.reduce or a
simple loop) to find the item with the largest cost without cloning data, keep
the existing latest = data[data.length - 1] logic and the same peak variable
name so subsequent code is unchanged, and ensure the function still returns null
when either latest or peak is falsy.
In `@src/lib/data-transforms.ts`:
- Around line 13-408: This change touches many transforms
(recalculateDayFromBreakdowns, filterByModels, filterByProviders,
aggregateToDailyFormat, toModelCostChartData, toTokenChartData,
toRequestChartData, toWeekdayData and related helpers), so before merging run
the full verification gates and manual flows: run npm run verify, npm run
test:unit:coverage and fix any failing unit tests/coverage gaps, run npm run
test:e2e and resolve failing e2e tests, then manually exercise the dashboard
load, auto-import, JSON upload, filtering (by date/model/provider/month), export
and aggregation paths to confirm outputs (charts, weekday aggregation,
monthly/yearly aggregation) are correct; iterate on the affected functions above
until all gates and manual checks pass.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9763b0d3-7641-49af-9ceb-8aba1111f9b9
📒 Files selected for processing (41)
.github/workflows/release.ymlRELEASING.mdpackage.jsonserver/model-normalization.jsonsrc/components/cards/MonthMetrics.tsxsrc/components/cards/SecondaryMetrics.tsxsrc/components/cards/TodayMetrics.tsxsrc/components/charts/ChartCard.tsxsrc/components/charts/CorrelationAnalysis.tsxsrc/components/charts/CostByModelOverTime.tsxsrc/components/charts/CostByWeekday.tsxsrc/components/charts/CostOverTime.tsxsrc/components/charts/CumulativeCost.tsxsrc/components/charts/ModelMix.tsxsrc/components/charts/TokenEfficiency.tsxsrc/components/features/anomaly/AnomalyDetection.tsxsrc/components/features/drill-down/DrillDownModal.tsxsrc/components/features/forecast/CostForecast.tsxsrc/components/features/limits/ProviderLimitsSection.tsxsrc/components/features/settings/SettingsModal.tsxsrc/components/layout/FilterBar.tsxsrc/components/tables/ModelEfficiency.tsxsrc/components/tables/ProviderEfficiency.tsxsrc/components/tables/RecentDays.tsxsrc/components/ui/card.tsxsrc/lib/auto-import.tssrc/lib/data-transforms.tssrc/lib/formatters.tssrc/lib/help-content.tssrc/locales/de/common.jsonsrc/locales/en/common.jsontests/frontend/chart-card.test.tsxtests/frontend/filter-bar.test.tsxtests/frontend/phase4-correctness.test.tsxtests/frontend/provider-limits-section.test.tsxtests/frontend/sortable-tables.test.tsxtests/unit/analytics.test.tstests/unit/code-rabbit-phase1.test.tstests/unit/code-rabbit-phase4.test.tstests/unit/help-content.test.tstests/unit/model-normalization.test.ts
✅ Files skipped from review due to trivial changes (2)
- src/components/features/anomaly/AnomalyDetection.tsx
- server/model-normalization.json
🚧 Files skipped from review as they are similar to previous changes (20)
- .github/workflows/release.yml
- src/components/charts/CostByWeekday.tsx
- RELEASING.md
- src/components/features/forecast/CostForecast.tsx
- src/lib/formatters.ts
- src/components/charts/ModelMix.tsx
- src/components/cards/SecondaryMetrics.tsx
- src/components/charts/ChartCard.tsx
- src/components/charts/TokenEfficiency.tsx
- src/components/tables/ModelEfficiency.tsx
- src/components/cards/TodayMetrics.tsx
- src/components/ui/card.tsx
- package.json
- src/components/charts/CumulativeCost.tsx
- src/components/cards/MonthMetrics.tsx
- src/components/tables/RecentDays.tsx
- src/components/layout/FilterBar.tsx
- src/components/charts/CorrelationAnalysis.tsx
- src/components/tables/ProviderEfficiency.tsx
- src/components/features/limits/ProviderLimitsSection.tsx
Summary by CodeRabbit
New Features
Bug Fixes
Chores
Documentation