Ctrl K
ComunidadGuías…

src/dashboard/usage.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * Normalize the app.devin.ai billing reads into one dashboard payload.
 *
 * Pure and defensive: the upstream shape is FastAPI JSON, but a missing field
 * must degrade to null rather than NaN or a thrown render error.
 */

function num(value, fallback = null) {
  return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
}

function bool(value, fallback = null) {
  return typeof value === 'boolean' ? value : fallback;
}

function str(value) {
  return typeof value === 'string' && value ? value : null;
}

/** Clamp a percentage to 0–100. Non-numbers stay null (no string coercion). */
export function clampPercent(value) {
  if (typeof value !== 'number' || !Number.isFinite(value)) return null;
  return Math.min(100, Math.max(0, value));
}

/**
 * Render an ACU limit. `null` means unlimited (the upstream sends null, never 0),
 * so it must render as ∞ rather than as a zero budget.
 */
export function formatAcuLimit(value) {
  if (value === null || value === undefined) return '∞';
  if (typeof value !== 'number' || !Number.isFinite(value)) return null;
  return String(value);
}

/**
 * Milliseconds until an ISO reset timestamp, or null when the timestamp is
 * missing/invalid. `Date.parse` honours the embedded offset, so the countdown
 * does not depend on the browser's local timezone.
 */
export function resetCountdown(resetAt, now = Date.now()) {
  if (typeof resetAt !== 'string') return null;
  const at = Date.parse(resetAt);
  if (!Number.isFinite(at)) return null;
  return Math.max(0, at - now);
}

function normalizeDays(days) {
  if (!Array.isArray(days)) return [];
  return days.slice(0, 366).map((day) => ({
    date: str(day?.date),
    // A row exists only when the upstream sent a date. `amount`/`cumulative`
    // may still be absent on that row: unknown is null, never a fabricated 0.
    amount: num(day?.amount),
    cumulative: num(day?.cumulative),
  })).filter((day) => day.date);
}

export function normalizeUsage({ quota, status, limits, dailyUsage, now = Date.now() } = {}) {
  const q = quota && typeof quota === 'object' ? quota : {};
  const s = status && typeof status === 'object' ? status : {};
  const l = limits && typeof limits === 'object' ? limits : {};
  const d = dailyUsage && typeof dailyUsage === 'object' ? dailyUsage : {};
  const days = normalizeDays(d.days);
  const products = Array.isArray(d.products)
    ? d.products.slice(0, 12).map((product) => ({
      view: str(product?.view),
      total: num(product?.total),
      days: normalizeDays(product?.days),
    })).filter((product) => product.view)
    : [];

  return {
    plan: str(s.plan_slug),
    billingValid: bool(s.is_subscription_valid),
    billingMethod: str(s.billing_method),
    availableCredits: num(s.available_credits),
    // Signed on-demand (overage) balance in USD, passed through verbatim. A
    // negative value is a real overdraft (usage posted beyond the prepaid
    // amount), not a rounding error and not an "unknown" sentinel; the panel
    // renders it with its sign instead of clamping it to zero.
    overageCredits: num(s.overage_credits),
    quota: {
      isQuotaPlan: bool(q.is_quota_plan),
      hasAllocation: bool(q.has_quota_allocation),
      dailyPercentage: clampPercent(q.daily_percentage),
      weeklyPercentage: clampPercent(q.weekly_percentage),
      dailyResetAt: str(q.daily_reset_at),
      weeklyResetAt: str(q.weekly_reset_at),
      overageBalance: num(q.overage_balance),
      hideDailyQuota: bool(q.hide_daily_quota),
    },
    limits: {
      maxAcuLimit: num(l.max_acu_limit),
      maxTotalSessionAcuLimit: num(l.max_total_session_acu_limit),
    },
    cycle: {
      start: str(d.cycle_start),
      end: str(d.cycle_end),
      previousAvailable: bool(d.previous_cycle_available),
      // `null` when the daily-usage read did not carry a total: a cycle total of
      // 0 would assert "no ACU spent this cycle", which is the fabricated-zero
      // class this payload exists to avoid. A real reported 0 stays 0.
      total: num(d.total),
    },
    days,
    products,
    reset: {
      dailyInMs: resetCountdown(q.daily_reset_at, now),
      weeklyInMs: resetCountdown(q.weekly_reset_at, now),
    },
    serverNow: now,
  };
}