Files
textbee/web/lib/usage.ts
isra el a949729c07 feat: rebuild dashboard home around real usage and activity data
The home page was four all-time counters that could not answer either
question a user actually opens the dashboard for: how much quota is left, and
did my recent sends work.

Removed fabricated data:
- Every stat card rendered a green TrendingUp arrow whenever a value existed.
  Nothing computed a trend. The stats endpoint returns running totals with no
  time window, so there was nothing to compare against.
- "Since last year" on the sent and received counts. getStatsForUser sums
  device counters with no date filter, so these are all-time totals.
- "Active Devices" with a "Connected now" caption, over a number that counts
  every device including disabled ones. Enabled count is now derived from the
  device list we already fetch.
- "Active keys" over a count that included revoked keys; now uses the active
  API key list.

Added, all from fields the API already returns:
- Usage cards leading the page: today and this month against their limits,
  with progress bars, an amber near-limit state past 80% and an upgrade link.
  Unlimited plans (-1) show usage with no meter, since there is no ceiling to
  measure against.
- Recent activity: the last 5 messages for the connected device. Messages are
  only exposed per device, so the card names the device rather than implying
  account-wide coverage.
- All-time totals compacted from four large cards into one honest strip.

lib/usage.ts extracts the daily/monthly derivation that subscription-info
already did inline, so the dashboard and billing page cannot disagree about
remaining quota. Unit tested including the -1 sentinel, custom limit
overrides, and an over-100 percentage (possible when a limit is lowered
mid-period).

No backend changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:32:59 +03:00

68 lines
1.9 KiB
TypeScript

import type { Subscription } from '@/lib/api/types'
// The backend uses -1 to mean "no limit".
export const UNLIMITED = -1
export type UsageWindow = {
used: number
limit: number | undefined
remaining: number
// 0-100, clamped. Meaningless when unlimited.
percentage: number
unlimited: boolean
// True once the user is close enough to the limit to warrant a nudge.
nearLimit: boolean
atLimit: boolean
}
export const NEAR_LIMIT_PERCENT = 80
function buildWindow(
used: number | undefined,
limit: number | undefined,
remaining: number | undefined,
percentage: number | undefined
): UsageWindow {
const unlimited = limit === UNLIMITED
// The backend reports percentage directly, but it is only meaningful for a
// real limit and can exceed 100 when a limit was lowered mid-period.
const pct = unlimited ? 0 : Math.min(100, Math.max(0, percentage ?? 0))
return {
used: used ?? 0,
limit,
remaining: remaining ?? 0,
percentage: pct,
unlimited,
nearLimit: !unlimited && pct >= NEAR_LIMIT_PERCENT && pct < 100,
atLimit: !unlimited && pct >= 100,
}
}
/**
* Derive the daily and monthly send windows from a subscription.
*
* Shared by the dashboard usage cards and the billing page so the two can
* never disagree about how much quota is left. `usage` values win over `plan`
* values because they already account for per-account custom overrides.
*/
export function deriveUsage(subscription: Subscription | undefined) {
const plan = subscription?.plan
const usage = subscription?.usage
return {
daily: buildWindow(
usage?.processedSmsToday,
usage?.dailyLimit ?? plan?.dailyLimit,
usage?.dailyRemaining,
usage?.dailyUsagePercentage
),
monthly: buildWindow(
usage?.processedSmsLastMonth,
usage?.monthlyLimit ?? plan?.monthlyLimit,
usage?.monthlyRemaining,
usage?.monthlyUsagePercentage
),
}
}