feat: add per-channel billing ratio and channel stats dashboard#4894
feat: add per-channel billing ratio and channel stats dashboard#4894JacksonsY wants to merge 3 commits into
Conversation
Add a configurable billing ratio on each channel that scales channel-side usage statistics only, without affecting user billing. Backend: - Add Channel.ChannelRatio field (>=0, default 1) with GetChannelRatio() - UpdateChannelUsedQuota applies the ratio when accumulating used_quota - Snapshot the ratio into Log.channel_ratio at consume time; hide it from non-admin users in formatUserLogs - New admin endpoint GET /api/data/channel returning the hourly channel-dimension cost time series Frontend (web/default + web/classic): - Channel edit form: billing ratio input - Usage log: per-row channel cost line for admins - New admin-only "Channel Stats" dashboard section with overview cards and channel cost ranking/trend charts - i18n for all locales
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
✅ Files skipped from review due to trivial changes (3)
🚧 Files skipped from review as they are similar to previous changes (2)
WalkthroughThis PR adds per-channel billing ratios, snapshots ratios on consume logs, records channel-adjusted quota for hourly aggregation, exposes an admin API to fetch channel quota time-series, and updates classic/default frontends with configuration, columns, charts, and translations. ChangesChannel Billing Ratio & Cost Reporting
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (10)
web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx (2)
705-716: 💤 Low valueConsider adding a tooltip or aria-label to explain the 'A' prefix.
The 'A' prefix on the channel cost badge (line 714) likely stands for "Admin" or "Adjusted," but this is not immediately clear to users. Consider adding a tooltip or aria-label for better UX.
💡 Example with tooltip
Wrap the badge in a tooltip:
const channelCostNode = showChannelCost ? ( <TooltipProvider> <Tooltip> <TooltipTrigger asChild> <span className='inline-flex w-fit items-center rounded-md border border-amber-200 bg-amber-50 px-1.5 py-0.5 font-mono text-[11px] leading-none font-medium text-amber-700 tabular-nums dark:border-amber-800 dark:bg-amber-950/40 dark:text-amber-300'> A {formatLogQuota(Math.round(quota * (channelRatio as number)))} </span> </TooltipTrigger> <TooltipContent>{t('Channel Cost')}</TooltipContent> </Tooltip> </TooltipProvider> ) : null🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx` around lines 705 - 716, The channel cost badge (channelCostNode) shows an unexplained 'A' prefix when showChannelCost is true (uses channelRatio and formatLogQuota); add accessible explanatory text by wrapping the badge in a tooltip or adding an aria-label/title that explains the 'A' (e.g., "Admin/Adjusted channel cost") so screen readers and hover users understand the prefix; ensure the tooltip/aria-label text is localized and attach it to the same span that renders A {formatLogQuota(Math.round(quota * (channelRatio as number)))} so the hint appears for both visual and assistive users.
373-378: ⚡ Quick winFormat channel ratio consistently with other ratios.
Other ratios in this file (group ratio, user group ratio) are formatted using
formatRatioCompact()with an 'x' suffix (see lines 86, 209, 240). For consistency, consider formatting the channel ratio similarly.♻️ Proposed formatting for consistency
{typeof log.channel_ratio === 'number' && log.channel_ratio !== 1 && ( <p className='text-muted-foreground text-xs'> - {t('Channel Billing Ratio')}: {log.channel_ratio} + {t('Channel Billing Ratio')}: {formatRatioCompact(log.channel_ratio)}x </p> )}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx` around lines 373 - 378, Replace the raw channel_ratio output with the same compact formatter used for other ratios: call formatRatioCompact on log.channel_ratio (and append or pass the 'x' suffix the same way group and user group ratios do) inside the JSX conditional that checks typeof log.channel_ratio and !== 1, so the Channel Billing Ratio rendering uses formatRatioCompact(log.channel_ratio, ...) consistent with the other ratio displays.web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx (1)
317-331: ⚡ Quick winConsider using formatRatioCompact for ratio display consistency.
For consistency with other ratio formatting throughout the codebase (e.g., group ratios in the columns file use
formatRatioCompact()), consider formatting the channel ratio here as well.♻️ Proposed formatting for consistency
Add the import at the top if not already present:
import { formatRatioCompact } from '../lib/format' // or wherever it's exportedThen update the ratio display:
rows.push({ label: t('Channel Billing Ratio'), - value: `${log.channel_ratio}x`, + value: `${formatRatioCompact(log.channel_ratio)}x`, })Note:
formatRatioCompactis defined incommon-logs-columns.tsxat lines 67-72. You may need to move it to a shared utility file or re-implement it here.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx` around lines 317 - 331, The channel ratio is currently shown as `${log.channel_ratio}x`; change this to use the shared formatter by importing formatRatioCompact and calling it where the ratio row is added (update the rows.push call that uses log.channel_ratio), e.g., replace the raw string construction in the rows.push for label t('Channel Billing Ratio') with formatRatioCompact(log.channel_ratio) and add the appropriate import for formatRatioCompact (re-export or move it to a shared util if needed) so the component uses the same ratio formatting as other components.web/default/src/i18n/locales/en.json (1)
4355-4366: ⚡ Quick winScope these new i18n keys to avoid collisions with generic labels.
"Cost Trend"is a shared/generic key, but its value was changed to channel-specific wording. Please add feature-scoped keys (e.g.,dashboard.channels.*) and keep generic labels generic.💡 Proposed i18n key refactor
- "Channel Billing Ratio": "Channel Billing Ratio", - "Channel Cost": "Channel Cost", - "Channel Stats": "Channel Stats", - "Total Channel Cost": "Total Channel Cost", - "Total Raw Cost": "Total Raw Cost", - "Overall Ratio": "Overall Ratio", - "Saved": "Saved", - "Channel Cost Ranking": "Channel Cost Ranking", - "Cost Trend": "Channel Cost Trend", - "Top Channels": "Top Channels", - "View channel-dimension cost statistics": "View channel-dimension cost statistics", - "Channel billing ratio. Only scales channel-side usage stats (used quota / dashboard), does not affect user billing. 0 means this channel is not counted.": "Channel billing ratio. Only scales channel-side usage stats (used quota / dashboard), does not affect user billing. 0 means this channel is not counted.", + "dashboard.channels.channelBillingRatio": "Channel Billing Ratio", + "dashboard.channels.channelCost": "Channel Cost", + "dashboard.channels.title": "Channel Stats", + "dashboard.channels.totalChannelCost": "Total Channel Cost", + "dashboard.channels.totalRawCost": "Total Raw Cost", + "dashboard.channels.overallRatio": "Overall Ratio", + "dashboard.channels.savings": "Saved", + "dashboard.channels.costRanking": "Channel Cost Ranking", + "dashboard.channels.costTrend": "Channel Cost Trend", + "dashboard.channels.topChannels": "Top Channels", + "dashboard.channels.description": "View channel-dimension cost statistics", + "channels.form.channelBillingRatio.help": "Channel billing ratio. Only scales channel-side usage stats (used quota / dashboard), does not affect user billing. 0 means this channel is not counted.",As per coding guidelines,
web/default/src/i18n/**/*.{ts,tsx,json}should “Use hierarchical and semantically clear translation key names such asdashboard.overview.titleand maintain naming consistency”.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/default/src/i18n/locales/en.json` around lines 4355 - 4366, The new channel-specific i18n entries should be moved from generic keys into a feature-scoped namespace to avoid collisions: rename keys like "Cost Trend", "Top Channels", "Channel Cost", "Channel Billing Ratio", "Channel Stats", "Total Channel Cost", "Total Raw Cost", "Overall Ratio", "Saved", "Channel Cost Ranking", and the long description into hierarchical keys such as dashboard.channels.costTrend, dashboard.channels.topChannels, dashboard.channels.channelCost, dashboard.channels.billingRatio, dashboard.channels.stats, dashboard.channels.totalChannelCost, dashboard.channels.totalRawCost, dashboard.channels.overallRatio, dashboard.channels.saved, dashboard.channels.costRanking, and dashboard.channels.billingRatioDescription (or similar), and keep any existing generic "Cost Trend" key intact; update any code references that read the old flat keys (search for usages of "Cost Trend", "Top Channels", etc.) to the new dashboard.channels.* keys so translations resolve correctly.web/default/src/i18n/locales/fr.json (1)
4355-4366: 🏗️ Heavy liftUse hierarchical i18n keys for the new channel-stats strings.
These new entries use sentence-style keys instead of hierarchical keys (e.g.,
dashboard.channels.totalCost), which breaks the i18n naming rule for this path and will keep key naming inconsistent.As per coding guidelines, "Use hierarchical and semantically clear translation key names such as
dashboard.overview.titleand maintain naming consistency".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/default/src/i18n/locales/fr.json` around lines 4355 - 4366, The new flat sentence-style translation keys ("Channel Billing Ratio", "Channel Cost", "Channel Stats", "Total Channel Cost", "Total Raw Cost", "Overall Ratio", "Saved", "Channel Cost Ranking", "Cost Trend", "Top Channels", "View channel-dimension cost statistics", "Channel billing ratio. Only scales channel-side usage stats (used quota / dashboard), does not affect user billing. 0 means this channel is not counted.") must be converted to hierarchical keys to match the i18n convention (e.g., dashboard.channels.billingRatio, dashboard.channels.cost, dashboard.channels.stats, dashboard.channels.totalCost, dashboard.channels.totalRawCost, dashboard.channels.overallRatio, dashboard.channels.saved, dashboard.channels.costRanking, dashboard.channels.costTrend, dashboard.channels.topChannels, dashboard.channels.viewStats, dashboard.channels.billingRatioDescription); update fr.json to replace the flat keys with these hierarchical keys (preserving the corresponding French translations), and ensure any code that references the old flat keys (components or templates using these strings) is updated to use the new hierarchical keys consistently.web/default/src/i18n/locales/ru.json (1)
4355-4366: ⚡ Quick winUse hierarchical, context-specific i18n keys for these new entries.
These additions use flat phrase keys (and
"Saved"is especially ambiguous). Please switch to scoped keys (e.g.,dashboard.channels.totalSaved,dashboard.channels.costTrend) to keep semantics stable and avoid collisions.As per coding guidelines, use “hierarchical and semantically clear translation key names such as
dashboard.overview.titleand maintain naming consistency”.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/default/src/i18n/locales/ru.json` around lines 4355 - 4366, Replace the flat translation keys with hierarchical, context-specific keys under a common scope (e.g., dashboard.channels) to avoid collisions and clarify semantics: rename "Channel Billing Ratio" -> "dashboard.channels.billingRatio", "Channel Cost" -> "dashboard.channels.cost", "Channel Stats" -> "dashboard.channels.stats", "Total Channel Cost" -> "dashboard.channels.totalCost", "Total Raw Cost" -> "dashboard.channels.totalRawCost", "Overall Ratio" -> "dashboard.channels.overallRatio", "Saved" -> "dashboard.channels.totalSaved" (avoid ambiguous single-word keys), "Channel Cost Ranking" -> "dashboard.channels.costRanking", "Cost Trend" -> "dashboard.channels.costTrend", "Top Channels" -> "dashboard.channels.top", "View channel-dimension cost statistics" -> "dashboard.channels.viewChannelCostStats", and "Channel billing ratio. Only scales channel-side usage stats..." -> "dashboard.channels.billingRatioDescription"; update any code references to the original flat keys to use these new hierarchical keys and ensure the Russian values remain unchanged.web/default/src/i18n/locales/zh.json (1)
4355-4366: ⚡ Quick winUse hierarchical i18n keys for the newly added channel-stats labels.
These new keys should follow semantic hierarchical naming (for example under
dashboard.channels.*) instead of phrase-as-key literals to stay consistent with the i18n guideline and keep key management maintainable.As per coding guidelines, "Use hierarchical and semantically clear translation key names such as
dashboard.overview.titleand maintain naming consistency".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/default/src/i18n/locales/zh.json` around lines 4355 - 4366, The new i18n entries use phrase-as-key literals (e.g., "Channel Billing Ratio", "Channel Cost", "Channel Stats", "Total Channel Cost", "Total Raw Cost", "Overall Ratio", "Saved", "Channel Cost Ranking", "Cost Trend", "Top Channels", "View channel-dimension cost statistics", "Channel billing ratio. Only scales channel-side usage stats (...)")—replace these with hierarchical, semantically clear keys under a common namespace such as dashboard.channels (for example dashboard.channels.billingRatio, dashboard.channels.cost, dashboard.channels.stats, dashboard.channels.totalCost, dashboard.channels.totalRawCost, dashboard.channels.overallRatio, dashboard.channels.saved, dashboard.channels.costRanking, dashboard.channels.costTrend, dashboard.channels.topChannels, dashboard.channels.viewStats, dashboard.channels.billingRatioDescription) and move the existing Chinese translations as the values; update any components that reference the old literal keys to use the new dashboard.channels.* keys.controller/usedata.go (1)
30-43: 💤 Low valueConsider adding time range validation for performance protection.
While the endpoint correctly follows the existing admin pattern (no time limits), unbounded date ranges could cause slow queries or timeouts on large log databases. Adding an optional warning or soft limit (e.g., 3-6 months) could improve admin UX without breaking legitimate use cases.
Note: The ignored
strconv.ParseInterrors are acceptable here per established codebase patterns for query parameter parsing.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@controller/usedata.go` around lines 30 - 43, The GetChannelQuotaDates handler currently passes startTimestamp/endTimestamp directly to model.GetChannelQuotaData allowing unbounded ranges; add a time-range validation/cap: compute the duration between startTimestamp and endTimestamp (or from a default when one side missing), and if it exceeds a configurable soft limit (e.g., 90 days) either return a 400 with a clear message or automatically cap the range to the max and include a "warning" field in the JSON response; update GetChannelQuotaDates to enforce this check before calling model.GetChannelQuotaData and document the chosen behaviour (soft cap vs error) in the handler.model/log.go (1)
479-509: ⚡ Quick winConsider adding ORDER BY for deterministic result ordering.
The query lacks an
ORDER BYclause, so results may be returned in arbitrary database-dependent order. While the frontend can sort client-side, ordering in the database is more efficient and ensures consistent results across calls.📊 Suggested ORDER BY clause
if endTime != 0 { tx = tx.Where("created_at <= ?", endTime) } - if err := tx.Group("channel_id, (created_at - (created_at % 3600))").Find(&points).Error; err != nil { + if err := tx.Group("channel_id, (created_at - (created_at % 3600))").Order("created_at ASC, channel_id ASC").Find(&points).Error; err != nil { return nil, err }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@model/log.go` around lines 479 - 509, The query in GetChannelQuotaData lacks a deterministic ORDER BY; update the query builder (tx) before the Find call to add an explicit ordering, e.g. Order("channel_id ASC, (created_at - (created_at % 3600)) ASC") (or use the alias/column names you prefer) so the returned points slice is consistently ordered by channel and hour; make this change near the tx.Group(...).Find(&points) call in the GetChannelQuotaData function.model/channel.go (1)
821-833: 💤 Low valueConsider the implications of rounding at the individual quota level.
The use of
math.Roundon each quota update means rounding happens per-transaction rather than on aggregated totals. While this provides consistency, cumulative rounding errors could cause the sum of adjusted quotas to diverge slightly fromSUM(original_quota) * ratio. Since this affects only channel-dimension statistics (not user billing), the impact is low, but be aware that high-volume channels may see minor discrepancies in analytics.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@model/channel.go` around lines 821 - 833, applyChannelRatio currently rounds each adjusted quota per transaction using math.Round, causing cumulative rounding error; change applyChannelRatio to return a float64 (remove math.Round) so it returns float64(quota) * ratio and adjust callers that consume applyChannelRatio to aggregate the float64 values and perform rounding only when persisting/reporting channel-level used_quota; keep references to applyChannelRatio, CacheGetChannel and channel.GetChannelRatio() so you update call sites accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@web/classic/src/components/table/channels/modals/EditChannelModal.jsx`:
- Line 1035: The advanced-settings auto-expand condition treats null as
non-default; normalize channel_ratio before comparing by replacing checks like
(data.channel_ratio !== undefined && data.channel_ratio !== 1) with a normalized
comparison using a default of 1 (e.g. use ( (data.channel_ratio ?? 1) !== 1 ) or
assign const ratio = data.channel_ratio ?? 1 and compare ratio !== 1) so null
(and undefined) are treated as the default value when deciding to auto-expand;
update the condition in the auto-expand logic referencing data.channel_ratio
accordingly.
In `@web/classic/src/components/table/usage-logs/UsageLogsColumnDefs.jsx`:
- Around line 833-843: The gating for channel-cost display uses parseInt(text)
which truncates fractional values; update the showChannelCost condition (where
channelRatio, showChannelCost, and channelCostNode are defined and renderQuota
is used) to parse the value numerically using Number (or parseFloat) and guard
with Number.isFinite, e.g. replace parseInt(text) > 0 with a check like
Number.isFinite(Number(text)) && Number(text) > 0 so fractional positive costs
(e.g., "0.5") are correctly considered.
In `@web/classic/src/i18n/locales/fr.json`:
- Around line 2312-2313: The two FR translations for the keys "渠道倍率" and
"渠道计费倍率" use "Coefficient" but must match the project's existing terminology by
using "Ratio"; update the values for "渠道倍率" and "渠道计费倍率" to use "Ratio" (e.g.,
"Ratio du canal" and "Ratio de facturation du canal" or equivalent consistent
phrasing used elsewhere) so they align with other French locale entries.
In
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`:
- Around line 2549-2552: The current input for channel_ratio uses
value={field.value ?? 1} and onChange calling
field.onChange(Number(e.target.value)), which coerces an empty string to 0;
change it to allow an empty string to be stored and only convert to number when
the input is non-empty: set the input value to field.value ?? '' and in the
onChange call pass e.target.value as '' when empty or Number(e.target.value)
otherwise, invoking field.onChange accordingly so clearing the field does not
set channel_ratio to 0 (targets: the value prop and the onChange that calls
field.onChange for the channel_ratio field).
In `@web/default/src/features/channels/lib/channel-form.ts`:
- Line 38: The validation message for channel_ratio is hardcoded; import the
i18next translator in this non-React module (import { t } from 'i18next') and
replace the literal message in the z.number().min(...) for channel_ratio with a
call to t('...') using an appropriate translation key (e.g.
'validation.channel_ratio_min') so the schema uses i18n; ensure the import is
added at the top of the module and the zod rule references
t('validation.channel_ratio_min') instead of the hardcoded string.
In `@web/default/src/features/dashboard/components/channels/channel-charts.tsx`:
- Around line 122-127: The query currently swallows failures by mapping failed
responses to null in the select, causing the UI to display 0s as if data were
valid; update the useQuery call (queryKey: ['dashboard','channel-quota',
timeRange], queryFn: getChannelQuotaDates) to surface errors (stop converting
failures to null in select), include isError and error from useQuery, and render
an explicit error state in the component when isError is true (e.g., show an
error message or fallback UI instead of totals); ensure getChannelQuotaDates
rejects/throws on HTTP errors so useQuery can mark isError and provide the real
error.
In `@web/default/src/i18n/locales/fr.json`:
- Line 4361: The "Saved" locale entry currently uses the past-participle value
"Économisé"; update the JSON key "Saved" in the French locale so the label is
the noun form "Économies" instead of "Économisé" (i.e., replace the value for
"Saved" with "Économies" in fr.json).
In `@web/default/src/i18n/locales/ja.json`:
- Line 4361: The Japanese translation for the metric label key "Saved" is
ambiguous; update the value for the "Saved" entry in ja.json from "節約" to the
clearer metric label "節約額" (i.e., change the string value for the "Saved" key to
"節約額").
- Around line 4355-4366: The new flat i18n entries ("Channel Billing Ratio",
"Channel Cost", "Channel Stats", "Total Channel Cost", "Total Raw Cost",
"Overall Ratio", "Saved", "Channel Cost Ranking", "Cost Trend", "Top Channels",
"View channel-dimension cost statistics", "Channel billing ratio. Only scales
channel-side usage stats...") must be migrated to hierarchical, feature-scoped
keys (e.g., dashboard.channelStats.totalCost,
dashboard.channelStats.topChannels, usage.channel.billingRatio,
usage.channel.billingRatio.description) and the code references updated to use
those new keys; rename each string to a semantic key under an appropriate
feature namespace (dashboard or usage/usageLogs), replace usages in UI
components and templates to the new keys (search for literal keys like "Channel
Cost" and "Channel Billing Ratio" in code), and keep the original Japanese
translations as values under the new keys to preserve content.
In `@web/default/src/i18n/locales/vi.json`:
- Line 4361: The translation for the i18n key "Saved" is currently the verb
"Tiết kiệm" which reads like an action; update the value for the "Saved" key to
a noun phrase appropriate for a dashboard metric such as "Khoản tiết kiệm" or
"Tiền tiết kiệm" so the label clearly represents a savings amount rather than an
action.
---
Nitpick comments:
In `@controller/usedata.go`:
- Around line 30-43: The GetChannelQuotaDates handler currently passes
startTimestamp/endTimestamp directly to model.GetChannelQuotaData allowing
unbounded ranges; add a time-range validation/cap: compute the duration between
startTimestamp and endTimestamp (or from a default when one side missing), and
if it exceeds a configurable soft limit (e.g., 90 days) either return a 400 with
a clear message or automatically cap the range to the max and include a
"warning" field in the JSON response; update GetChannelQuotaDates to enforce
this check before calling model.GetChannelQuotaData and document the chosen
behaviour (soft cap vs error) in the handler.
In `@model/channel.go`:
- Around line 821-833: applyChannelRatio currently rounds each adjusted quota
per transaction using math.Round, causing cumulative rounding error; change
applyChannelRatio to return a float64 (remove math.Round) so it returns
float64(quota) * ratio and adjust callers that consume applyChannelRatio to
aggregate the float64 values and perform rounding only when persisting/reporting
channel-level used_quota; keep references to applyChannelRatio, CacheGetChannel
and channel.GetChannelRatio() so you update call sites accordingly.
In `@model/log.go`:
- Around line 479-509: The query in GetChannelQuotaData lacks a deterministic
ORDER BY; update the query builder (tx) before the Find call to add an explicit
ordering, e.g. Order("channel_id ASC, (created_at - (created_at % 3600)) ASC")
(or use the alias/column names you prefer) so the returned points slice is
consistently ordered by channel and hour; make this change near the
tx.Group(...).Find(&points) call in the GetChannelQuotaData function.
In
`@web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx`:
- Around line 705-716: The channel cost badge (channelCostNode) shows an
unexplained 'A' prefix when showChannelCost is true (uses channelRatio and
formatLogQuota); add accessible explanatory text by wrapping the badge in a
tooltip or adding an aria-label/title that explains the 'A' (e.g.,
"Admin/Adjusted channel cost") so screen readers and hover users understand the
prefix; ensure the tooltip/aria-label text is localized and attach it to the
same span that renders A {formatLogQuota(Math.round(quota * (channelRatio as
number)))} so the hint appears for both visual and assistive users.
- Around line 373-378: Replace the raw channel_ratio output with the same
compact formatter used for other ratios: call formatRatioCompact on
log.channel_ratio (and append or pass the 'x' suffix the same way group and user
group ratios do) inside the JSX conditional that checks typeof log.channel_ratio
and !== 1, so the Channel Billing Ratio rendering uses
formatRatioCompact(log.channel_ratio, ...) consistent with the other ratio
displays.
In `@web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx`:
- Around line 317-331: The channel ratio is currently shown as
`${log.channel_ratio}x`; change this to use the shared formatter by importing
formatRatioCompact and calling it where the ratio row is added (update the
rows.push call that uses log.channel_ratio), e.g., replace the raw string
construction in the rows.push for label t('Channel Billing Ratio') with
formatRatioCompact(log.channel_ratio) and add the appropriate import for
formatRatioCompact (re-export or move it to a shared util if needed) so the
component uses the same ratio formatting as other components.
In `@web/default/src/i18n/locales/en.json`:
- Around line 4355-4366: The new channel-specific i18n entries should be moved
from generic keys into a feature-scoped namespace to avoid collisions: rename
keys like "Cost Trend", "Top Channels", "Channel Cost", "Channel Billing Ratio",
"Channel Stats", "Total Channel Cost", "Total Raw Cost", "Overall Ratio",
"Saved", "Channel Cost Ranking", and the long description into hierarchical keys
such as dashboard.channels.costTrend, dashboard.channels.topChannels,
dashboard.channels.channelCost, dashboard.channels.billingRatio,
dashboard.channels.stats, dashboard.channels.totalChannelCost,
dashboard.channels.totalRawCost, dashboard.channels.overallRatio,
dashboard.channels.saved, dashboard.channels.costRanking, and
dashboard.channels.billingRatioDescription (or similar), and keep any existing
generic "Cost Trend" key intact; update any code references that read the old
flat keys (search for usages of "Cost Trend", "Top Channels", etc.) to the new
dashboard.channels.* keys so translations resolve correctly.
In `@web/default/src/i18n/locales/fr.json`:
- Around line 4355-4366: The new flat sentence-style translation keys ("Channel
Billing Ratio", "Channel Cost", "Channel Stats", "Total Channel Cost", "Total
Raw Cost", "Overall Ratio", "Saved", "Channel Cost Ranking", "Cost Trend", "Top
Channels", "View channel-dimension cost statistics", "Channel billing ratio.
Only scales channel-side usage stats (used quota / dashboard), does not affect
user billing. 0 means this channel is not counted.") must be converted to
hierarchical keys to match the i18n convention (e.g.,
dashboard.channels.billingRatio, dashboard.channels.cost,
dashboard.channels.stats, dashboard.channels.totalCost,
dashboard.channels.totalRawCost, dashboard.channels.overallRatio,
dashboard.channels.saved, dashboard.channels.costRanking,
dashboard.channels.costTrend, dashboard.channels.topChannels,
dashboard.channels.viewStats, dashboard.channels.billingRatioDescription);
update fr.json to replace the flat keys with these hierarchical keys (preserving
the corresponding French translations), and ensure any code that references the
old flat keys (components or templates using these strings) is updated to use
the new hierarchical keys consistently.
In `@web/default/src/i18n/locales/ru.json`:
- Around line 4355-4366: Replace the flat translation keys with hierarchical,
context-specific keys under a common scope (e.g., dashboard.channels) to avoid
collisions and clarify semantics: rename "Channel Billing Ratio" ->
"dashboard.channels.billingRatio", "Channel Cost" -> "dashboard.channels.cost",
"Channel Stats" -> "dashboard.channels.stats", "Total Channel Cost" ->
"dashboard.channels.totalCost", "Total Raw Cost" ->
"dashboard.channels.totalRawCost", "Overall Ratio" ->
"dashboard.channels.overallRatio", "Saved" -> "dashboard.channels.totalSaved"
(avoid ambiguous single-word keys), "Channel Cost Ranking" ->
"dashboard.channels.costRanking", "Cost Trend" ->
"dashboard.channels.costTrend", "Top Channels" -> "dashboard.channels.top",
"View channel-dimension cost statistics" ->
"dashboard.channels.viewChannelCostStats", and "Channel billing ratio. Only
scales channel-side usage stats..." ->
"dashboard.channels.billingRatioDescription"; update any code references to the
original flat keys to use these new hierarchical keys and ensure the Russian
values remain unchanged.
In `@web/default/src/i18n/locales/zh.json`:
- Around line 4355-4366: The new i18n entries use phrase-as-key literals (e.g.,
"Channel Billing Ratio", "Channel Cost", "Channel Stats", "Total Channel Cost",
"Total Raw Cost", "Overall Ratio", "Saved", "Channel Cost Ranking", "Cost
Trend", "Top Channels", "View channel-dimension cost statistics", "Channel
billing ratio. Only scales channel-side usage stats (...)")—replace these with
hierarchical, semantically clear keys under a common namespace such as
dashboard.channels (for example dashboard.channels.billingRatio,
dashboard.channels.cost, dashboard.channels.stats, dashboard.channels.totalCost,
dashboard.channels.totalRawCost, dashboard.channels.overallRatio,
dashboard.channels.saved, dashboard.channels.costRanking,
dashboard.channels.costTrend, dashboard.channels.topChannels,
dashboard.channels.viewStats, dashboard.channels.billingRatioDescription) and
move the existing Chinese translations as the values; update any components that
reference the old literal keys to use the new dashboard.channels.* keys.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f359f915-6ae2-4fc7-a557-0f35185f255f
📒 Files selected for processing (33)
controller/usedata.gomodel/channel.gomodel/log.gorouter/api-router.goweb/classic/src/components/table/channels/ChannelsColumnDefs.jsxweb/classic/src/components/table/channels/modals/EditChannelModal.jsxweb/classic/src/components/table/usage-logs/UsageLogsColumnDefs.jsxweb/classic/src/hooks/channels/useChannelsData.jsxweb/classic/src/i18n/locales/en.jsonweb/classic/src/i18n/locales/fr.jsonweb/classic/src/i18n/locales/ja.jsonweb/classic/src/i18n/locales/ru.jsonweb/classic/src/i18n/locales/vi.jsonweb/classic/src/i18n/locales/zh-TW.jsonweb/default/src/features/channels/components/channels-columns.tsxweb/default/src/features/channels/components/drawers/channel-mutate-drawer.tsxweb/default/src/features/channels/constants.tsweb/default/src/features/channels/lib/channel-form.tsweb/default/src/features/channels/types.tsweb/default/src/features/dashboard/api.tsweb/default/src/features/dashboard/components/channels/channel-charts.tsxweb/default/src/features/dashboard/index.tsxweb/default/src/features/dashboard/section-registry.tsxweb/default/src/features/dashboard/types.tsweb/default/src/features/usage-logs/components/columns/common-logs-columns.tsxweb/default/src/features/usage-logs/components/dialogs/details-dialog.tsxweb/default/src/features/usage-logs/data/schema.tsweb/default/src/i18n/locales/en.jsonweb/default/src/i18n/locales/fr.jsonweb/default/src/i18n/locales/ja.jsonweb/default/src/i18n/locales/ru.jsonweb/default/src/i18n/locales/vi.jsonweb/default/src/i18n/locales/zh.json
| model_mapping: z.string().optional(), | ||
| priority: z.number().optional(), | ||
| weight: z.number().optional(), | ||
| channel_ratio: z.number().min(0, 'Channel ratio must be >= 0').optional(), |
There was a problem hiding this comment.
Localize the new schema validation message.
The channel_ratio error text is hardcoded in a non-React .ts module, so it won’t follow i18n conventions used elsewhere.
💡 Suggested fix
import { z } from 'zod'
+import { t } from 'i18next'
@@
- channel_ratio: z.number().min(0, 'Channel ratio must be >= 0').optional(),
+ channel_ratio: z.number().min(0, t('Channel ratio must be >= 0')).optional(),As per coding guidelines, "web/default/**/*.ts: In non-React environments (utility functions, constants, class methods), use import { t } from 'i18next' for translations; note this will not auto-update on language changes."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| channel_ratio: z.number().min(0, 'Channel ratio must be >= 0').optional(), | |
| import { z } from 'zod' | |
| import { t } from 'i18next' | |
| channel_ratio: z.number().min(0, t('Channel ratio must be >= 0')).optional(), |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/default/src/features/channels/lib/channel-form.ts` at line 38, The
validation message for channel_ratio is hardcoded; import the i18next translator
in this non-React module (import { t } from 'i18next') and replace the literal
message in the z.number().min(...) for channel_ratio with a call to t('...')
using an appropriate translation key (e.g. 'validation.channel_ratio_min') so
the schema uses i18n; ensure the import is added at the top of the module and
the zod rule references t('validation.channel_ratio_min') instead of the
hardcoded string.
| "Channel Billing Ratio": "チャネル課金倍率", | ||
| "Channel Cost": "チャネルコスト", | ||
| "Channel Stats": "チャネル統計", | ||
| "Total Channel Cost": "チャネルコスト合計", | ||
| "Total Raw Cost": "元コスト合計", | ||
| "Overall Ratio": "総合倍率", | ||
| "Saved": "節約", | ||
| "Channel Cost Ranking": "チャネルコストランキング", | ||
| "Cost Trend": "チャネルコスト推移", | ||
| "Top Channels": "人気チャネル", | ||
| "View channel-dimension cost statistics": "チャネル単位のコスト統計を表示", | ||
| "Channel billing ratio. Only scales channel-side usage stats (used quota / dashboard), does not affect user billing. 0 means this channel is not counted.": "チャネル課金倍率:チャネル単位の使用統計(使用済みクォータ/ダッシュボード)のみに反映され、ユーザーの課金には影響しません。0 はこのチャネルをコスト計上しないことを意味します。", |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift
Use feature-scoped hierarchical i18n keys for these new entries.
Lines 4355-4366 add raw text keys instead of semantic keys, which makes namespace ownership and reuse harder (e.g., dashboard.channelStats.totalCost, usageLogs.channel.billingRatio, etc.). Please migrate these new keys to a hierarchical scheme and align call sites.
As per coding guidelines, use “hierarchical and semantically clear translation key names such as dashboard.overview.title and maintain naming consistency”.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/default/src/i18n/locales/ja.json` around lines 4355 - 4366, The new flat
i18n entries ("Channel Billing Ratio", "Channel Cost", "Channel Stats", "Total
Channel Cost", "Total Raw Cost", "Overall Ratio", "Saved", "Channel Cost
Ranking", "Cost Trend", "Top Channels", "View channel-dimension cost
statistics", "Channel billing ratio. Only scales channel-side usage stats...")
must be migrated to hierarchical, feature-scoped keys (e.g.,
dashboard.channelStats.totalCost, dashboard.channelStats.topChannels,
usage.channel.billingRatio, usage.channel.billingRatio.description) and the code
references updated to use those new keys; rename each string to a semantic key
under an appropriate feature namespace (dashboard or usage/usageLogs), replace
usages in UI components and templates to the new keys (search for literal keys
like "Channel Cost" and "Channel Billing Ratio" in code), and keep the original
Japanese translations as values under the new keys to preserve content.
Follow-up refinements to the per-channel billing ratio feature: - Source the channel stats dashboard from the pre-aggregated quota_data table instead of the logs table; add channel_id and channel_quota columns to quota_data - Cap the channel stats time range at 1 month, mirroring GetUserQuotaDates - Fix the channel billing ratio form input so an empty value defaults to 1 instead of 0 (0 remains a deliberate value) - Use int for ChannelQuotaTrendPoint.ChannelQuota to match the quota_data column type - i18n: rename the "Cost Trend" key to "Channel Cost Trend" and show the ratio with an "x" suffix consistently - Add a unit test for Channel.GetChannelRatio
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@controller/usedata.go`:
- Around line 31-35: The handler currently parses startTimestamp and
endTimestamp and only checks the span length, allowing zero or negative/missing
timestamps to pass and cause unbounded aggregation; ensure you validate that
parsed startTimestamp and endTimestamp are > 0 and that endTimestamp >=
startTimestamp before calling model.GetChannelQuotaData (and return an
appropriate JSON error via c.JSON if validation fails). Update the
parsing/validation logic around startTimestamp/endTimestamp (where
strconv.ParseInt and c.Query are used) to check both bounds and ordering, and
reject invalid inputs prior to invoking model.GetChannelQuotaData.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 6725d98a-1e73-4460-be56-cf74f5770c96
📒 Files selected for processing (15)
controller/usedata.gomodel/channel.gomodel/channel_ratio_test.gomodel/log.gomodel/usedata.goweb/classic/src/components/table/usage-logs/UsageLogsColumnDefs.jsxweb/default/src/features/channels/components/drawers/channel-mutate-drawer.tsxweb/default/src/features/dashboard/components/channels/channel-charts.tsxweb/default/src/features/usage-logs/components/columns/common-logs-columns.tsxweb/default/src/i18n/locales/en.jsonweb/default/src/i18n/locales/fr.jsonweb/default/src/i18n/locales/ja.jsonweb/default/src/i18n/locales/ru.jsonweb/default/src/i18n/locales/vi.jsonweb/default/src/i18n/locales/zh.json
✅ Files skipped from review due to trivial changes (4)
- web/default/src/i18n/locales/zh.json
- web/default/src/i18n/locales/ru.json
- web/default/src/i18n/locales/fr.json
- web/default/src/i18n/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (5)
- web/default/src/i18n/locales/vi.json
- web/classic/src/components/table/usage-logs/UsageLogsColumnDefs.jsx
- web/default/src/features/dashboard/components/channels/channel-charts.tsx
- web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx
- model/channel.go
- web/classic: treat a null channel ratio as the default (1) when deciding whether to auto-expand advanced channel settings - web/classic: gate the channel cost line with numeric parsing (Number + isFinite) instead of parseInt to avoid truncation - web/classic: use "Ratio" instead of "Coefficient" for the French channel ratio labels, matching the rest of the locale - i18n: refine the fr/ja/vi wording of the "Saved" metric label
919a254 to
24677fe
Compare
📝 变更描述 / Description
为每个渠道增加一个可配置的「计费倍率」,用于折算渠道维度的用量统计(
channel.used_quota与数据面板),但不改变用户实际扣费。实现思路:用户扣费仍完全由分组倍率决定、保持不变;渠道维度的成本统计单独走一条口径——在
UpdateChannelUsedQuota内部通过渠道缓存解析该渠道的倍率,把累加进used_quota的额度乘以倍率,因此所有现有计费调用点都无需改动。消费日志在写入时把当时的倍率快照进Log.channel_ratio,保证历史统计不受后续改倍率影响;普通用户的日志接口会清掉该字段,仅管理员可见。数据面板新增一个管理员接口,按小时聚合渠道维度成本时间序列。🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。📸 运行证明 / Proof of Work
Summary by CodeRabbit
New Features
UI/UX Improvements
Tests