diff --git a/.specs/gastown-usage-based-billing.md b/.specs/gastown-usage-based-billing.md index d1c821796d..2ca9aa8fbc 100644 --- a/.specs/gastown-usage-based-billing.md +++ b/.specs/gastown-usage-based-billing.md @@ -461,10 +461,14 @@ Metering and enforcement are separately controlled: - **Announcement** — the default-off `GASTOWN_BILLING_ANNOUNCEMENT_ENABLED` web flag MAY show advance notice; it MUST NOT alter backend billing behavior. -The status object distinguishes these: `enabled` means metering is active (drives the run estimate -and the automatic-start control), while `enforcing` gates cold-start/terminal blocking in the UI. -Enforcement and announcement flags are always enabled in local development while retaining -default-off production behavior; metering runs in development because the meter binding is present. +The status object distinguishes these: `enabled` means metering is active (usage is reported and +the run estimate is populated), while `enforcing` gates cold-start/terminal blocking in the UI. +User-facing billing UI (the estimate pill, cost strings, and automatic-start control) is gated +solely on the announcement flag (`GASTOWN_BILLING_ANNOUNCEMENT_ENABLED`). Neither `enabled` (shadow +metering, on in production before launch) nor `enforcing` (backend enforcement) may reveal billing +UI to users until billing is announced. Enforcement and announcement flags are always enabled in +local development while retaining default-off production behavior; metering runs in development +because the meter binding is present. Dashboards MUST expose awake seconds, base Cloudflare cost, customer charge, gross multiplier, unclosed intervals, SKU admission denials, RPC failures, graceful-stop duration, forced stops, and @@ -503,6 +507,14 @@ counts of `warn` and `stop` verdicts. ## Changelog +### 2026-07-28 -- Gate user-facing billing UI on announcement, not metering + +- User-facing billing UI (estimate pill, cost strings, automatic-start control) is now gated solely + on the announcement flag (`GASTOWN_BILLING_ANNOUNCEMENT_ENABLED`), not on `billing.enabled` or + `billing.enforcing`. Because metering (`enabled`) is on in production for shadow data, keying the + UI off `enabled` surfaced billing estimates to users before launch. The status field `enabled` + remains metering-only; the web `TerminalBar` receives the announcement flag from the town layouts. + ### 2026-07-24 -- Separate reporting from enforcement - Metering now always reports to the meter when the `CONTAINER_USAGE` binding is present; diff --git a/apps/web/src/app/(app)/gastown/[townId]/MayorTerminalBar.tsx b/apps/web/src/app/(app)/gastown/[townId]/MayorTerminalBar.tsx index b6244c608e..c37a12eb55 100644 --- a/apps/web/src/app/(app)/gastown/[townId]/MayorTerminalBar.tsx +++ b/apps/web/src/app/(app)/gastown/[townId]/MayorTerminalBar.tsx @@ -8,12 +8,20 @@ import { GASTOWN_URL } from '@/lib/constants'; export function MayorTerminalBar({ params, basePath, + billingAnnouncementEnabled, }: { params: Promise<{ townId: string }>; basePath?: string; + billingAnnouncementEnabled?: boolean; }) { const { townId } = use(params); // Track dashboard navigation context and sync to TownDO useGastownUiContext(townId, GASTOWN_URL); - return ; + return ( + + ); } diff --git a/apps/web/src/app/(app)/gastown/[townId]/layout.tsx b/apps/web/src/app/(app)/gastown/[townId]/layout.tsx index 3b9e9f3659..ec17d917bc 100644 --- a/apps/web/src/app/(app)/gastown/[townId]/layout.tsx +++ b/apps/web/src/app/(app)/gastown/[townId]/layout.tsx @@ -3,6 +3,7 @@ import { DrawerStackProvider } from '@/components/gastown/DrawerStack'; import { renderDrawerContent } from '@/components/gastown/DrawerStackContent'; import { TerminalBarPadding } from '@/components/gastown/TerminalBarPadding'; import { HideAppTopbar } from '@/components/gastown/HideAppTopbar'; +import { GASTOWN_BILLING_ANNOUNCEMENT_ENABLED } from '@/lib/config.server'; import { MayorTerminalBar } from './MayorTerminalBar'; import { OnboardingTooltipsWrapper } from './OnboardingTooltipsWrapper'; @@ -18,7 +19,10 @@ export default function TownLayout({ {children} - + diff --git a/apps/web/src/app/(app)/organizations/[id]/gastown/[townId]/layout.tsx b/apps/web/src/app/(app)/organizations/[id]/gastown/[townId]/layout.tsx index 0e6dd77999..6a1d93d37d 100644 --- a/apps/web/src/app/(app)/organizations/[id]/gastown/[townId]/layout.tsx +++ b/apps/web/src/app/(app)/organizations/[id]/gastown/[townId]/layout.tsx @@ -3,6 +3,7 @@ import { DrawerStackProvider } from '@/components/gastown/DrawerStack'; import { renderDrawerContent } from '@/components/gastown/DrawerStackContent'; import { TerminalBarPadding } from '@/components/gastown/TerminalBarPadding'; import { HideAppTopbar } from '@/components/gastown/HideAppTopbar'; +import { GASTOWN_BILLING_ANNOUNCEMENT_ENABLED } from '@/lib/config.server'; import { MayorTerminalBar } from '@/app/(app)/gastown/[townId]/MayorTerminalBar'; import { OnboardingTooltips } from '@/components/gastown/OnboardingTooltips'; @@ -21,7 +22,11 @@ export default async function OrgTownLayout({ {children} - + diff --git a/apps/web/src/components/gastown/TerminalBar.tsx b/apps/web/src/components/gastown/TerminalBar.tsx index 9911737f7c..97cd91ac0b 100644 --- a/apps/web/src/components/gastown/TerminalBar.tsx +++ b/apps/web/src/components/gastown/TerminalBar.tsx @@ -52,6 +52,13 @@ type TerminalBarProps = { townId: string; /** Override base path for org-scoped routes (e.g. /organizations/[id]/gastown/[townId]) */ basePath?: string; + /** + * Whether user-facing billing UI (estimate pill, cost strings, automatic-start + * control) may be shown. Driven by GASTOWN_BILLING_ANNOUNCEMENT_ENABLED. This is + * intentionally separate from `billing.enabled`, which only reflects shadow + * metering and is on in production before billing is announced to users. + */ + billingAnnouncementEnabled?: boolean; }; type BillingStatus = GastownOutputs['gastown']['getBillingStatus']; @@ -61,7 +68,11 @@ type BillingStatus = GastownOutputs['gastown']['getBillingStatus']; * Agent terminal tabs are opened/closed via TerminalBarContext. * Can be positioned at bottom/top/right/left with drag-to-resize. */ -export function TerminalBar({ townId, basePath: basePathOverride }: TerminalBarProps) { +export function TerminalBar({ + townId, + basePath: basePathOverride, + billingAnnouncementEnabled = false, +}: TerminalBarProps) { const trpc = useGastownTRPC(); const townBasePath = basePathOverride ?? `/gastown/${townId}`; const { state: sidebarState, isMobile } = useSidebar(); @@ -378,6 +389,7 @@ export function TerminalBar({ townId, basePath: basePathOverride }: TerminalBarP setPosition={setPosition} closeTab={closeTab} billing={billing} + billingAnnouncementEnabled={billingAnnouncementEnabled} townId={townId} /> {!collapsed && ( @@ -452,6 +465,7 @@ export function TerminalBar({ townId, basePath: basePathOverride }: TerminalBarP setPosition={setPosition} closeTab={closeTab} billing={billing} + billingAnnouncementEnabled={billingAnnouncementEnabled} townId={townId} /> void; closeTab: (tabId: string) => void; billing?: BillingStatus; + billingAnnouncementEnabled: boolean; townId: string; }) { const borderClass = horizontal ? 'border-b border-white/[0.06]' : 'border-r border-white/[0.06]'; @@ -605,6 +622,12 @@ function TabBar({ const showRunCostTooltip = billing?.estimatedRunCharge !== undefined && (billing.state === 'running' || billing.state === 'warning' || billing.state === 'stopping'); + // Whether to surface user-facing billing UI (estimate pill, cost strings, + // automatic-start control). Gated solely on the announcement flag. `billing.enabled` + // reflects shadow metering (on in production before launch) and `billing.enforcing` + // reflects backend enforcement; neither should reveal billing UI to users until + // billing is announced. + const showBilling = billing !== undefined && billingAnnouncementEnabled; return (
- {horizontal && billing && (billing.enabled || billing.runPolicy === 'paused_by_user') && ( + {horizontal && billing && showBilling && (
)} - {!horizontal && billing && (billing.enabled || billing.runPolicy === 'paused_by_user') && ( + {!horizontal && billing && showBilling && (