diff --git a/README.md b/README.md
index aa796e7..218af8b 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,13 @@
# GitHub Copilot Billing Preview
+> [!IMPORTANT]
+> **This app has been retired.** The hosted instance no longer accepts report uploads. Review and manage your Copilot spend in your [billing settings](https://github.com/settings/billing). To learn more, see [GitHub Copilot billing](https://docs.github.com/copilot/concepts/billing).
+
A web application for previewing and comparing your future GitHub Copilot bills as you transition to the new usage-based billing model. Upload your CSV billing reports to explore requests, costs, AI Credits, and trends across users, organizations, models, and cost centers.
Production instance:
-This project is in active development. It is intended to help GitHub Copilot customers understand usage-based billing preview data during the transition period.
+This project is no longer under active development. The source code remains available for reference and for anyone who wants to fork it.
## Features
diff --git a/index.html b/index.html
index f57a11e..3c02792 100644
--- a/index.html
+++ b/index.html
@@ -4,19 +4,19 @@
-
GitHub Copilot Billing Preview
+ GitHub Copilot Billing Preview has been retired
-
-
+
+
-
-
+
+
diff --git a/src/App.tsx b/src/App.tsx
index db46d65..a826d16 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,913 +1,9 @@
-import { useCallback, useRef, useState } from 'react'
-import type { ChangeEvent, DragEvent, KeyboardEvent, MouseEvent } from 'react'
-import { MarkGithubIcon, GraphIcon, PeopleIcon, CopilotIcon, TableIcon, OrganizationIcon, DatabaseIcon, InfoIcon, QuestionIcon, CreditCardIcon } from '@primer/octicons-react'
-
-import { NewVersionBanner, UploadPage } from './components'
-import { SeatCountConfirmation } from './components/SeatCountConfirmation'
-import { UsersView } from './views/UsersView'
-import type { SeatOverrides } from './views/UsersView'
-import { UserDetailsView } from './views/UserDetailsView'
-import { CostCentersView } from './views/CostCentersView'
-import { OrganizationsView } from './views/OrganizationsView'
-import { ModelsView } from './views/ModelsView'
-import { ReportGuideView } from './views/ReportGuideView'
-import { FaqView } from './views/FaqView'
-import { ProductsView } from './views/ProductsView'
-import { OverviewView } from './views/OverviewView'
-import { CostManagementView } from './views/CostManagementView'
-import { SpendInsightsView } from './views/SpendInsightsView'
-import { appLinks } from './config/links'
-import { QuickStatsAggregator, type QuickStatsResult } from './pipeline/aggregators/quickStatsAggregator'
-import { ReportContextAggregator, type ReportContextResult } from './pipeline/aggregators/reportContextAggregator'
-import { DailyUsageAggregator, type DailyUsageData } from './pipeline/aggregators/dailyUsageAggregator'
-import { ModelUsageAggregator, type ModelUsageResult } from './pipeline/aggregators/modelUsageAggregator'
-import { ProductUsageAggregator, type ProductUsageResult } from './pipeline/aggregators/productUsageAggregator'
-import { CostCenterAggregator, type CostCenterResult } from './pipeline/aggregators/costCenterAggregator'
-import { OrganizationAggregator, type OrganizationResult } from './pipeline/aggregators/organizationAggregator'
-import { UserUsageAggregator, type UserUsageResult } from './pipeline/aggregators/userUsageAggregator'
-import {
- calculateLicenseSummary,
- inferReportPlanScope,
- type AicIncludedCreditsOverrides,
-} from './pipeline/aicIncludedCredits'
-import { resolveIncludedCreditsPolicy } from './pipeline/includedCreditsPolicy'
-import { PRODUCT_BUDGET_COPILOT, PRODUCT_BUDGET_COPILOT_CLOUD_AGENT, PRODUCT_BUDGET_SPARK } from './pipeline/productClassification'
-import { runPipeline } from './pipeline/runPipeline'
-import type { ReportFormatMetadata } from './pipeline/reportAdapters'
-import { runBudgetSimulation, type BudgetSimulationResult } from './utils/budgetSimulation'
-import { EMPTY_BUDGET_VALUES, getDefaultBudgetValues, getUserSpendSegmentsByUsername, type BudgetField, type BudgetValues } from './utils/costManagementBudgets'
-import { calculateIndividualPlanUpgradeRecommendation, getIndividualLicenseMonthlyCost } from './utils/individualPlanUpgrade'
-import { normalizeSeatCount } from './utils/seatCounts'
-import { getReportMode, isNativeAiCreditsMode } from './utils/reportMode'
-import { useAppVersionCheck } from './hooks/useAppVersionCheck'
-
-type Status = 'idle' | 'processing' | 'done'
-type ActiveView = 'overview' | 'users' | 'userDetails' | 'costCenters' | 'orgs' | 'models' | 'products' | 'spendInsights' | 'costManagement' | 'guide' | 'faq'
-
-const BUSINESS_LICENSE_MONTHLY_COST = 19
-const ENTERPRISE_LICENSE_MONTHLY_COST = 39
+import { SunsetPage } from './components'
function App() {
- const [status, setStatus] = useState('idle')
- const [quickStats, setQuickStats] = useState(null)
- const [reportMetadata, setReportMetadata] = useState(null)
- const [reportContext, setReportContext] = useState(null)
- const [error, setError] = useState(null)
- const [fileName, setFileName] = useState(null)
- const [dragActive, setDragActive] = useState(false)
- const [dailyUsageData, setDailyUsageData] = useState([])
- const [activeView, setActiveView] = useState('overview')
- const [userUsage, setUserUsage] = useState(null)
- const [modelUsage, setModelUsage] = useState(null)
- const [productUsage, setProductUsage] = useState(null)
- const [selectedUsername, setSelectedUsername] = useState('')
- const [costCenters, setCostCenters] = useState(null)
- const [orgs, setOrgs] = useState(null)
- const [progress, setProgress] = useState(0)
- const [rowsProcessed, setRowsProcessed] = useState(0)
- const [seatOverrides, setSeatOverrides] = useState({})
- const [budgetValues, setBudgetValues] = useState(EMPTY_BUDGET_VALUES)
- const [budgetSimulation, setBudgetSimulation] = useState(null)
- const [budgetSimulationError, setBudgetSimulationError] = useState(null)
- const [isApplyingBudgetSimulation, setIsApplyingBudgetSimulation] = useState(false)
- const [seatConfirmationPending, setSeatConfirmationPending] = useState(false)
- const [seatConfirmationError, setSeatConfirmationError] = useState(null)
- const [isApplyingSeatConfirmation, setIsApplyingSeatConfirmation] = useState(false)
- const fileInputRef = useRef(null)
- const currentFileRef = useRef(null)
- const latestRunIdRef = useRef(0)
- const latestSimulationIdRef = useRef(0)
- const { isUpdateAvailable, reloadApp } = useAppVersionCheck()
-
- const applyProcessedData = useCallback(({
- quickStats,
- reportMetadata,
- reportContext,
- dailyUsageData,
- modelUsage,
- productUsage,
- costCenters,
- orgs,
- userUsage,
- }: {
- quickStats: QuickStatsResult
- reportMetadata: ReportFormatMetadata
- reportContext: ReportContextResult
- dailyUsageData: DailyUsageData[]
- modelUsage: ModelUsageResult
- productUsage: ProductUsageResult
- costCenters: CostCenterResult
- orgs: OrganizationResult
- userUsage: UserUsageResult
- }) => {
- setQuickStats(quickStats)
- setReportMetadata(reportMetadata)
- setReportContext(reportContext)
- setDailyUsageData(dailyUsageData)
- setModelUsage(modelUsage)
- setProductUsage(productUsage)
- setCostCenters(costCenters)
- setOrgs(orgs)
- setUserUsage(userUsage)
- }, [])
-
- const buildReportData = useCallback(async (
- file: File,
- includedCreditsOverrides: AicIncludedCreditsOverrides = {},
- onProgress?: (progressInfo: { rowsProcessed: number; progressPercent: number }) => void,
- ) => {
- let statsAggregator!: QuickStatsAggregator
- let contextAggregator!: ReportContextAggregator
- let dailyAggregator!: DailyUsageAggregator
- let modelAggregator!: ModelUsageAggregator
- let productAggregator!: ProductUsageAggregator
- let costCenterAggregator!: CostCenterAggregator
- let orgAggregator!: OrganizationAggregator
- let userAggregator!: UserUsageAggregator
-
- const pipelineResult = await runPipeline(file, (reportMetadata, includedCreditsPolicy) => {
- statsAggregator = new QuickStatsAggregator()
- contextAggregator = new ReportContextAggregator()
- dailyAggregator = new DailyUsageAggregator(reportMetadata)
- modelAggregator = new ModelUsageAggregator(reportMetadata)
- productAggregator = new ProductUsageAggregator(reportMetadata)
- costCenterAggregator = new CostCenterAggregator(reportMetadata)
- orgAggregator = new OrganizationAggregator(reportMetadata)
- userAggregator = new UserUsageAggregator(reportMetadata, includedCreditsPolicy)
-
- return [
- statsAggregator,
- contextAggregator,
- dailyAggregator,
- modelAggregator,
- productAggregator,
- costCenterAggregator,
- orgAggregator,
- userAggregator,
- ]
- }, {
- includedCreditsOverrides,
- progressResolution: 500,
- onProgress,
- })
-
- return {
- quickStats: {
- ...statsAggregator.result(),
- lineCount: pipelineResult.reportRowCount,
- },
- reportMetadata: pipelineResult.reportMetadata,
- reportContext: contextAggregator.result(),
- dailyUsageData: dailyAggregator.result().dailyData,
- modelUsage: modelAggregator.result(),
- productUsage: productAggregator.result(),
- costCenters: costCenterAggregator.result(),
- orgs: orgAggregator.result(),
- userUsage: userAggregator.result(),
- }
- }, [])
-
- const reportMode = getReportMode(reportMetadata)
- const isNativeAiCreditsReport = isNativeAiCreditsMode(reportMode)
- const rangeStart = reportContext?.startDate ?? null
- const rangeEnd = reportContext?.endDate ?? null
- const includedCreditsPolicy = resolveIncludedCreditsPolicy(reportMode, { startDate: rangeStart, endDate: rangeEnd })
- const showOrganizationPromotionalDataDisclaimer = reportMode === 'transition-period-billing-preview'
- || includedCreditsPolicy.id === 'native-ai-credits-summer-promo'
-
- const getDefaultSeatCounts = useCallback(() => {
- const summary = calculateLicenseSummary(userUsage?.users ?? [], includedCreditsPolicy)
- return {
- business: normalizeSeatCount(
- summary.rows.find((row) => row.label === 'Copilot Business')?.users ?? 0,
- 0,
- ),
- enterprise: normalizeSeatCount(
- summary.rows.find((row) => row.label === 'Copilot Enterprise')?.users ?? 0,
- 0,
- ),
- }
- }, [includedCreditsPolicy, userUsage])
-
- const resetReportState = useCallback(({ status, fileName }: { status: Status; fileName: string | null }) => {
- setStatus(status)
- setError(null)
- setQuickStats(null)
- setReportMetadata(null)
- setReportContext(null)
- setDailyUsageData([])
- setUserUsage(null)
- setModelUsage(null)
- setProductUsage(null)
- setSelectedUsername('')
- setCostCenters(null)
- setOrgs(null)
- setActiveView('overview')
- setFileName(fileName)
- setDragActive(false)
- setProgress(0)
- setRowsProcessed(0)
- setSeatOverrides({})
- setSeatConfirmationPending(false)
- setSeatConfirmationError(null)
- setIsApplyingSeatConfirmation(false)
- setBudgetValues(EMPTY_BUDGET_VALUES)
- setBudgetSimulation(null)
- setBudgetSimulationError(null)
- setIsApplyingBudgetSimulation(false)
- }, [])
-
- const handleProcess = useCallback(async (file: File) => {
- currentFileRef.current = file
- const runId = ++latestRunIdRef.current
- latestSimulationIdRef.current += 1
- resetReportState({ status: 'processing', fileName: file.name })
-
- try {
- const nextData = await buildReportData(file, {}, (progressInfo) => {
- if (runId !== latestRunIdRef.current) return
- setRowsProcessed(progressInfo.rowsProcessed)
- setProgress(progressInfo.progressPercent)
- })
-
- if (runId !== latestRunIdRef.current) return
-
- setProgress(100)
- applyProcessedData(nextData)
- setBudgetValues(getDefaultBudgetValues(nextData.userUsage.users))
- setSeatConfirmationError(null)
- const processedUsers = nextData.userUsage.users
- const hasOrgContext = processedUsers.some((user) => user.organizations.length > 0 || user.costCenters.length > 0)
- const processedPlanScope = inferReportPlanScope(processedUsers.length, hasOrgContext)
- setSeatConfirmationPending(processedPlanScope === 'organization')
- setStatus('done')
- } catch (err) {
- if (runId !== latestRunIdRef.current) return
- setError(err instanceof Error ? err.message : 'Failed to process the report.')
- setStatus('idle')
- setProgress(0)
- setRowsProcessed(0)
- }
- }, [applyProcessedData, buildReportData, resetReportState])
-
- const resolveIncludedCreditOverrides = useCallback((overrides: SeatOverrides): AicIncludedCreditsOverrides => {
- if (overrides.business === undefined && overrides.enterprise === undefined) {
- return {}
- }
-
- const { business: defaultBusiness, enterprise: defaultEnterprise } = getDefaultSeatCounts()
-
- return {
- business: overrides.business === undefined
- ? defaultBusiness
- : normalizeSeatCount(overrides.business, defaultBusiness),
- enterprise: overrides.enterprise === undefined
- ? defaultEnterprise
- : normalizeSeatCount(overrides.enterprise, defaultEnterprise),
- }
- }, [getDefaultSeatCounts])
-
- const compactSeatOverrides = useCallback((overrides: AicIncludedCreditsOverrides): SeatOverrides => {
- if (overrides.business === undefined && overrides.enterprise === undefined) {
- return {}
- }
-
- const { business: defaultBusiness, enterprise: defaultEnterprise } = getDefaultSeatCounts()
- const compactOverrides: SeatOverrides = {}
-
- if ((overrides.business ?? defaultBusiness) > defaultBusiness) {
- compactOverrides.business = overrides.business
- }
-
- if ((overrides.enterprise ?? defaultEnterprise) > defaultEnterprise) {
- compactOverrides.enterprise = overrides.enterprise
- }
-
- return compactOverrides
- }, [getDefaultSeatCounts])
-
- const handleBudgetValueChange = useCallback((field: BudgetField, value: string) => {
- latestSimulationIdRef.current += 1
- setBudgetValues((current) => ({
- ...current,
- [field]: value,
- }))
- setBudgetSimulation(null)
- setBudgetSimulationError(null)
- setIsApplyingBudgetSimulation(false)
- }, [])
-
- const handleSeatOverridesChange = useCallback(async (
- overrides: SeatOverrides,
- onError?: (message: string) => void,
- ): Promise => {
- const file = currentFileRef.current
- if (!file) return false
-
- const runId = ++latestRunIdRef.current
- latestSimulationIdRef.current += 1
- const resolvedOverrides = resolveIncludedCreditOverrides(overrides)
- setBudgetSimulation(null)
- setBudgetSimulationError(null)
- setIsApplyingBudgetSimulation(false)
- if (!onError) {
- setError(null)
- }
-
- try {
- const nextData = await buildReportData(file, resolvedOverrides)
- if (runId !== latestRunIdRef.current) return false
-
- applyProcessedData(nextData)
- setSeatOverrides(compactSeatOverrides(resolvedOverrides))
- return true
- } catch (err) {
- if (runId !== latestRunIdRef.current) return false
- const message = err instanceof Error ? err.message : 'Failed to recalculate usage-based billing.'
- if (onError) {
- onError(message)
- } else {
- setError(message)
- }
- return false
- }
- }, [applyProcessedData, buildReportData, compactSeatOverrides, resolveIncludedCreditOverrides])
-
- const handleSeatConfirmationApply = useCallback(async (counts: { business: number; enterprise: number }) => {
- setIsApplyingSeatConfirmation(true)
- setSeatConfirmationError(null)
- try {
- const { business: defaultBusiness, enterprise: defaultEnterprise } = getDefaultSeatCounts()
- if (counts.business === defaultBusiness && counts.enterprise === defaultEnterprise) {
- setSeatConfirmationPending(false)
- return
- }
-
- const success = await handleSeatOverridesChange(
- { business: counts.business, enterprise: counts.enterprise },
- setSeatConfirmationError,
- )
- if (success) {
- setSeatConfirmationPending(false)
- }
- } finally {
- setIsApplyingSeatConfirmation(false)
- }
- }, [getDefaultSeatCounts, handleSeatOverridesChange])
-
- const handleApplyBudgetSimulation = useCallback(async () => {
- const file = currentFileRef.current
- if (!file) return
-
- const budgetReportUsers = userUsage?.users ?? []
- const hasBudgetOrganizationContext = budgetReportUsers.some((user) => user.organizations.length > 0 || user.costCenters.length > 0)
- const isIndividualBudgetReport = inferReportPlanScope(budgetReportUsers.length, hasBudgetOrganizationContext) === 'individual'
- const parsedAccountBudget = budgetValues.account.trim() === '' ? undefined : Number(budgetValues.account)
- const parsedUserBudget = !isIndividualBudgetReport && budgetValues.user.trim() !== '' ? Number(budgetValues.user) : undefined
- const parsedPowerUserBudget = !isIndividualBudgetReport && budgetValues.powerUser.trim() !== '' ? Number(budgetValues.powerUser) : undefined
- const parsedHeavyUserBudget = !isIndividualBudgetReport && budgetValues.heavyUser.trim() !== '' ? Number(budgetValues.heavyUser) : undefined
- const parsedProductCloudAgentBudget = !isIndividualBudgetReport && budgetValues.productCloudAgent.trim() !== '' ? Number(budgetValues.productCloudAgent) : undefined
- const parsedProductSparkBudget = !isIndividualBudgetReport && budgetValues.productSpark.trim() !== '' ? Number(budgetValues.productSpark) : undefined
- const parsedProductCopilotBudget = !isIndividualBudgetReport && budgetValues.productCopilot.trim() !== '' ? Number(budgetValues.productCopilot) : undefined
-
- if (
- parsedAccountBudget === undefined
- && parsedUserBudget === undefined
- && parsedPowerUserBudget === undefined
- && parsedHeavyUserBudget === undefined
- && parsedProductCloudAgentBudget === undefined
- && parsedProductSparkBudget === undefined
- && parsedProductCopilotBudget === undefined
- ) {
- setBudgetSimulation(null)
- setBudgetSimulationError(isIndividualBudgetReport
- ? 'Enter an additional usage budget in USD before running the simulation.'
- : 'Enter a user-level, account-level, or product-level budget in USD before running the simulation.')
- return
- }
-
- if (
- (parsedAccountBudget !== undefined && !Number.isFinite(parsedAccountBudget))
- || (parsedUserBudget !== undefined && !Number.isFinite(parsedUserBudget))
- || (parsedPowerUserBudget !== undefined && !Number.isFinite(parsedPowerUserBudget))
- || (parsedHeavyUserBudget !== undefined && !Number.isFinite(parsedHeavyUserBudget))
- || (parsedProductCloudAgentBudget !== undefined && !Number.isFinite(parsedProductCloudAgentBudget))
- || (parsedProductSparkBudget !== undefined && !Number.isFinite(parsedProductSparkBudget))
- || (parsedProductCopilotBudget !== undefined && !Number.isFinite(parsedProductCopilotBudget))
- ) {
- setBudgetSimulation(null)
- setBudgetSimulationError('Enter valid USD budget values before running the simulation.')
- return
- }
-
- const simulationId = ++latestSimulationIdRef.current
- setBudgetSimulationError(null)
- setIsApplyingBudgetSimulation(true)
-
- try {
- const result = await runBudgetSimulation(
- file,
- {
- accountBudgetUsd: parsedAccountBudget,
- userBudgetUsd: parsedUserBudget,
- userBudgetUsdBySpendSegment: {
- power: parsedPowerUserBudget,
- heavy: parsedHeavyUserBudget,
- },
- userSpendSegmentsByUsername: getUserSpendSegmentsByUsername(budgetReportUsers),
- productBudgetsUsd: {
- [PRODUCT_BUDGET_COPILOT_CLOUD_AGENT]: parsedProductCloudAgentBudget,
- [PRODUCT_BUDGET_SPARK]: parsedProductSparkBudget,
- [PRODUCT_BUDGET_COPILOT]: parsedProductCopilotBudget,
- },
- },
- resolveIncludedCreditOverrides(seatOverrides),
- { reportMetadata: reportMetadata ?? undefined },
- )
-
- if (simulationId !== latestSimulationIdRef.current) return
- setBudgetSimulation(result)
- } catch (err) {
- if (simulationId !== latestSimulationIdRef.current) return
- setBudgetSimulation(null)
- setBudgetSimulationError(err instanceof Error ? err.message : 'Failed to run the budget simulation.')
- } finally {
- if (simulationId === latestSimulationIdRef.current) {
- setIsApplyingBudgetSimulation(false)
- }
- }
- }, [
- budgetValues.account,
- budgetValues.heavyUser,
- budgetValues.powerUser,
- budgetValues.productCloudAgent,
- budgetValues.productCopilot,
- budgetValues.productSpark,
- budgetValues.user,
- reportMetadata,
- resolveIncludedCreditOverrides,
- seatOverrides,
- userUsage,
- ])
-
- const preventDefault = (event: DragEvent) => {
- event.preventDefault()
- event.stopPropagation()
- }
-
- const onDrop = (event: DragEvent) => {
- preventDefault(event)
- setDragActive(false)
- const file = event.dataTransfer.files?.[0]
- if (file) {
- void handleProcess(file)
- }
- }
-
- const onDragOver = (event: DragEvent) => {
- preventDefault(event)
- setDragActive(true)
- }
-
- const onDragLeave = (event: DragEvent) => {
- preventDefault(event)
- setDragActive(false)
- }
-
- const onFileChange = (event: ChangeEvent) => {
- const file = event.target.files?.[0]
- if (file) {
- void handleProcess(file)
- event.target.value = ''
- }
- }
-
- const resetToUploadView = () => {
- latestRunIdRef.current += 1
- latestSimulationIdRef.current += 1
- currentFileRef.current = null
- if (fileInputRef.current) {
- fileInputRef.current.value = ''
- }
-
- resetReportState({ status: 'idle', fileName: null })
- }
-
- const triggerFileDialog = (event?: MouseEvent | KeyboardEvent) => {
- event?.preventDefault()
- fileInputRef.current?.click()
- }
-
- const onKeyDown = (event: KeyboardEvent) => {
- if (event.key === 'Enter' || event.key === ' ') {
- triggerFileDialog(event)
- }
- }
-
- const hasReport = status === 'done' && fileName !== null && reportMetadata !== null
- const showSeatConfirmation = hasReport && seatConfirmationPending
- const reportUsers = userUsage?.users ?? []
- const hasOrganizationContext = reportUsers.some((user) => user.organizations.length > 0 || user.costCenters.length > 0)
- const reportPlanScope = inferReportPlanScope(reportUsers.length, hasOrganizationContext)
- const isIndividualReport = reportPlanScope === 'individual' && reportUsers.length === 1
- const individualUser = isIndividualReport ? reportUsers[0] : null
- const { business: defaultBusinessSeats, enterprise: defaultEnterpriseSeats } = getDefaultSeatCounts()
- const effectiveBusinessSeats = seatOverrides.business ?? defaultBusinessSeats
- const effectiveEnterpriseSeats = seatOverrides.enterprise ?? defaultEnterpriseSeats
- const organizationLicenseAmount = effectiveBusinessSeats * BUSINESS_LICENSE_MONTHLY_COST + effectiveEnterpriseSeats * ENTERPRISE_LICENSE_MONTHLY_COST
- const licenseAmount = reportPlanScope === 'organization'
- ? organizationLicenseAmount || undefined
- : individualUser
- ? getIndividualLicenseMonthlyCost(individualUser.totalMonthlyQuota, includedCreditsPolicy)
- : undefined
- const licenseSeatCounts = reportPlanScope === 'organization'
- ? { business: effectiveBusinessSeats, enterprise: effectiveEnterpriseSeats }
- : undefined
- const includedAicPoolSize = reportPlanScope === 'organization'
- ? (effectiveBusinessSeats * includedCreditsPolicy.organizationPlans.business.monthlyIncludedCredits) + (effectiveEnterpriseSeats * includedCreditsPolicy.organizationPlans.enterprise.monthlyIncludedCredits)
- : calculateLicenseSummary(reportUsers, includedCreditsPolicy).totalIncludedAic
-
- const selectedUser = individualUser
- ?? (selectedUsername && userUsage
- ? userUsage.users.find((user) => user.username === selectedUsername) ?? null
- : null)
- const canShowSpendInsights = Boolean(userUsage) && !isIndividualReport && reportUsers.length > 1
- const visibleActiveView = (activeView === 'spendInsights' && !canShowSpendInsights)
- || (isNativeAiCreditsReport && (activeView === 'guide' || activeView === 'faq'))
- ? 'overview'
- : activeView
- const userNavActive = isIndividualReport
- ? visibleActiveView === 'userDetails'
- : visibleActiveView === 'users' || visibleActiveView === 'userDetails'
- const openUserView = () => {
- if (isIndividualReport) {
- setActiveView('userDetails')
- return
- }
-
- setActiveView('users')
- }
-
- const overviewTotals = dailyUsageData.reduce(
- (totals, day) => {
- totals.requests += day.requests
- totals.grossAmount += day.grossAmount
- totals.discountAmount += day.discountAmount
- totals.netAmount += day.netAmount
- totals.aicQuantity += day.aicQuantity
- totals.aicGrossAmount += day.aicGrossAmount
- totals.aicNetAmount += day.aicNetAmount
- return totals
- },
- { requests: 0, grossAmount: 0, discountAmount: 0, netAmount: 0, aicQuantity: 0, aicGrossAmount: 0, aicNetAmount: 0 },
- )
- const overviewPruNetAmount = overviewTotals.netAmount
- const overviewAicNetAmount = overviewTotals.aicNetAmount
- const overviewAicDiscountAmount = Math.max(overviewTotals.aicGrossAmount - overviewAicNetAmount, 0)
- const monthlyAicAdditionalUsageBills = Array.from(dailyUsageData.reduce((monthlyBills, day) => {
- const monthKey = day.date.slice(0, 7)
- monthlyBills.set(monthKey, (monthlyBills.get(monthKey) ?? 0) + day.aicNetAmount)
- return monthlyBills
- }, new Map()).values())
- const individualUpgradeRecommendation = individualUser
- ? calculateIndividualPlanUpgradeRecommendation({
- totalMonthlyQuota: individualUser.totalMonthlyQuota,
- currentMonthlyAicAdditionalUsageBillsUsd: monthlyAicAdditionalUsageBills,
- includedCreditsPolicy,
- })
- : null
-
- const sidebarItemBase = 'flex items-center gap-[10px] w-full px-3 py-[10px] border-0 rounded-md bg-transparent text-[13px] font-medium cursor-pointer text-left transition-colors hover:bg-bg-muted hover:text-fg-default disabled:opacity-40 disabled:cursor-default disabled:hover:bg-transparent disabled:hover:text-fg-muted focus-visible:outline-2 focus-visible:outline-app-accent focus-visible:outline-offset-[-2px] max-sm:justify-center max-sm:p-2'
- const sidebarActive = 'bg-app-accent-subtle text-app-accent font-semibold hover:bg-app-accent-muted'
- const sidebarInactive = 'text-fg-muted'
- const viewContentClasses = 'max-w-[var(--width-content-max)] w-full mx-auto px-6 pt-8 pb-12 flex flex-col gap-6'
-
return (
-
-
-
-
-
- {hasReport ? (
- showSeatConfirmation ? (
-
{ void handleSeatConfirmationApply(counts) }}
- />
- ) : (
- <>
-
-
- File:
- {fileName ?? 'Processing…'}
- {reportContext && (reportContext.startDate || reportContext.endDate) && (
- <>
- |
- Report window:
-
- {reportContext.startDate ?? '—'} to {reportContext.endDate ?? '—'}
-
- >
- )}
- {quickStats && (
- <>
- |
- Total rows:
- {quickStats.lineCount.toLocaleString()}
- >
- )}
-
-
-
-
-
-
- setActiveView('overview')}
- >
-
- Overview
-
-
-
-
- {isIndividualReport ? 'User' : 'Users'}
-
-
- {modelUsage && modelUsage.models.length > 0 && (
- setActiveView('models')}
- >
-
- Models
-
- )}
-
- setActiveView('products')}
- >
-
- Products
-
-
- {orgs && orgs.organizations.length > 0 && (
- setActiveView('orgs')}
- >
-
- Organizations
-
- )}
-
- {costCenters && costCenters.costCenters.length > 0 && (
- setActiveView('costCenters')}
- >
-
- Cost Centers
-
- )}
-
- {canShowSpendInsights && (
- setActiveView('spendInsights')}
- >
-
- Spend Insights
-
- )}
-
- setActiveView('costManagement')}
- >
-
- Cost Management
-
-
- {!isNativeAiCreditsReport && (
- <>
-
-
- setActiveView('guide')}
- >
-
- Report Format
-
-
- setActiveView('faq')}
- >
-
- FAQ
-
- >
- )}
-
-
-
-
- {visibleActiveView === 'overview' ? (
- setActiveView('users') : undefined}
- reportMode={reportMode}
- showOrganizationPromotionalDataDisclaimer={showOrganizationPromotionalDataDisclaimer}
- />
- ) : visibleActiveView === 'models' ? (
- modelUsage && modelUsage.models.length > 0 ? (
-
-
-
- ) : null
- ) : visibleActiveView === 'users' && !isIndividualReport ? (
-
- {
- void handleSeatOverridesChange(overrides)
- }}
- onSelectUser={(username) => {
- setSelectedUsername(username)
- setActiveView('userDetails')
- }}
- reportMode={reportMode}
- includedCreditsPolicy={includedCreditsPolicy}
- />
-
- ) : visibleActiveView === 'userDetails' || (visibleActiveView === 'users' && isIndividualReport) ? (
-
- setActiveView('users')}
- />
-
- ) : visibleActiveView === 'costCenters' ? (
-
-
-
- ) : visibleActiveView === 'products' ? (
-
- ) : visibleActiveView === 'spendInsights' ? (
-
- {
- setSelectedUsername(username)
- setActiveView('userDetails')
- }}
- />
-
- ) : visibleActiveView === 'costManagement' ? (
-
-
-
- ) : !isNativeAiCreditsReport && visibleActiveView === 'guide' ? (
-
-
-
- ) : !isNativeAiCreditsReport && visibleActiveView === 'faq' ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
- >
- )
- ) : (
-
- )}
-
- {hasReport && (
-
- This is a preview based on your uploaded usage data. Actual bills may differ.
- Your data never leaves your browser.{' '}
- Something is not right? Submit an issue .
-
- )}
-
-
+
+
)
}
diff --git a/src/components/SunsetPage.tsx b/src/components/SunsetPage.tsx
new file mode 100644
index 0000000..9a5a79a
--- /dev/null
+++ b/src/components/SunsetPage.tsx
@@ -0,0 +1,63 @@
+import { MarkGithubIcon } from '@primer/octicons-react'
+import { appLinks } from '../config/links'
+
+const linkClasses = 'text-fg-accent underline underline-offset-2 hover:no-underline'
+
+export function SunsetPage() {
+ return (
+
+
+
+
+ Copilot Billing Preview has been retired
+
+
+ We've retired the GitHub Copilot Billing Preview app, and it's no longer available. You can review
+ and manage your Copilot spend directly in your GitHub billing settings.
+
+
+ The app helped you understand your bill during the move to usage-based billing . Your billing
+ settings now give you a more complete view that the app's underlying reports couldn't show,
+ including user-level budgets, cost centers, and usage pool allocation.
+
+
+ What to use instead
+
+ You can review and manage your Copilot spend in your billing settings:
+
+
+
+ See your AI usage on the AI usage page in your{' '}
+
+ billing settings
+
+ , where you can group, filter, and export your AI credit data.
+
+
+ Set{' '}
+
+ budgets
+ {' '}
+ to cap your spend.
+
+
+ If you're an organization or enterprise, user-level budgets help you control and track spend at an
+ individual level.
+
+ Pull raw usage data anytime from your usage reports and the billing API.
+
+
+ To learn more, see{' '}
+
+ GitHub Copilot billing
+
+ .
+
+
+
+
+ GitHub billing remains the source of record for your Copilot spend.
+
+
+ )
+}
diff --git a/src/components/UploadPage.tsx b/src/components/UploadPage.tsx
deleted file mode 100644
index b27d8e9..0000000
--- a/src/components/UploadPage.tsx
+++ /dev/null
@@ -1,152 +0,0 @@
-import type { DragEvent, KeyboardEvent, MouseEvent } from 'react'
-import { MarkGithubIcon, UploadIcon, LockIcon } from '@primer/octicons-react'
-import { appLinks } from '../config/links'
-import { DeprecationBanner } from './DeprecationBanner'
-
-export interface UploadPageProps {
- dragActive: boolean
- isProcessing: boolean
- progress: number
- rowsProcessed: number
- error: string | null
- onDrop: (event: DragEvent) => void
- onDragOver: (event: DragEvent) => void
- onDragLeave: (event: DragEvent) => void
- onClickDropzone: (event?: MouseEvent | KeyboardEvent) => void
- onKeyDown: (event: KeyboardEvent) => void
-}
-
-export function UploadPage({
- dragActive,
- isProcessing,
- progress,
- rowsProcessed,
- error,
- onDrop,
- onDragOver,
- onDragLeave,
- onClickDropzone,
- onKeyDown,
-}: UploadPageProps) {
- const zoneBase =
- 'relative border-2 border-dashed rounded-[16px] text-center ' +
- 'py-7 px-5 sm:py-10 sm:px-8 ' +
- 'transition-[border-color,background,transform] duration-200 ease-in-out'
-
- const zoneState = isProcessing
- ? 'cursor-default border-border-accent bg-bg-accent-muted'
- : dragActive
- ? 'cursor-pointer border-border-accent bg-bg-accent-muted'
- : 'cursor-pointer border-border-default bg-bg-muted hover:border-border-emphasis hover:bg-bg-muted'
-
- const zoneFocus = isProcessing
- ? ''
- : 'focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-border-accent'
-
- return (
-
-
-
-
Copilot Billing Preview
-
- GitHub Copilot usage-based billing uses AI Credits to measure usage and calculate costs.
- This tool helps you analyze your Copilot billing reports and understand usage, included credits,
- and potential charges.
-
-
- Upload a Copilot usage-based billing CSV to analyze your report. Enterprise Admins or Billing Managers
- can download usage-based billing reports from{' '}
- Billing and licensing → Usage → AI usage .
-
-
-
- Learn more about usage-based billing →
-
-
-
- {error && (
-
- ⚠️ {error}
-
- )}
-
-
- {isProcessing ? (
- <>
-
-
-
Processing file…
-
0 ? `${rowsProcessed.toLocaleString()} rows processed` : undefined}
- >
-
-
- {rowsProcessed > 0 && (
-
- {rowsProcessed.toLocaleString()} rows processed
-
- )}
-
- >
- ) : (
- <>
-
-
-
-
Drop your CSV here or click to browse
-
Copilot usage-based billing CSV
- >
- )}
-
-
-
Accepted: transition-period billing preview reports and usage-based billing reports
-
-
-
- Your data stays private
-
-
- * All processing happens in your browser. Your CSV is never uploaded to any server.
- * No data is stored, cached, or sent over the network.
- * When you close this tab, your data is gone.
- * This page makes zero external network requests with your data.
-
-
-
-
- This analysis is based on your uploaded usage data. GitHub billing remains the source of record.
-
- Your data never leaves your browser. Something is not right?{' '}
-
- Submit an issue
-
- .
-
-
- )
-}
diff --git a/src/components/index.ts b/src/components/index.ts
index 3578acb..cdea5e2 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -9,7 +9,6 @@ export type { DualAxisLineChartProps, LineSeries } from './DualAxisLineChart'
export { NewVersionBanner } from './NewVersionBanner'
export type { NewVersionBannerProps } from './NewVersionBanner'
export { DeprecationBanner } from './DeprecationBanner'
-export { UploadPage } from './UploadPage'
-export type { UploadPageProps } from './UploadPage'
+export { SunsetPage } from './SunsetPage'
export { SummaryCard, CostBreakdownCard, BillingComparisonCard } from './ui'
export type { SummaryCardProps, CostBreakdownCardProps, CostBreakdownItem, BillingComparisonCardProps } from './ui'
diff --git a/src/config/links.ts b/src/config/links.ts
index 6519a49..fcf1605 100644
--- a/src/config/links.ts
+++ b/src/config/links.ts
@@ -3,6 +3,9 @@ export const appLinks = {
billingBudgets: 'https://github.com/settings/billing/budgets',
newBillingBudget: 'https://github.com/settings/billing/budgets/new',
billingDocs: 'https://docs.github.com/billing',
+ billingSettings: 'https://github.com/settings/billing',
+ budgetsDocs: 'https://docs.github.com/billing/how-tos/set-up-budgets',
+ copilotBillingDocs: 'https://docs.github.com/copilot/concepts/billing',
manageCopilotUsageDocs: 'https://docs.github.com/billing/managing-your-copilot-usage',
promotionalAmountsDocs: 'https://docs.github.com/en/enterprise-cloud@latest/copilot/concepts/billing/usage-based-billing-for-organizations-and-enterprises#promotional-amounts-for-existing-customers',
usageBasedBillingForIndividualsDocs: 'https://docs.github.com/en/copilot/concepts/billing/usage-based-billing-for-individuals',