Costs: the plan's included usage counts as paid for the usage it covered, out of what plans leave for running g1t; the run button shows it is running with CSS alone (sudo ships no JavaScript)
5 files+99−190/5 viewed
| 65 | 65 | font-variant-numeric: tabular-nums; | |
| 66 | 66 | } | |
| 67 | 67 | } | |
| 68 | + | ||
| 69 | + | /* | |
| 70 | + | * sudo ships no JavaScript, so a long-running button says it is running | |
| 71 | + | * with CSS alone: a clicked button keeps focus while the old page waits for | |
| 72 | + | * the response. `:focus-visible` is left out, so tabbing to it does not | |
| 73 | + | * read as running. | |
| 74 | + | */ | |
| 75 | + | .run-button .run-busy, | |
| 76 | + | .run-button + .run-note { | |
| 77 | + | display: none; | |
| 78 | + | } | |
| 79 | + | .run-button:focus:not(:focus-visible) .run-idle { | |
| 80 | + | display: none; | |
| 81 | + | } | |
| 82 | + | .run-button:focus:not(:focus-visible) .run-busy { | |
| 83 | + | display: inline-flex; | |
| 84 | + | align-items: center; | |
| 85 | + | gap: 0.5rem; | |
| 86 | + | } | |
| 87 | + | .run-button:focus:not(:focus-visible) + .run-note { | |
| 88 | + | display: inline; | |
| 89 | + | } | |
| 90 | + | .run-spinner { | |
| 91 | + | width: 0.8rem; | |
| 92 | + | height: 0.8rem; | |
| 93 | + | border-radius: 9999px; | |
| 94 | + | border: 2px solid currentColor; | |
| 95 | + | border-right-color: transparent; | |
| 96 | + | animation: run-spin 0.8s linear infinite; | |
| 97 | + | } | |
| 98 | + | @keyframes run-spin { | |
| 99 | + | to { | |
| 100 | + | transform: rotate(360deg); | |
| 101 | + | } | |
| 102 | + | } | |
| 103 | + | @media (prefers-reduced-motion: reduce) { | |
| 104 | + | .run-spinner { | |
| 105 | + | animation: none; | |
| 106 | + | } | |
| 107 | + | } |
| 1 | 1 | import type { ReactNode } from "react"; | |
| 2 | − | import { Form, Link, data, redirect, useNavigation } from "react-router"; | |
| 2 | + | import { Link, data, redirect } from "react-router"; | |
| 3 | 3 | ||
| 4 | 4 | import type { CostsReport, PriceProposal } from "@g1t/contracts"; | |
| 5 | 5 | ||
| 613 | 613 | } | |
| 614 | 614 | ||
| 615 | 615 | /** g1t's own spend: the daily breaker, comped budgets, and this month by what paid. */ | |
| 616 | − | /** The button that runs the nightly analysis now, saying so while it runs. */ | |
| 616 | + | /** | |
| 617 | + | * The button that runs the nightly analysis now. sudo runs no JavaScript, | |
| 618 | + | * so app.css's run-button rules say it is running while the page waits. | |
| 619 | + | */ | |
| 617 | 620 | function RunButton() { | |
| 618 | − | const navigation = useNavigation(); | |
| 619 | − | const running = navigation.state !== "idle" && navigation.formData?.get("intent") === "run"; | |
| 620 | 621 | return ( | |
| 621 | − | <Form method="post" className="flex items-center gap-3"> | |
| 622 | + | <form method="post" className="flex items-center gap-3"> | |
| 622 | 623 | <input type="hidden" name="intent" value="run" /> | |
| 623 | − | <Button variant="quiet" type="submit" disabled={running}> | |
| 624 | − | {running ? "Running the analysis…" : "Run the analysis now"} | |
| 624 | + | <Button variant="quiet" type="submit" className="run-button"> | |
| 625 | + | <span className="run-idle">Run the analysis now</span> | |
| 626 | + | <span className="run-busy"> | |
| 627 | + | <span className="run-spinner" aria-hidden="true" /> | |
| 628 | + | Running the analysis… | |
| 629 | + | </span> | |
| 625 | 630 | </Button> | |
| 626 | − | {running && <span className="text-xs text-muted">Reading the bill and reconciling 31 days; about a minute.</span>} | |
| 627 | − | </Form> | |
| 631 | + | <span className="run-note text-xs text-muted">Reading the bill and reconciling 31 days; about a minute.</span> | |
| 632 | + | </form> | |
| 628 | 633 | ); | |
| 629 | 634 | } | |
| 630 | 635 | ||
| 638 | 643 | // Reports from before the statement have only the totals. | |
| 639 | 644 | const given = o.givenMicros ?? 0; | |
| 640 | 645 | const usageCost = o.usageCostMicros ?? o.costMicros - given; | |
| 641 | − | const usageMargin = o.usageMarginMicros ?? o.usageMicros - usageCost; | |
| 646 | + | const included = o.includedMicros ?? 0; | |
| 647 | + | const usageIn = o.usageMicros + included; | |
| 648 | + | const usageMargin = o.usageMarginMicros ?? usageIn - usageCost; | |
| 642 | 649 | const usagePercent = o.usageMarginPercent !== undefined ? o.usageMarginPercent : null; | |
| 643 | 650 | const running = o.runningCostMicros ?? 0; | |
| 644 | 651 | const unmapped = o.unmappedCostMicros ?? 0; | |
| 656 | 663 | const rows: { title: string; note: string; in: number | null; cost: number; result: number | null; tone?: "danger" | "warn" | "muted" }[] = [ | |
| 657 | 664 | { | |
| 658 | 665 | title: "Usage sold", | |
| 659 | − | note: "What workspaces paid for usage, against what that usage cost", | |
| 660 | − | in: o.usageMicros, | |
| 666 | + | note: | |
| 667 | + | included > 0 | |
| 668 | + | ? `What workspaces paid for usage (${usd(o.usageMicros)}) and their plan's included usage (${usd(included)}), against what it cost` | |
| 669 | + | : "What workspaces paid for usage, against what that usage cost", | |
| 670 | + | in: usageIn, | |
| 661 | 671 | cost: usageCost, | |
| 662 | 672 | result: usageMargin, | |
| 663 | 673 | tone: usageMargin < 0 ? "danger" : undefined, | |
| 664 | 674 | }, | |
| 665 | 675 | { | |
| 666 | 676 | title: "Running g1t", | |
| 667 | − | note: "Plans, against Workers, D1, KV, Queues and the rest of the platform", | |
| 668 | − | in: o.plansMicros, | |
| 677 | + | note: | |
| 678 | + | included > 0 | |
| 679 | + | ? `Plans (${usd(o.plansMicros)}) less the included usage they paid for, against Workers, D1, KV, Queues and the rest` | |
| 680 | + | : "Plans, against Workers, D1, KV, Queues and the rest of the platform", | |
| 681 | + | in: o.plansMicros - included, | |
| 669 | 682 | cost: running, | |
| 670 | − | result: o.plansMicros - running, | |
| 671 | − | tone: o.plansMicros - running < 0 ? "warn" : undefined, | |
| 683 | + | result: o.plansMicros - included - running, | |
| 684 | + | tone: o.plansMicros - included - running < 0 ? "warn" : undefined, | |
| 672 | 685 | }, | |
| 673 | 686 | { | |
| 674 | 687 | title: "Cloudflare subscriptions", | |
| 697 | 710 | <Stat | |
| 698 | 711 | label="Margin on usage sold" | |
| 699 | 712 | value={percentLabel(usagePercent)} | |
| 700 | − | hint={`${usd(o.usageMicros)} paid for usage that cost ${usd(usageCost)}`} | |
| 713 | + | hint={`${usd(usageIn)} paid for usage that cost ${usd(usageCost)}`} | |
| 701 | 714 | tone={marginTone(usagePercent, floor)} | |
| 702 | 715 | /> | |
| 703 | 716 | <Stat |
| 2432 | 2432 | /// `cost_micros` by who g1t pays: Cloudflare's bill (billed amounts, | |
| 2433 | 2433 | /// after the included allowances), and model providers (the ledger's | |
| 2434 | 2434 | /// cost of the tokens, which Cloudflare's bill does not show). | |
| 2435 | + | /// What the plan's included usage paid for, at price (the ledger's | |
| 2436 | + | /// `credit_micros`, comped workspaces left out): money in for usage, | |
| 2437 | + | /// paid out of `plans_micros`. | |
| 2438 | + | #[serde(default)] | |
| 2439 | + | pub included_micros: i64, | |
| 2435 | 2440 | #[serde(default)] | |
| 2436 | 2441 | pub cloudflare_cost_micros: i64, | |
| 2437 | 2442 | #[serde(default)] |
| 1081 | 1081 | givenTrialMicros?: number; | |
| 1082 | 1082 | givenPoolMicros?: number; | |
| 1083 | 1083 | /** costMicros by who g1t pays: Cloudflare's bill (billed, after included allowances) and model providers (tokens, not on Cloudflare's bill). */ | |
| 1084 | + | /** What the plan's included usage paid for, at price: money in for usage, paid out of plansMicros. */ | |
| 1085 | + | includedMicros?: number; | |
| 1084 | 1086 | cloudflareCostMicros?: number; | |
| 1085 | 1087 | modelsCostMicros?: number; | |
| 1086 | 1088 | }; |
| 1447 | 1447 | let sold = (overall.cost_micros - overall.given_micros).max(0); | |
| 1448 | 1448 | overall.sold_margin_micros = revenue - sold; | |
| 1449 | 1449 | overall.sold_margin_percent = margin_percent(revenue, sold); | |
| 1450 | − | overall.usage_margin_micros = overall.usage_micros - overall.usage_cost_micros; | |
| 1451 | − | overall.usage_margin_percent = margin_percent(overall.usage_micros, overall.usage_cost_micros); | |
| 1450 | + | // The plan's included usage was paid for by the plan's price: it is | |
| 1451 | + | // money in for the usage it covered, and out of what the plans | |
| 1452 | + | // leave for running g1t. | |
| 1453 | + | #[derive(Deserialize)] | |
| 1454 | + | struct Included { | |
| 1455 | + | micros: Option<i64>, | |
| 1456 | + | } | |
| 1457 | + | overall.included_micros = self | |
| 1458 | + | .db | |
| 1459 | + | .prepare(format!( | |
| 1460 | + | "SELECT SUM(COALESCE(credit_micros, 0)) AS micros FROM ledger | |
| 1461 | + | WHERE kind = 'usage' AND created_at >= ?1 AND created_at <= ?2 AND workspace NOT IN ({})", | |
| 1462 | + | crate::sales::INTERNAL_SQL | |
| 1463 | + | )) | |
| 1464 | + | .bind(&[since.as_str().into(), format!("{until}T23:59:59.999Z").into()])? | |
| 1465 | + | .first::<Included>(None) | |
| 1466 | + | .await? | |
| 1467 | + | .and_then(|r| r.micros) | |
| 1468 | + | .unwrap_or(0); | |
| 1469 | + | let usage_in = overall.usage_micros + overall.included_micros; | |
| 1470 | + | overall.usage_margin_micros = usage_in - overall.usage_cost_micros; | |
| 1471 | + | overall.usage_margin_percent = margin_percent(usage_in, overall.usage_cost_micros); | |
| 1452 | 1472 | let mut products: Vec<ProductMargin> = products.into_values().collect(); | |
| 1453 | 1473 | products.sort_by_key(|p| std::cmp::Reverse(p.cost_micros.max(p.value_micros))); | |
| 1454 | 1474 |