// The console's HTTP server: hand-written HTML from Go templates, no // JavaScript, no build step, no external resources. Auth model: OAuth at // Keymail is the ONE-TIME sign-up ceremony; returning sign-ins are a // single-use confirmation link the console delivers into the user's Keymail // Apps inbox with its stored grant. package console import ( "context" "crypto/ed25519" "crypto/subtle" "crypto/x509" "embed" "encoding/json" "errors" "fmt" "html/template" "log" "math" mrand "math/rand/v2" "net" "net/http" netmail "net/mail" "net/url" "os" "regexp" "slices" "sort" "strings" "sync" "sync/atomic" "time" "github.com/aws/aws-sdk-go-v2/service/costexplorer" "github.com/carlosframework/platform/internal/appconfig" "github.com/carlosframework/platform/internal/applog" "github.com/carlosframework/platform/internal/boxid" "github.com/carlosframework/platform/internal/dbreads" "github.com/carlosframework/platform/internal/dnsrec" "github.com/carlosframework/platform/internal/domains" "github.com/carlosframework/platform/internal/economics" "github.com/carlosframework/platform/internal/edgesfile" "github.com/carlosframework/platform/internal/geo" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/infraview" "github.com/carlosframework/platform/internal/lease" "github.com/carlosframework/platform/internal/mail" "github.com/carlosframework/platform/internal/moves" "github.com/carlosframework/platform/internal/objstore" "github.com/carlosframework/platform/internal/plans" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/publicrelease" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/status" "github.com/carlosframework/platform/internal/svcrole" "github.com/carlosframework/platform/internal/telemetry" "github.com/carlosframework/platform/internal/usage" "github.com/carlosframework/platform/internal/version" ) //go:embed templates/*.html var tmplFS embed.FS var templates = template.Must(template.New("").Funcs(template.FuncMap{ "fmtTime": func(t time.Time) string { return t.UTC().Format("2006-01-02 15:04 UTC") }, // until renders a future horizon the way the cards speak: "~4h", never a // timestamp — the safety-delay strip's "goes live ~4h" and Task 6's // summary strip both read it this way. "until": untilText, // ago renders a past instant the way a person scanning a dashboard reads // one — "2h4m ago" — at humanAge's deliberately coarse resolution. The // absolute stamp stays available where precision matters (title attrs, // the Activity tab); a zero time is unknown, so it dashes out. "ago": func(t time.Time) string { if t.IsZero() { return "—" } return humanAge(time.Since(t)) + " ago" }, // trashCooloffDays is the deletion policy's restore window, in the unit // the Settings page speaks. One source: release.TrashCooloff. "trashCooloffDays": func() int { return int(release.TrashCooloff.Hours() / 24) }, // temporaryCooloffDays is a temporary app's restore window, in days: the // delete confirmation shows it for an app with an expiry (temporary static // sites spec §5, "the delete confirmation in the browser"). "temporaryCooloffDays": func() int { return int(release.TemporaryCooloff.Hours() / 24) }, // The fleet template's em-dash helpers: fleet.json's null-propagation // rule (internal/status/fleet.go) means these *string/*int/*bool fields // are unknown, not zero, and unknown must render as a titled em-dash — // never a blank cell or a guessed value — same rule index.html's JS // enforces client-side (see asText/asBool there). "strOrDash": func(v *string) string { if v == nil { return "—" } return *v }, "intOrDash": func(v *int) string { if v == nil { return "—" } return fmt.Sprint(*v) }, "boolWord": func(v *bool, whenTrue, whenFalse string) string { if v == nil { return "—" } if *v { return whenTrue } return whenFalse }, "emptyOrDash": func(s string) string { if s == "" { return "—" } return s }, // pct renders a fraction (0..1, as CPU/Mem/Disk readings and unit CPU // fractions are stored — internal/metrics.HostSample/UnitSample) as a // whole-percent string; nil means no reading, never zero, so it renders // the same em-dash every other "unknown" field on this console does. "pct": func(v *float64) string { if v == nil { return "—" } return fmt.Sprintf("%.0f%%", *v*100) }, // alertVal renders one AlertRow's value in ITS OWN rule's unit — // see alertValueText. LoadMetrics only ever returns firing rules, so an // alert row always carries a value; there is no nil case to dash out. "alertVal": alertValueText, // healthSlug: the Running tab's health-filter links and the hidden // carried-filter field both need a health word's URL form (listquery.go). "healthSlug": healthSlug, "healthTone": healthTone, "instanceRowPage": func(p page, row InstanceRow) any { return struct { Page page Row InstanceRow }{p, row} }, // logLines: the Logs tab's window folded into the terminal pane's lines // (logsview.go). The padding is computed across the whole window, so it // has to be one call over all the records, not one per row. "logLines": logLines, // sortedKV: the Analytics tab's three top-key tables (analyticsview.go). // A Go template ranges a map in key order, which would sort a top-pages // table alphabetically — the one order that hides which page is the // popular one. This puts the biggest first and leaves the overflow // bucket last, wherever its count happens to land. "sortedKV": sortedKV, // geoCountries and geoAttributions: the Analytics tab's Countries table // and the credit its sources require (geoip spec §9, §13). "geoCountries": geoCountries, "geoLabel": geoLabel, "geoAttributions": geoAttributions, // bytesOrDash renders a byte count in the smallest unit that keeps it // readable; nil (no reading, from a unit that reported CPU but not // memory/IO that tick) is "—", never a fabricated 0 B. "bytesOrDash": func(v *int64) string { if v == nil { return "—" } b := float64(*v) switch { case b >= 1<<30: return fmt.Sprintf("%.1f GB", b/(1<<30)) case b >= 1<<20: return fmt.Sprintf("%.1f MB", b/(1<<20)) case b >= 1<<10: return fmt.Sprintf("%.1f KB", b/(1<<10)) default: return fmt.Sprintf("%d B", *v) } }, // loadOrDash renders a 1-minute load average: an absolute number of // runnable tasks, not a fraction, so pct (which multiplies by 100 and // stamps a %) would be a lie about the same *float64. nil is the same // em-dash every unknown on this console gets. "loadOrDash": func(v *float64) string { if v == nil { return "—" } return fmt.Sprintf("%.2f", *v) }, // mapGet reads one named field out of Fleet.Instances — a map so a // template range would iterate it key-sorted, alphabetically, which // scrambles the static/resident/hibernating/awake/asleep order the // public page and the design spec both fix; indexing by name keeps that // order explicit in the template instead. "mapGet": func(m map[string]*int, k string) *int { return m[k] }, // icon inlines one vendored Lucide SVG by slug — see icons.go for the // set, the licence, and why template.HTML is safe there and nowhere // else. Chrome is drawn; emoji stay content. "icon": icon, // join renders fleetPageBox.Tags back into the tag form's editable // text input, comma-separated — the exact grammar SetFleetBoxTags // expects back, so an owner opening the form sees the same string they'd // type to leave it unchanged. "join": strings.Join, // fleetName reads the fleet out of a placement target ("fleet/pi" -> "pi") // so the Placement section can NAME the fleet and link to its page rather // than printing the raw Region value at a customer. A target that does not // parse yields "", which the template's own {{if}} already handles as // "not placed" — the same reading appPlacement itself takes. "fleetName": func(target string) string { f, _ := registry.FleetPlacement(target); return f }, // ---- /fleet/economics (Task 13): server-side arithmetic mirroring // internal/economics/compute.go, so the page's initial render needs no // JS to show real numbers — see economics.html's inline script for the // live-recompute twin of every function below (same names, JS-cased). // usd renders a signed dollar figure — negative (a box or account // running at a loss under the active scenario) as "-$N.NN", never a // bare minus glued to the sign the reader expects at the front. "usd": func(v float64) string { if math.IsNaN(v) || math.IsInf(v, 0) { return "—" } if v < 0 { return fmt.Sprintf("-$%.2f", -v) } return fmt.Sprintf("$%.2f", v) }, // pctf is pct (above) for a plain, never-nil float64 — margin and // utilization fractions (economics.AccountEconomics.MarginFrac, // BoxEconomics.Utilization) are always populated, never a *float64 // "no reading yet" case pct exists to dash out. "pctf": func(v float64) string { return fmt.Sprintf("%.0f%%", v*100) }, // marginMultiple renders cost-plus's margin knob the way an operator // thinks about markup, not the accounting fraction it's stored as: // multiple = 1/(1-margin), so the default 0.8 reads "5.0× cost" per the // brief. Out of Validate's legal [0,1) range is "—", never a divide by // zero or a negative multiple. "marginMultiple": func(margin float64) string { if margin < 0 || margin >= 1 { return "—" } return fmt.Sprintf("%.1f×", 1/(1-margin)) }, // marginCell renders MarginFrac as pctf normally, but an em-dash for a // pooled row (FreeTierPos=="pooled") — Task 6's carried ruling: a // pool/try/""-tier account's revenue is pooled, not individually priced // (buildEconomicsView's tier override), so its MarginFrac is genuinely // 0 — but "0%" reads as an account LOSING money, not one that simply // isn't priced on its own. The JS twin (econ_maths_js's marginCell) // mirrors this exactly. "marginCell": func(frac float64, pos string) string { if pos == "pooled" { return "—" } return fmt.Sprintf("%.0f%%", frac*100) }, // tierWord renders a fleet-pool tier (store.go's validAccountTiers) for // display: "" (never assigned) reads "unassigned" rather than a blank // chip label; every other value (try/pool/hosted/fleet/dedicated) // passes through unchanged. Feeds both a tier chip's visible text and // its "chip-tier-" class (economics_accounts.html, Task 6). "tierWord": func(tier string) string { if tier == "" { return "unassigned" } return tier }, // tierKnown reports whether acct has a real console account row at all // (present as a key in EconomicsView.Tiers, via the comma-ok form — // html/template's "index" can't distinguish absent from an explicit "" // tier, which is exactly the distinction this exists for). A usage-only // account (fleet or edge traffic with no matching accounts-table row — // e.g. a fleet-only account whose owner never signed up) is absent from // the map entirely: economics_accounts.html renders its tier chip but // WITHOUT the per-row tier-set form, since POSTing that form's account // sqid to /fleet/economics/tier can only ever fail with "No such // account." (handleEconomicsTierSave's AccountBySqid lookup) — fix // round, Task 6 review, IMPORTANT 2. "tierKnown": func(tiers map[string]string, acct string) bool { _, ok := tiers[acct] return ok }, "add": func(vs ...float64) float64 { var sum float64 for _, v := range vs { sum += v } return sum }, "sub": func(a, b float64) float64 { return a - b }, "mul": func(a, b float64) float64 { return a * b }, "div": func(a, b float64) float64 { if b == 0 { return 0 } 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 // margin" headline tile. "marginOf": func(revenue, cost float64) float64 { if revenue <= 0 { return 0 } return (revenue - cost) / revenue }, // sumAcctCost/sumAcctRevenue/sumBoxIdle fold EconomicsView.Accounts / // .Boxes into the headline tiles' period totals: total cost is every // account's allocated+metered cost, PLUS every box's unallocated idle // headroom (never smeared across tenants — economics.AllocateBox's own // doc comment), PLUS fixed overheads (overheadUSD below); total revenue // is simply the sum of what every account would owe. "sumAcctCost": func(accts []economics.AccountEconomics) float64 { var sum float64 for _, a := range accts { sum += a.CostUSD } return sum }, "sumAcctRevenue": func(accts []economics.AccountEconomics) float64 { var sum float64 for _, a := range accts { sum += a.RevenueUSD } return sum }, "sumBoxIdle": func(boxes []BoxEconomics) float64 { var sum float64 for _, b := range boxes { sum += b.IdleUSD } return sum }, // overheadUSD prorates the rate card's fixed monthly LineItems // (domains, DNS, ...) over the period being viewed, by the same // hours-over-HoursPerMonth proration economics.AllocateBox uses for a // box's EBS line — periodDays rather than CoveredDays, deliberately: // overhead is a standing monthly bill that runs whether or not usage // was fully sampled, so it prorates by calendar days in the period, not // by how much of it the sweep actually covered. "overheadUSD": func(sc economics.Scenario, periodDays int) float64 { var monthly float64 for _, li := range sc.RateCard.Overheads { monthly += li.USDMonth } return monthly * float64(periodDays) * 24 / economics.HoursPerMonth }, // sumMeter totals one named meter (usage.Meter* — "egress_gb", // "requests_1k", "storage_gb_month", ...) across every account in // EconomicsView.Meters, for the model-side breakdown's S3/egress lines. "sumMeter": func(meters map[string]map[string]float64, meter string) float64 { var sum float64 for _, mm := range meters { sum += mm[meter] } return sum }, // sumBoxesEC2/sumBoxesEBS split each box's total cost (BoxEconomics // carries the sum, not the two components) back into the rate card's // two box-cost lines for the model-side breakdown, using the same // box-then-default spec lookup economics.AllocateBox performs. "sumBoxesEC2": func(boxes []BoxEconomics, rc economics.RateCard) float64 { var sum float64 for _, b := range boxes { spec, ok := rc.Boxes[b.Box] if !ok { spec = rc.Boxes[""] } sum += spec.HourUSD * b.Hours } return sum }, "sumBoxesEBS": func(boxes []BoxEconomics, rc economics.RateCard) float64 { var sum float64 for _, b := range boxes { spec, ok := rc.Boxes[b.Box] if !ok { spec = rc.Boxes[""] } sum += spec.EBSGB * rc.EBSGBMonthUSD * (b.Hours / economics.HoursPerMonth) } return sum }, // periodMonth/billForMonth resolve which of EconomicsView.Bills (this // month + last, always both if present regardless of period — // loadRecentBills's doc comment) corresponds to the period on screen, // so the breakdown reconciles against the RIGHT bill: "last" looks at // last calendar month's, "mtd"/"7d" both look at this one. "periodMonth": func(period string) string { now := time.Now().UTC() if period == "last" { firstOfThisMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC) return usage.MonthOf(firstOfThisMonth.AddDate(0, -1, 0)) } return usage.MonthOf(now) }, "billForMonth": func(bills []economics.Bill, month string) *economics.Bill { for i := range bills { if bills[i].Month == month { return &bills[i] } } return nil }, // billServiceChecked is the "counts as CARLOS" default rule, ONE place // for both the checkbox render and the delta sum below: an EMPTY // Scenario.BillServices counts every service (an old scenario, or one // nobody has ever unchecked anything on), a NON-empty one counts only // what's named. The inline JS mirrors this exact rule (billServiceChecked) // when a knob or checkbox changes. "billServiceChecked": func(billServices []string, service string) bool { if len(billServices) == 0 { return true } for _, s := range billServices { if s == service { return true } } return false }, // sumBillServicesChecked totals a bill's Services map, restricted to the // ones billServiceChecked keeps — the breakdown's bill-side total. "sumBillServicesChecked": func(services map[string]float64, billServices []string) float64 { var sum float64 for name, v := range services { if len(billServices) == 0 { sum += v continue } for _, s := range billServices { if s == name { sum += v break } } } return sum }, // scenarioJSON marshals one economics.Scenario for the save-scenario // form's hidden field — the value the door at /fleet/economics/scenarios // parses back (handleEconomicsScenarioSave). html/template still // attribute-escapes this like any other string substitution; that's // correct and wanted here (a form value), unlike DataJSON's