Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail | 1 | /** |
| 2 | * Costs & margin: the arithmetic behind the page, apart from the SVG and | |
| 3 | * the Workers runtime so it can be tested under Node. Money is in micros. | |
| 4 | */ | |
| Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052) | 5 | import type { BillRead, CloudflareCycle, CostDay, CostMappingInput, CostSettings, CycleMeter, OverallMargin, PauseLevel, PlatformGuard, SpendCaps } from "@g1t/contracts"; |
| Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail | 6 | |
| 7 | import { parseDollars, usd } from "./money.ts"; | |
| 8 | ||
| 9 | /** Buckets Cloudflare does not bill: their cost is g1t's own figure. */ | |
| 10 | export const NOT_CLOUDFLARE = new Set(["models"]); | |
| 11 | ||
| 12 | /** A day's cost: Cloudflare's bill, or g1t's own figure where Cloudflare does not bill it. */ | |
| 13 | export function dayCost(day: CostDay): number { | |
| 14 | return NOT_CLOUDFLARE.has(day.bucket) ? day.ownCostMicros : day.cfCostMicros; | |
| 15 | } | |
| 16 | ||
| 17 | export type DayFigures = { day: string; revenueMicros: number; costMicros: number }; | |
| 18 | ||
| 19 | /** Every day from `since` to `until`, inclusive (YYYY-MM-DD, UTC). */ | |
| 20 | export function daysBetween(since: string, until: string): string[] { | |
| 21 | const start = Date.parse(`${since}T00:00:00Z`); | |
| 22 | const end = Date.parse(`${until}T00:00:00Z`); | |
| 23 | if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return []; | |
| 24 | const out: string[] = []; | |
| 25 | for (let at = start; at <= end && out.length < 400; at += 86_400_000) out.push(new Date(at).toISOString().slice(0, 10)); | |
| 26 | return out; | |
| 27 | } | |
| 28 | ||
| 29 | /** | |
| 30 | * A day per day of the range, every day there even with nothing on it. | |
| 31 | * All of g1t: money in (what workspaces paid, the plan too) against every | |
| 32 | * cost. One product: what customers were charged for it at price against | |
| 33 | * what it cost. | |
| 34 | */ | |
| 35 | export function daySeries(days: CostDay[], since: string, until: string, bucket: string | null): DayFigures[] { | |
| 36 | const totals = new Map<string, DayFigures>(daysBetween(since, until).map((day) => [day, { day, revenueMicros: 0, costMicros: 0 }])); | |
| 37 | for (const row of days) { | |
| 38 | if (bucket && row.bucket !== bucket) continue; | |
| 39 | const figures = totals.get(row.day); | |
| 40 | if (!figures) continue; | |
| 41 | figures.revenueMicros += bucket ? row.valueMicros : row.cashMicros; | |
| 42 | figures.costMicros += dayCost(row); | |
| 43 | } | |
| 44 | return [...totals.values()]; | |
| 45 | } | |
| 46 | ||
| Merge costs and margin review: gateway query, own spend, discount meters, superseded rises | 47 | /** |
| 48 | * The margin on the price a markup gives, in percent: cost plus 20% is a | |
| 49 | * 16.7% margin, since the 20% is of the cost and the margin of the price. | |
| 50 | */ | |
| 51 | export function marginOnPrice(markupPercent: number): number { | |
| 52 | return markupPercent > -100 ? (markupPercent / (100 + markupPercent)) * 100 : 0; | |
| 53 | } | |
| 54 | ||
| 55 | /** | |
| 56 | * Who g1t paid over the range: Cloudflare's usage bill, its subscriptions | |
| 57 | * (a month's, over the range) and the model providers. The same total as | |
| 58 | * the statement's All in. | |
| 59 | */ | |
| 60 | export function whoPaid( | |
| 61 | overall: { costMicros: number; cloudflareCostMicros?: number; modelsCostMicros?: number }, | |
| 62 | subscriptionsMicros: number, | |
| 63 | ): { totalMicros: number; cloudflareMicros: number; subscriptionsMicros: number; modelsMicros: number } { | |
| 64 | const modelsMicros = overall.modelsCostMicros ?? 0; | |
| 65 | const cloudflareMicros = overall.cloudflareCostMicros ?? overall.costMicros - modelsMicros; | |
| 66 | return { totalMicros: overall.costMicros + subscriptionsMicros, cloudflareMicros, subscriptionsMicros, modelsMicros }; | |
| 67 | } | |
| 68 | ||
| Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052) | 69 | /** |
| 70 | * Cloudflare's subscriptions over the range: billing's figure (each day its | |
| 71 | * billing cycle's share, the same accrual as the month view), or from a | |
| 72 | * billing that does not send one, a month's over 30 days. | |
| 73 | */ | |
| 74 | export function subscriptionsOver(monthlyMicros: number, days: number, overall?: Pick<OverallMargin, "subscriptionsMicros">): number { | |
| 75 | if (typeof overall?.subscriptionsMicros === "number") return overall.subscriptionsMicros; | |
| Merge costs and margin review: gateway query, own spend, discount meters, superseded rises | 76 | return Math.round((monthlyMicros * days) / 30); |
| 77 | } | |
| 78 | ||
| Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052) | 79 | /** What was given away, by why, for the statement: only the ones with any. */ |
| 80 | export function givenParts(o: OverallMargin): [string, number][] { | |
| 81 | return ( | |
| 82 | [ | |
| 83 | ["100% discounts", o.givenCompedMicros ?? 0], | |
| 84 | ["free use", o.givenFreeMicros ?? 0], | |
| 85 | ["trial", o.givenTrialMicros ?? 0], | |
| 86 | ["open-source pool", o.givenPoolMicros ?? 0], | |
| 87 | ["partial discounts", o.givenDiscountMicros ?? 0], | |
| 88 | ["promotional credit", o.givenCreditPromotionalMicros ?? 0], | |
| 89 | ["goodwill credit", o.givenCreditGoodwillMicros ?? 0], | |
| 90 | ["testing resets", o.givenResetMicros ?? 0], | |
| 91 | ["charged without real money", o.givenUnpaidMicros ?? 0], | |
| 92 | ] as [string, number][] | |
| 93 | ).filter(([, micros]) => micros > 0); | |
| 94 | } | |
| 95 | ||
| 96 | /** How a cycle meter's cost was arrived at, in words. */ | |
| 97 | export function basisLabel(basis: string): string { | |
| 98 | if (basis === "cloudflare") return "Cloudflare's cost"; | |
| 99 | if (basis === "list") return "List price past the included amount"; | |
| 100 | return "No list price: counted at $0"; | |
| 101 | } | |
| 102 | ||
| 103 | /** A cycle's headline: cost so far, the projection and the average day, as Cloudflare's Billable usage page puts them. */ | |
| 104 | export function cycleHeadline(cycle: CloudflareCycle): { title: string; detail: string } { | |
| 105 | return { | |
| 106 | title: `${cycle.start} to ${cycle.end}, day ${cycle.daysElapsed} of ${cycle.days}`, | |
| 107 | detail: `${usd(cycle.usageMicros, { cents: true })} so far, ${usd(cycle.averageDailyMicros, { cents: true })} a day; projected ${usd(cycle.projectedMicros, { cents: true })} for the cycle, and ${usd(cycle.subscriptionsMicros, { cents: true })} of subscriptions`, | |
| 108 | }; | |
| 109 | } | |
| 110 | ||
| 111 | /** Meters with no list price that were used: costed at $0 until one is added. */ | |
| 112 | export function unpricedMeters(meters: CycleMeter[]): CycleMeter[] { | |
| 113 | return meters.filter((m) => m.basis === "none" && m.quantity > 0); | |
| 114 | } | |
| 115 | ||
| 116 | /** What the last read of the bill got, in a sentence, and whether it looks incomplete. */ | |
| 117 | export function billReadNote(read: BillRead): { text: string; warn: boolean } { | |
| 118 | const parts = [`${read.rows.toLocaleString("en-US")} rows in ${read.pages} ${read.pages === 1 ? "page" : "pages"} for ${read.since} to ${read.until}`]; | |
| 119 | if (read.pricingOnlyRows > 0) parts.push(`${read.pricingOnlyRows} with only a pricing quantity, which can be in blocks`); | |
| 120 | parts.push(read.costedRows > 0 ? `${read.costedRows} with Cloudflare's own cost` : "none with a cost of Cloudflare's, so the list prices apply"); | |
| 121 | return { text: `${parts.join("; ")}.`, warn: read.rows === 0 || read.pricingOnlyRows > 0 }; | |
| 122 | } | |
| 123 | ||
| Merge costs and margin review: gateway query, own spend, discount meters, superseded rises | 124 | /** |
| 125 | * A price version's cost and price as the table shows them. A rate g1t | |
| 126 | * sets has no cost behind it; a weight is a multiplier, not money. | |
| 127 | */ | |
| 128 | export function versionCells(v: { costMicros: number; priceMicros: number; basis?: string }): { cost: string; price: string; note: string | null } { | |
| 129 | if (v.basis === "weight") { | |
| 130 | const weight = Number((v.costMicros / 1_000_000).toFixed(6)); | |
| The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404. | 131 | return { cost: "—", price: `×${weight}`, note: "A weight, not money (retired agent token pricing)" }; |
| Merge costs and margin review: gateway query, own spend, discount meters, superseded rises | 132 | } |
| 133 | if (v.basis === "rate") return { cost: "—", price: unitDollars(v.priceMicros), note: "g1t's own rate: no cost behind it" }; | |
| 134 | return { cost: unitDollars(v.costMicros), price: unitDollars(v.priceMicros), note: null }; | |
| 135 | } | |
| 136 | ||
| 137 | /** | |
| 138 | * What became of a proposal, in words, for its line: who decided it, and | |
| 139 | * when one never took effect because a later measurement replaced it. | |
| 140 | */ | |
| 141 | export function proposalOutcome(p: { status: string; decidedBy: string | null; effectiveAt: string | null }): string | null { | |
| 142 | const by = p.decidedBy ? `${p.status === "superseded" ? "applied" : p.status} by ${p.decidedBy}` : null; | |
| 143 | const replaced = "replaced by a later measurement before it took effect; nothing was charged at it"; | |
| 144 | if (p.status === "superseded") return by ? `${by}, then ${replaced}` : "replaced by a later measurement"; | |
| 145 | if ((p.status === "applied" || p.status === "approved") && !p.effectiveAt) return by ? `${by}, then ${replaced}` : replaced; | |
| 146 | return by; | |
| 147 | } | |
| 148 | ||
| Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail | 149 | /** Margin as a whole percent of revenue; none when there was none. */ |
| 150 | export function marginPercent(revenueMicros: number, costMicros: number): number | null { | |
| 151 | return revenueMicros > 0 ? ((revenueMicros - costMicros) / revenueMicros) * 100 : null; | |
| 152 | } | |
| 153 | ||
| 154 | /** `12.3%`, `−4.0%`, or a dash. */ | |
| 155 | export function percentLabel(percent: number | null | undefined, { signed = false }: { signed?: boolean } = {}): string { | |
| 156 | if (percent == null || !Number.isFinite(percent)) return "—"; | |
| 157 | const text = `${Math.abs(percent).toFixed(1)}%`; | |
| 158 | if (percent < 0) return `−${text}`; | |
| 159 | return signed && percent > 0 ? `+${text}` : text; | |
| 160 | } | |
| 161 | ||
| 162 | /** How a margin reads against the floor: under it is danger, close to it a warning. */ | |
| 163 | export function marginTone(percent: number | null | undefined, floor: number): "danger" | "warn" | "mint" | undefined { | |
| 164 | if (percent == null) return undefined; | |
| 165 | if (percent < floor) return "danger"; | |
| 166 | if (percent < floor + 5) return "warn"; | |
| 167 | return "mint"; | |
| 168 | } | |
| 169 | ||
| 170 | /** A count: `1,234` or `1.2M`. */ | |
| 171 | export function countLabel(value: number): string { | |
| 172 | if (!Number.isFinite(value)) return "—"; | |
| 173 | if (Math.abs(value) >= 10_000_000) return `${(value / 1_000_000).toFixed(1)}M`; | |
| 174 | return Math.round(value).toLocaleString("en-US"); | |
| 175 | } | |
| 176 | ||
| 177 | /** What a kind of drift is called. */ | |
| 178 | export function driftLabel(kind: string): string { | |
| 179 | return { count: "Count", cost: "Cost", leak: "Leak" }[kind] ?? kind; | |
| 180 | } | |
| 181 | ||
| 182 | /** A cost per unit in micros, as dollars with as many places as it needs: `$0.000016`, `$0.15`. */ | |
| 183 | export function unitDollars(micros: number): string { | |
| 184 | const dollars = micros / 1_000_000; | |
| 185 | if (dollars === 0) return "$0"; | |
| 186 | if (Math.abs(dollars) >= 1) return `$${dollars.toFixed(2)}`; | |
| 187 | const places = Math.min(10, Math.max(2, 2 - Math.floor(Math.log10(Math.abs(dollars))))); | |
| 188 | return `$${dollars.toFixed(places)}`; | |
| 189 | } | |
| 190 | ||
| 191 | /** The range shown: 7 to 90 days, 30 when not said. */ | |
| 192 | export function parseRange(raw: string | null): number { | |
| 193 | const days = Number(raw); | |
| 194 | return Number.isInteger(days) && days >= 7 && days <= 90 ? days : 30; | |
| 195 | } | |
| 196 | ||
| 197 | /** The product to chart, if it is one the page has. */ | |
| 198 | export function parseBucket(raw: string | null, known: string[]): string | null { | |
| 199 | return raw && known.includes(raw) ? raw : null; | |
| 200 | } | |
| 201 | ||
| 202 | export type Parsed<T> = { ok: true; value: T } | { ok: false; error: string }; | |
| 203 | ||
| 204 | function numberField(form: FormData, name: string): number { | |
| 205 | const raw = String(form.get(name) ?? "").trim(); | |
| 206 | return raw === "" ? Number.NaN : Number(raw); | |
| 207 | } | |
| 208 | ||
| 209 | /** The guardrails from the settings form. */ | |
| 210 | export function parseCostSettings(form: FormData): Parsed<CostSettings> { | |
| 211 | const autoApplyPercent = numberField(form, "autoApplyPercent"); | |
| 212 | const noticeDays = numberField(form, "noticeDays"); | |
| 213 | const marginFloorPercent = numberField(form, "marginFloorPercent"); | |
| 214 | const alertDays = numberField(form, "alertDays"); | |
| 215 | const anomalyFactor = numberField(form, "anomalyFactor"); | |
| 216 | const minDaily = parseDollars(String(form.get("minDailyCost") ?? "")); | |
| 217 | const anomalyFloor = parseDollars(String(form.get("anomalyFloor") ?? "")); | |
| 218 | if (!(autoApplyPercent >= 0 && autoApplyPercent <= 100)) return { ok: false, error: "The guardrail is a percentage from 0 to 100." }; | |
| 219 | if (!(Number.isInteger(noticeDays) && noticeDays >= 0 && noticeDays <= 90)) return { ok: false, error: "Notice is 0 to 90 days." }; | |
| 220 | if (!(marginFloorPercent >= -100 && marginFloorPercent <= 100)) return { ok: false, error: "The margin floor is a percentage." }; | |
| 221 | if (!(Number.isInteger(alertDays) && alertDays >= 1 && alertDays <= 30)) return { ok: false, error: "Alert after 1 to 30 days." }; | |
| 222 | if (!(anomalyFactor > 0 && anomalyFactor <= 100)) return { ok: false, error: "The factor is a positive number." }; | |
| 223 | if (minDaily == null || anomalyFloor == null) return { ok: false, error: "Amounts are dollars to the cent." }; | |
| 224 | return { | |
| 225 | ok: true, | |
| 226 | value: { | |
| 227 | autoApply: form.get("autoApply") === "on", | |
| 228 | autoApplyPercent, | |
| 229 | noticeDays, | |
| 230 | marginFloorPercent, | |
| 231 | alertDays, | |
| 232 | minDailyCostMicros: minDaily, | |
| 233 | anomalyFactor, | |
| 234 | anomalyFloorMicros: anomalyFloor, | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 235 | cardFee: form.get("cardFee") === "on", |
| Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail | 236 | }, |
| 237 | }; | |
| 238 | } | |
| 239 | ||
| 240 | const NAME = /^(\*|[a-z0-9][a-z0-9_]{0,79})$/; | |
| 241 | ||
| 242 | /** A mapping from the mapping form: Cloudflare's product and meter (or `*`) to one of g1t's products. */ | |
| 243 | export function parseMapping(form: FormData): Parsed<CostMappingInput> { | |
| 244 | const value = (name: string) => String(form.get(name) ?? "").trim(); | |
| 245 | const product = value("product").toLowerCase(); | |
| 246 | const meter = value("meter").toLowerCase() || "*"; | |
| 247 | const remove = form.get("remove") === "1"; | |
| 248 | if (!NAME.test(product) || product === "*") return { ok: false, error: "Cloudflare's product, as the lines table names it." }; | |
| 249 | if (!NAME.test(meter)) return { ok: false, error: "A meter prefix as the lines table names it, or * for all of the product." }; | |
| 250 | if (remove) return { ok: true, value: { product, meter, remove: true } }; | |
| 251 | const bucket = value("bucket").toLowerCase(); | |
| 252 | if (!NAME.test(bucket) || bucket === "*") return { ok: false, error: "Which of g1t's products it is a cost of." }; | |
| 253 | const drift = value("driftPercent"); | |
| 254 | const driftPercent = drift === "" ? null : Number(drift); | |
| 255 | if (driftPercent != null && !(driftPercent > 0 && driftPercent <= 1000)) return { ok: false, error: "Drift is a percentage above zero." }; | |
| 256 | return { | |
| 257 | ok: true, | |
| 258 | value: { | |
| 259 | product, | |
| 260 | meter, | |
| 261 | bucket, | |
| 262 | priceMeter: value("priceMeter") || null, | |
| 263 | ownMeter: value("ownMeter") || null, | |
| 264 | scaleToOwn: form.get("scaleToOwn") === "on", | |
| 265 | driftPercent, | |
| 266 | note: value("note").slice(0, 200), | |
| 267 | }, | |
| 268 | }; | |
| 269 | } | |
| 270 | ||
| 271 | // --- g1t's own spend (billing's budget) --------------------------------------- | |
| 272 | ||
| 273 | /** | |
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 274 | * The red bar on every sudo page: the daily breaker open, or a 100%-discount |
| Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail | 275 | * account's monthly budget used up. Null when neither. |
| 276 | */ | |
| 277 | export function spendBanner(caps: SpendCaps): string | null { | |
| 278 | const parts: string[] = []; | |
| 279 | if (caps.tripped) { | |
| 280 | parts.push( | |
| The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404. | 281 | `the daily breaker is open (${usd(caps.todayMicros)} of ${usd(caps.dailyCapMicros)} today), so new work g1t pays for waits until 00:00 UTC`, |
| Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail | 282 | ); |
| 283 | } | |
| 284 | for (const budget of caps.comped) { | |
| 285 | if (budget.ceilingMicros > 0 && budget.usedMicros >= budget.ceilingMicros) { | |
| 286 | parts.push(`${budget.name} used its ${usd(budget.ceilingMicros)} monthly budget, so new work on it is refused`); | |
| 287 | } | |
| 288 | } | |
| 289 | if (parts.length === 0) return null; | |
| 290 | const text = parts.join("; and "); | |
| 291 | return `${text.charAt(0).toUpperCase()}${text.slice(1)}.`; | |
| 292 | } | |
| 293 | ||
| 294 | /** What g1t paid this month, by bucket, with the free tier and Cloudflare's subscriptions; and the total. */ | |
| 295 | export function spendRows(caps: SpendCaps): { rows: { key: string; title: string; micros: number; note: string }[]; totalMicros: number } { | |
| 296 | const notes: Record<string, string> = { | |
| Merge costs and margin review: gateway query, own spend, discount meters, superseded rises | 297 | comped: "Work on accounts with a 100% discount: what it cost g1t, not its price", |
| 298 | trial: "Trial credit, at what it cost g1t", | |
| 299 | oss: "Checks and workflows on public repositories, at what they cost g1t", | |
| 300 | given: "Free workspaces' overruns past their trial, at what they cost g1t", | |
| 301 | unpaid: "Usage charged while payments are in Stripe's test mode: what it cost g1t, not what was charged", | |
| Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail | 302 | }; |
| 303 | const rows = caps.monthBuckets.map((b) => ({ key: b.bucket, title: b.title, micros: b.micros, note: notes[b.bucket] ?? "" })); | |
| Merge costs and margin review: gateway query, own spend, discount meters, superseded rises | 304 | rows.push({ key: "free", title: "Free tier", micros: caps.freeTierMicros, note: "Free workspaces' share of git, storage and platform, as last reconciled" }); |
| Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052) | 305 | const monthly = usd(caps.fixedMonthlyMicros, { cents: true }); |
| 306 | const accrued = typeof caps.fixedMonthMicros === "number"; | |
| Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing | 307 | rows.push({ |
| 308 | key: "fixed", | |
| 309 | title: "Cloudflare subscriptions", | |
| Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052) | 310 | micros: accrued ? (caps.fixedMonthMicros as number) : caps.fixedMonthlyMicros, |
| 311 | note: accrued | |
| 312 | ? `This month's days so far, each its billing cycle's share of ${monthly} a month${caps.fixedSource === "cloudflare" ? ", as Cloudflare lists them" : ", estimated (CLOUDFLARE_FIXED_MONTHLY_MICROS)"}` | |
| 313 | : caps.fixedSource === "cloudflare" | |
| 314 | ? "The whole month, as Cloudflare lists them" | |
| 315 | : "The whole month, estimated (CLOUDFLARE_FIXED_MONTHLY_MICROS)", | |
| Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing | 316 | }); |
| Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail | 317 | return { rows, totalMicros: rows.reduce((sum, row) => sum + row.micros, 0) }; |
| 318 | } | |
| 319 | ||
| 320 | /** How far a cap is used, 0 to 100, for a meter. */ | |
| 321 | export function capPercent(usedMicros: number, capMicros: number): number { | |
| 322 | if (capMicros <= 0) return 0; | |
| 323 | return Math.max(0, Math.min(100, (usedMicros / capMicros) * 100)); | |
| 324 | } | |
| Merge platform pause and the hourly usage watcher: staff can pause compute, schedules, indexing or renders for everyone, the watcher emails on a breach and is never blind quietly, and the models proxy holds each run to its cap (billing 0051, integrations 0006) | 325 | |
| 326 | // --- Platform pause and usage watch (billing's platform.rs) ----------------- | |
| 327 | ||
| 328 | /** What each level of the platform pause stops, for the page and the banner. */ | |
| 329 | export const PAUSE_LEVELS: { level: PauseLevel; title: string; stops: string }[] = [ | |
| The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404. | 330 | { level: "compute", title: "Compute", stops: "New checks, workflow jobs and deploy builds, for every workspace. Runs already going finish." }, |
| 331 | { level: "schedules", title: "Schedules", stops: "Actions' cron-triggered runs." }, | |
| Merge platform pause and the hourly usage watcher: staff can pause compute, schedules, indexing or renders for everyone, the watcher emails on a breach and is never blind quietly, and the models proxy holds each run to its cap (billing 0051, integrations 0006) | 332 | { level: "indexing", title: "Indexing", stops: "Context embeddings and backfills, and search's backfills (they go on from where they were when resumed)." }, |
| 333 | { level: "renders", title: "Renders", stops: "Social card images: a cache miss gets the brand card or the static logo." }, | |
| 334 | ]; | |
| 335 | ||
| 336 | /** Whether a form's level is one of the four. */ | |
| 337 | export function parsePauseLevel(value: unknown): PauseLevel | null { | |
| 338 | const level = String(value ?? ""); | |
| 339 | return PAUSE_LEVELS.some((l) => l.level === level) ? (level as PauseLevel) : null; | |
| 340 | } | |
| 341 | ||
| 342 | /** The red bar on every sudo page while any level is paused. Null when none is. */ | |
| 343 | export function pauseBanner(guard: PlatformGuard): string | null { | |
| 344 | const paused = guard.levels.filter((l) => l.paused); | |
| 345 | if (paused.length === 0) return null; | |
| 346 | const names = paused.map((l) => (l.auto ? `${l.level} (by the usage watcher)` : l.level)); | |
| 347 | const list = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`; | |
| 348 | return `Paused across g1t: ${list}.`; | |
| 349 | } | |
| 350 | ||
| 351 | /** `1.2M`, `240k`, `2.0B`: a count in a few characters. */ | |
| 352 | export function count(value: number): string { | |
| 353 | const n = Math.abs(value); | |
| 354 | if (n >= 1e9) return `${(value / 1e9).toFixed(1)}B`; | |
| 355 | if (n >= 1e6) return `${(value / 1e6).toFixed(1)}M`; | |
| 356 | if (n >= 1e3) return `${Math.round(value / 1e3)}k`; | |
| 357 | return `${Math.round(value)}`; | |
| 358 | } | |
| 359 | ||
| 360 | /** An hour's value as a share of its threshold, rounded; null with no threshold. */ | |
| 361 | export function thresholdShare(value: number, threshold: number): number | null { | |
| 362 | return threshold > 0 ? Math.round((value / threshold) * 100) : null; | |
| 363 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.