carlos / platform Public

console: wire economics.Project into the headline tiles (mtd only)

The spec promises "month-to-date with projection" and "projections are
labelled projections", but economics.Project had zero callers.

Adds a "project" template func (server.go) wrapping economics.Project,
and on /fleet/economics: when Period=="mtd" and CoveredDays>0, each of
the revenue/cost/profit tiles (not margin — a ratio doesn't project)
gains a second line, "projected $X for the month". The inline knob
script's econ_maths_js gains the exact JS transliteration (project(mtd,
covered, days)); recompute() updates the same three lines on a knob
change, guarded by a Go template (not just JS) mtd check so a non-mtd
page's JS source never carries the word "projected" at all, only a
non-mtd render's DOM lacking the element (the honesty rule the finding
started from). The file header comment's transliteration list now names
Project alongside AllocateBox/PriceAccount.

New view-level test (economicspage_test.go) renders the template
directly against a hand-built EconomicsView (CoveredDays 10/31, known
account cost/revenue) rather than a real sweep, so the expected
projected figures are exact: asserts the mtd render contains both the
word "projected" and the right figures, and a 7d render contains
neither.

economics_accounts.html shares econ_maths_js but never calls project()
- confirmed no name collision, no behavior change to that page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 6fb890508947d4ff6ef0141d2a1f304b6915f4cc parent d293ca6
3 files changed, +118 −1
  • internal/console/economicspage_test.go +74 −0
  • internal/console/server.go +9 −0
  • internal/console/templates/economics.html +35 −1
diff --git a/internal/console/economicspage_test.go b/internal/console/economicspage_test.go
index 5cc78d4af..9ba7fce25 100644
--- a/internal/console/economicspage_test.go
+++ b/internal/console/economicspage_test.go
@@ -1,6 +1,8 @@
package console
import (
+ "bytes"
+ "fmt"
"net/http"
"strings"
"testing"
@@ -114,6 +116,78 @@ func TestEconomicsPageContent(t *testing.T) {
}
}
+// TestEconomicsPageMTDShowsProjection is the spec's "month-to-date with
+// projection" promise (F2, unit-economics final review): on "mtd" with at
+// least one covered day, each of the three dollar tiles (revenue, cost,
+// profit — never margin, a ratio doesn't project) gains a second line
+// naming the word "projected" and carrying economics.Project's own figure.
+// Rendered directly through templates.ExecuteTemplate against a hand-built
+// EconomicsView (the ladder_test.go pattern) rather than a real
+// LoadEconomics sweep, so CoveredDays/PeriodDays are exact, known numbers
+// instead of whatever today's real calendar date happens to produce.
+func TestEconomicsPageMTDShowsProjection(t *testing.T) {
+ view := &EconomicsView{
+ Period: "mtd",
+ CoveredDays: 10,
+ PeriodDays: 31,
+ Accounts: []economics.AccountEconomics{{Account: fixtureAcct, CostUSD: 100, RevenueUSD: 200}},
+ }
+ // No overhead/box-idle line items on the scenario, so the tiles' totals
+ // are exactly the one account's own numbers: revenue 200, cost 100,
+ // profit 100 — easy to hand-check economics.Project's 31/10 scale-up
+ // against.
+ p := page{Title: "Fleet economics", CSRF: "tok", Economics: view}
+ var buf bytes.Buffer
+ if err := templates.ExecuteTemplate(&buf, "economics", p); err != nil {
+ t.Fatalf("render economics: %v", err)
+ }
+ html := buf.String()
+
+ wantRevenue := usd(economics.Project(200, 10, 31)) // $620.00
+ wantCost := usd(economics.Project(100, 10, 31)) // $310.00
+ wantProfit := usd(economics.Project(100, 10, 31)) // $310.00
+ for _, want := range []string{
+ "projected " + wantRevenue + " for the month",
+ "projected " + wantCost + " for the month",
+ "projected " + wantProfit + " for the month",
+ } {
+ if !strings.Contains(html, want) {
+ t.Fatalf("mtd page missing %q in:\n%s", want, html)
+ }
+ }
+ if !strings.Contains(html, "projected") {
+ t.Fatal("mtd page must contain the word \"projected\" (honesty rule)")
+ }
+
+ // A non-mtd period must show none of it: the projected line only ever
+ // exists on "mtd" (economics.html's $showProjection guard).
+ view7d := &EconomicsView{
+ Period: "7d",
+ CoveredDays: 7,
+ PeriodDays: 7,
+ Accounts: []economics.AccountEconomics{{Account: fixtureAcct, CostUSD: 100, RevenueUSD: 200}},
+ }
+ p7d := page{Title: "Fleet economics", CSRF: "tok", Economics: view7d}
+ var buf7d bytes.Buffer
+ if err := templates.ExecuteTemplate(&buf7d, "economics", p7d); err != nil {
+ t.Fatalf("render economics (7d): %v", err)
+ }
+ if strings.Contains(buf7d.String(), "projected") {
+ t.Fatalf("?period=7d page must not contain \"projected\":\n%s", buf7d.String())
+ }
+}
+
+// usd mirrors server.go's "usd" template func (same rounding, same sign
+// handling) so this test's expected strings are computed the exact way the
+// page itself renders them, without reaching into the unexported FuncMap
+// closure directly.
+func usd(v float64) string {
+ if v < 0 {
+ return fmt.Sprintf("-$%.2f", -v)
+ }
+ return fmt.Sprintf("$%.2f", v)
+}
+
// TestEconomicsPageHonestUnmetered: a box with a metrics day rollup but no
// usage/edge day ever written must render "unmetered" for that box's egress
// — never a blank or a fabricated zero. See BoxEconomics.EgressMetered and
diff --git a/internal/console/server.go b/internal/console/server.go
index 1b322b615..cb90ed8d2 100644
--- a/internal/console/server.go
+++ b/internal/console/server.go
@@ -202,6 +202,15 @@ var templates = template.Must(template.New("").Funcs(template.FuncMap{
},
"sub": func(a, b float64) float64 { return a - b },
"mul": func(a, b float64) float64 { return a * b },
+ // project is economics.Project (compute.go), exposed so the headline
+ // tiles' month-to-date figures can carry a "projected for the month"
+ // second line — the spec's "month-to-date with projection" promise. Only
+ // ever called from economics.html's tiles when Period=="mtd" and
+ // CoveredDays>0; the JS twin (economics.html's econ_maths_js define) is
+ // the exact transliteration a knob change re-runs.
+ "project": func(mtd float64, coveredDays, periodDays int) float64 {
+ return economics.Project(mtd, coveredDays, periodDays)
+ },
// marginOf is economics.PriceAccount's margin formula
// ((revenue-cost)/revenue, 0 when revenue is 0), applied to the
// dashboard's period TOTALS rather than one account — the "achieved
diff --git a/internal/console/templates/economics.html b/internal/console/templates/economics.html
index 991ca8f4a..b5063445a 100644
--- a/internal/console/templates/economics.html
+++ b/internal/console/templates/economics.html
@@ -3,7 +3,7 @@
is already priced under the active scenario (economicsview.go) — the
ONE inline <script> at the foot only RE-computes in place when an
operator tweaks a knob, transliterating
- internal/economics/compute.go's AllocateBox/PriceAccount
+ internal/economics/compute.go's AllocateBox/PriceAccount/Project
function-for-function (same names, JS-cased) so drift between the two
is reviewable at a glance. With JS off, every number above stays
exactly what the server rendered, and the knob drawer's only working
@@ -50,21 +50,33 @@
{{$cost := add (sumAcctCost .Economics.Accounts) (sumBoxIdle .Economics.Boxes) (overheadUSD .Economics.Scenario .Economics.PeriodDays)}}
{{$profit := sub $revenue $cost}}
{{$margin := marginOf $revenue $cost}}
+{{/* $showProjection: the spec's "month-to-date with projection" promise —
+ only meaningful on "mtd" (the other periods are already whole,
+ closed ranges with nothing left to project), and only once at least
+ one day of this month has actually been swept (economics.Project
+ itself returns 0 for CoveredDays<=0, which would render a false,
+ confident-looking "$0.00 for the month" rather than simply omitting
+ the line). The margin tile has no projected line — a ratio doesn't
+ project the way a dollar total does. */}}
+{{$showProjection := and (eq .Economics.Period "mtd") (gt .Economics.CoveredDays 0)}}
<section class="tiles" aria-label="Headline economics">
<div class="tile">
<h2>Would-be revenue</h2>
<p class="tile-value" id="tile-revenue">{{usd $revenue}}</p>
<p class="tile-detail">{{.Economics.CoveredDays}}/{{.Economics.PeriodDays}} days metered</p>
+ {{if $showProjection}}<p class="tile-detail" id="tile-revenue-projected">projected {{usd (project $revenue .Economics.CoveredDays .Economics.PeriodDays)}} for the month</p>{{end}}
</div>
<div class="tile">
<h2>Cost</h2>
<p class="tile-value" id="tile-cost">{{usd $cost}}</p>
<p class="tile-detail">{{.Economics.CoveredDays}}/{{.Economics.PeriodDays}} days metered</p>
+ {{if $showProjection}}<p class="tile-detail" id="tile-cost-projected">projected {{usd (project $cost .Economics.CoveredDays .Economics.PeriodDays)}} for the month</p>{{end}}
</div>
<div class="tile">
<h2>Profit</h2>
<p class="tile-value" id="tile-profit">{{usd $profit}}</p>
<p class="tile-detail">{{.Economics.CoveredDays}}/{{.Economics.PeriodDays}} days metered</p>
+ {{if $showProjection}}<p class="tile-detail" id="tile-profit-projected">projected {{usd (project $profit .Economics.CoveredDays .Economics.PeriodDays)}} for the month</p>{{end}}
</div>
<div class="tile">
<h2>Achieved margin</h2>
@@ -254,6 +266,19 @@
setText("tile-profit", usd(profitTotal));
setText("tile-margin", pctf(marginTotal));
+ {{/* The three lines below are wrapped in a Go template (not just
+ JS) "mtd" check, deliberately: setText's own `if (el)` guard would
+ already no-op on a period that never rendered the element, but
+ leaving the literal word live in every period's JS source would
+ defeat the honesty rule (a non-mtd page must not carry the word
+ at all, not merely fail to display it) — see
+ TestEconomicsPageMTDShowsProjection. */}}
+ {{if eq .Economics.Period "mtd"}}
+ setText("tile-revenue-projected", "projected " + usd(project(revenueTotal, payload.coveredDays, payload.periodDays)) + " for the month");
+ setText("tile-cost-projected", "projected " + usd(project(costTotal, payload.coveredDays, payload.periodDays)) + " for the month");
+ setText("tile-profit-projected", "projected " + usd(project(profitTotal, payload.coveredDays, payload.periodDays)) + " for the month");
+ {{end}}
+
document.querySelectorAll("[data-account]").forEach(function (row) {
var a = accounts[row.getAttribute("data-account")];
if (!a) { return; }
@@ -496,6 +521,15 @@
return neg ? "-" + s : s;
}
function pctf(v) { return Math.round(v * 100) + "%"; }
+ // project is economics.Project (compute.go) transliterated: scale a
+ // month-to-date figure to covered/period days, 0 when nothing has been
+ // covered yet (never a divide-by-zero NaN). recompute() only calls this
+ // on the one period whose tiles carry a second, scaled-up line at all —
+ // see that call site's own template guard.
+ function project(mtd, covered, days) {
+ if (covered <= 0) { return 0; }
+ return mtd * days / covered;
+ }
function marginMultiple(margin) {
if (margin < 0 || margin >= 1) { return "—"; }
return (1 / (1 - margin)).toFixed(1) + "×";