// 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