carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1{{/* /fleet/economics/accounts (Task 14): every account, one row each,
2 searchable and cursor-paginated (econlistquery.go: econAccountQuery/
3 pageEconAccounts — instanceQuery/pageInstanceRows's shape, applied to
4 economics.AccountEconomics instead of an InstanceRow). Columns are
5 honest about what the census meters actually hold: app_months and
6 instance_hours are month-fraction and hour-sum accumulators (see
7 internal/usage.Meter*), never a literal "how many apps/instances right
8 now" count, so the headers below say "app-months"/"instance-hours"
9 rather than the design brief's shorthand "apps"/"instances" — the
10 dashboard-truth rule (CLAUDE.md) that a total must never claim a
11 precision the underlying meter doesn't have.
12
13 Recompute: this page shares Task 13's knob drawer (define
14 "econ_knob_drawer", economics.html) and its pure maths functions
15 (define "econ_maths_js", same file) rather than re-deriving either —
16 see economics.html's header comment for why splicing econ_maths_js
17 into THIS page's own <script> IIFE keeps one scope. This page's own
18 recompute() (below) is deliberately smaller than economics.html's: no
19 tiles, no box table, no model-vs-bill, no pool-tile update (that lives
20 only on /fleet/economics) — it only re-prices the account rows
21 currently in the DOM (this page's one table, one page of up to
22 50 accounts), via the same allocateBox/priceAccount over the FULL
23 payload.boxLoads (a row's allocated cost depends on every box it
24 touched, not just the rows on screen), plus the SAME tier-aware
25 view-layer override economics.html's recompute() applies (Task 5/
26 Task 6): payload.tiers is read, and a pool/try/""-tier row's revenue,
27 margin and free-tier chip are overridden to pooled AFTER priceAccount
28 runs, exactly matching buildEconomicsView's server-side override — a
29 knob tweak must never flip a pooled row back to priced. The meter
30 columns (CPU-h, GB-h, egress, storage) are raw readings, not
31 knob-derived, so they never change under recompute; neither does the
32 Tier column (Task 6) — an account's fleet-pool tier is an operator
33 assignment via the per-row form below, not something a scenario knob
34 can move. */}}
35{{define "economics_accounts"}}{{template "top" .}}
36<h1>Fleet economics: accounts</h1>
37{{if .Flash}}<p class="banner">{{.Flash}}</p>{{end}}
38{{if .FlashErr}}<p class="banner error">{{.FlashErr}}</p>{{end}}
39{{if .Economics.Warning}}<p class="banner error">{{icon "triangle-alert"}} {{.Economics.Warning}}</p>{{end}}
40{{if .Economics.ScenarioWarning}}<p class="banner error">{{icon "triangle-alert"}} {{.Economics.ScenarioWarning}}</p>{{end}}
41
42<p class="small">Generated {{fmtTime .Economics.GeneratedAt}} &middot; scenario <strong>{{.Economics.Scenario.Name}}</strong> ({{.Economics.Scenario.Mode}}) &middot; {{.Economics.CoveredDays}}/{{.Economics.PeriodDays}} days metered</p>
43
44{{/* Period nav: switching period keeps the operator's search/filter
45 (Q/Pos) — econPeriodURL — but drops any cursor, since a cursor resumes
46 an account-name position computed against ONE period's row order and
47 means nothing against another period's. */}}
48<nav class="small" aria-label="Period">
49 Period:
50 <a href="{{econPeriodURL .EconQuery "mtd"}}"{{if eq .Economics.Period "mtd"}} aria-current="page"{{end}}>Month to date</a> &middot;
51 <a href="{{econPeriodURL .EconQuery "last"}}"{{if eq .Economics.Period "last"}} aria-current="page"{{end}}>Last month</a> &middot;
52 <a href="{{econPeriodURL .EconQuery "7d"}}"{{if eq .Economics.Period "7d"}} aria-current="page"{{end}}>Last 7 days</a>
53</nav>
54<p class="small"><a href="/fleet/economics">&larr; Back to the dashboard</a></p>
55
56{{if or .EconAccounts.Rows .EconQuery.Q .EconQuery.Pos}}
57{{/* The hidden period field only needs to exist for a non-default period:
58 "mtd" is what an absent "period" param already means, matching
59 econLinkURL/econPeriodURL's own "omit when mtd" rule — so a search
60 submitted on the default period doesn't grow a redundant ?period=mtd
61 into every subsequent link. */}}
62<form method="get" class="listctl">
63 <input type="search" name="q" value="{{.EconQuery.Q}}" placeholder="Filter accounts" aria-label="Filter accounts">
64 {{if .EconQuery.Pos}}<input type="hidden" name="pos" value="{{.EconQuery.Pos}}">{{end}}
65 {{if ne .EconQuery.Period "mtd"}}<input type="hidden" name="period" value="{{.EconQuery.Period}}">{{end}}
66 <button>Filter</button>
67</form>
68<p class="small">
69 <a href="{{.EconAccounts.AllURL}}"{{if not .EconQuery.Pos}} aria-current="true"{{end}}>All</a>
70 {{range .EconAccounts.Links}}
71 &middot; <a href="{{.Href}}"{{if .Active}} aria-current="true"{{end}}>{{.Word}} {{.Count}}</a>
72 {{end}}
73</p>
74{{end}}
75
76{{if .EconAccounts.Rows}}
77<div class="tablewrap">
78<table>
79 <tr>
80 <th scope="col">Account</th>
81 <th scope="col">App-months</th>
82 <th scope="col">Instance-hours</th>
83 <th scope="col">CPU-h</th>
84 <th scope="col">GB-h</th>
85 <th scope="col">Egress GB</th>
86 <th scope="col" title="the part of Egress GB that left its home box for a region edge first — the inter-region leg is a platform cost, absorbed, in no account's bill">Backhaul GB</th>
87 <th scope="col">Storage GB-month</th>
88 <th scope="col">Cost</th>
89 <th scope="col">Would-be revenue</th>
90 <th scope="col" title="Pooled rows (free-tier position &quot;pooled&quot;) show an em-dash: their revenue is shared through the fleet pool, not individually priced, so a margin fraction has nothing to divide. Fleet-tier rows can legitimately show 100%: fleet boxes are customer-owned hardware, zero cost to the platform, by design.">Margin</th>
91 <th scope="col">Free tier</th>
92 {{/* Tier (Task 6): the operator's fleet-pool ASSIGNMENT for this
93 account (store.go's validAccountTiers), distinct from the Free
94 tier column's PRICED POSITION — "pooled" over there can come from
95 any of try/pool/"" here. */}}
96 <th scope="col">Tier</th>
97 </tr>
98 {{range .EconAccounts.Rows}}
99 {{$tier := index $.Economics.Tiers .Account}}
100 {{/* tierKnown (fix round, Task 6 review IMPORTANT 2): "index" above
101 can't distinguish "absent from the map entirely" (a usage-only
102 account with NO console/accounts-table row — e.g. a fleet-only
103 account whose owner never signed up) from "present with the
104 explicit "" tier" (a real console account nobody has tiered yet) —
105 both read $tier=="". Only the second case gets the tier-set form:
106 POSTing the first's sqid to /fleet/economics/tier can only ever
107 fail with "No such account." (handleEconomicsTierSave's
108 AccountBySqid lookup). */}}
109 {{$known := tierKnown $.Economics.Tiers .Account}}
110 <tr data-account="{{.Account}}">
111 {{/* Account-names batch: a known console name renders as the primary
112 text with the sqid muted+small beside it — the ".small" class
113 already IS the muted treatment (layout.html: font-size 0.9rem,
114 color var(--muted); there is no separate ".muted" class anywhere in
115 these templates, so this reuses ".small" rather than inventing
116 one). A usage-only ghost row (no console account row at all, so
117 .Name is "") or an explicitly empty name renders the bare sqid
118 alone, exactly as before this batch. The row's data-account
119 attribute above and every other field on this row (the tier form
120 below) still key off .Account, the sqid — display only. */}}
121 <td>{{if .Name}}{{.Name}} <span class="small">{{.Account}}</span>{{else}}{{.Account}}{{end}}</td>
122 <td>{{printf "%.2f" (index .Meters "app_months")}}</td>
123 <td>{{printf "%.2f" (index .Meters "instance_hours")}}</td>
124 <td>{{printf "%.2f" (index .Meters "cpu_core_hours")}}</td>
125 <td>{{printf "%.2f" (index .Meters "mem_gb_hours")}}</td>
126 <td>{{printf "%.2f" (index .Meters "egress_gb")}}</td>
127 <td>{{printf "%.2f" (index .Meters "backhaul_gb")}}</td>
128 <td>{{printf "%.2f" (index .Meters "storage_gb_month")}}</td>
129 <td class="js-acct-cost">{{usd .Econ.CostUSD}}</td>
130 <td class="js-acct-revenue">{{usd .Econ.RevenueUSD}}</td>
131 <td class="js-acct-margin">{{marginCell .Econ.MarginFrac .Econ.FreeTierPos}}</td>
132 <td class="js-acct-freetier"><span class="chip chip-freetier-{{.Econ.FreeTierPos}}">{{.Econ.FreeTierPos}}</span></td>
133 <td>
134 <span class="chip chip-tier-{{tierWord $tier}}">{{tierWord $tier}}</span>
135 {{if $known}}
136 {{/* Progressive: a plain <select> + submit button, no JS required —
137 the door (POST /fleet/economics/tier, handleEconomicsTierSave)
138 already exists (Task 4/5). Only rendered for a REAL console
139 account (see $known's doc comment above). */}}
140 <form method="post" action="/fleet/economics/tier">
141 <input type="hidden" name="csrf" value="{{$.CSRF}}">
142 <input type="hidden" name="account" value="{{.Account}}">
143 <select name="tier">
144 <option value=""{{if eq $tier ""}} selected{{end}}>unassigned</option>
145 <option value="try"{{if eq $tier "try"}} selected{{end}}>try</option>
146 <option value="pool"{{if eq $tier "pool"}} selected{{end}}>pool</option>
147 <option value="hosted"{{if eq $tier "hosted"}} selected{{end}}>hosted</option>
148 <option value="fleet"{{if eq $tier "fleet"}} selected{{end}}>fleet</option>
149 <option value="dedicated"{{if eq $tier "dedicated"}} selected{{end}}>dedicated</option>
150 </select>
151 <button type="submit">Set</button>
152 </form>
153 {{end}}
154 </td>
155 </tr>
156 {{end}}
157</table>
158</div>
159<p class="small">App-months and instance-hours are accumulators, not live counts —
160a fraction of a calendar month and a sum of hours run, the meters this platform
161actually keeps. CPU-h/GB-h are box-side compute meters; egress and storage are
162edge/S3 meters. Cost and would-be revenue are this account's own share plus its
163allocated box cost, under the active scenario.</p>
164{{/* MINOR 3 (fix round, Task 6 review): the Margin column's title=
165 tooltip is invisible on touch/keyboard — this legend carries the
166 same two notes visibly, for everyone. */}}
167<p class="small">Margin: pooled rows show an em-dash — their revenue is shared through the fleet pool, not individually priced. Fleet-tier rows can legitimately show 100% margin — fleet boxes are customer-owned hardware, zero cost to the platform, by design.</p>
168{{if .EconAccounts.NextURL}}<p class="small"><a href="{{.EconAccounts.NextURL}}">Next {{len .EconAccounts.Rows}} shown of {{.EconAccounts.Total}} &rarr;</a></p>{{end}}
169{{else if or .EconQuery.Q .EconQuery.Pos}}
170<p>No accounts match.</p>
171{{else}}
172<p class="small">No accounts to price this period.</p>
173{{end}}
174
175<div id="econ-live">
176{{template "econ_knob_drawer" .}}
177</div>
178
179<script type="application/json" id="econ-data">{{.Economics.DataJSON}}</script>
180<script>
181(function () {
182 "use strict";
183 // Same source-order contract as economics.html's script: the payload
184 // <script> tag above is parsed and in the DOM before this one runs.
185 var dataEl = document.getElementById("econ-data");
186 if (!dataEl) { return; }
187 var payload;
188 try { payload = JSON.parse(dataEl.textContent); } catch (e) { return; }
189
190 {{template "econ_maths_js" .}}
191
192 // recompute re-prices only the account rows THIS page rendered (its one
193 // page of up to econAccountPageSize accounts) — allocateBox still runs
194 // over the FULL payload.boxLoads/totalAllocated (a row's allocated cost
195 // depends on every box it touched, not just the rows on screen), but the
196 // DOM write-back is scoped to "[data-account]" rows actually present,
197 // exactly like economics.html's account-row loop, minus the tiles/box
198 // table/model-vs-bill sections this page doesn't render.
199 function recompute() {
200 var sc = readScenarioFromDOM(payload.scenario);
201 setText("margin-multiple", marginMultiple(sc.margin));
202
203 var totalAllocated = {};
204 Object.keys(payload.boxLoads).forEach(function (box) {
205 var alloc = allocateBox(sc.rate_card, sc.allocation, payload.boxLoads[box]);
206 Object.keys(alloc.perAccount).forEach(function (acct) {
207 totalAllocated[acct] = (totalAllocated[acct] || 0) + alloc.perAccount[acct];
208 });
209 });
210
211 var tiers = payload.tiers || {};
212 document.querySelectorAll("[data-account]").forEach(function (row) {
213 var acct = row.getAttribute("data-account");
214 var a = priceAccount(sc, acct, payload.meters[acct] || {}, totalAllocated[acct] || 0);
215 // Tier-aware view-layer override (economics-v2 Task 5/Task 6), the
216 // SAME override economics.html's recompute() applies — see this
217 // page's header comment.
218 if (isPoolTier(tierOf(tiers, acct))) {
219 a.revenueUSD = 0;
220 a.marginFrac = 0;
221 a.freeTierPos = "pooled";
222 }
223 setCellText(row, ".js-acct-cost", usd(a.costUSD));
224 setCellText(row, ".js-acct-revenue", usd(a.revenueUSD));
225 setCellText(row, ".js-acct-margin", marginCell(a.marginFrac, a.freeTierPos));
226 var chip = row.querySelector(".js-acct-freetier .chip");
227 if (chip) {
228 chip.textContent = a.freeTierPos;
229 chip.className = "chip chip-freetier-" + a.freeTierPos;
230 }
231 });
232
233 var field = document.getElementById("scenario-json-field");
234 if (field) { field.value = JSON.stringify(sc); }
235 }
236
237 var live = document.getElementById("econ-live");
238 if (live) {
239 live.addEventListener("input", recompute);
240 live.addEventListener("change", recompute);
241 }
242})();
243</script>
244{{template "bottom" .}}{{end}}
245