From 7d3068470f1adff212b42e98d937a7a366318522 Mon Sep 17 00:00:00 2001 From: Anton Sizikov Date: Tue, 4 Aug 2026 11:23:58 +0200 Subject: [PATCH 1/2] feat: retire the app and replace the front page with a sunset notice The Copilot Billing Preview app is being sunset. Remove the CSV upload entry point and replace the landing page with a notice pointing people to their GitHub billing settings. - Add SunsetPage with retirement copy and links to billing settings, budgets docs, and Copilot billing docs - Delete UploadPage (dropzone, file picker, processing UI) - Reduce App to rendering SunsetPage - Update page title and social metadata Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6b6e7284-1e7e-4966-b47f-10333de62759 --- index.html | 10 +- src/App.tsx | 910 +--------------------------------- src/components/SunsetPage.tsx | 63 +++ src/components/UploadPage.tsx | 152 ------ src/components/index.ts | 3 +- src/config/links.ts | 3 + 6 files changed, 75 insertions(+), 1066 deletions(-) create mode 100644 src/components/SunsetPage.tsx delete mode 100644 src/components/UploadPage.tsx 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 ( -
- - -
-
- - Billing Preview -
- {hasReport && ( -
- -
- )} -
- - {hasReport ? ( - showSeatConfirmation ? ( - { void handleSeatConfirmationApply(counts) }} - /> - ) : ( - <> - - -
- - -
- {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 ? ( - <> -
-

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', From a4b1856a01cbc08c755e0ae018602fd1217b3b72 Mon Sep 17 00:00:00 2001 From: Anton Sizikov Date: Tue, 4 Aug 2026 12:02:44 +0200 Subject: [PATCH 2/2] docs: add retirement note to README Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6b6e7284-1e7e-4966-b47f-10333de62759 --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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