feat(status): port the four tag-derived notices from Go's spawn status (#56) - #64
Merged
Conversation
…us` (#56) `spawn status` answered "what did you configure" and nothing else. Go appends four notices after its remote `spored status` output (cmd/status.go:130-134); none were ported, and every one of them is pure tag reading that a browser can already do. The headline gap: spored writes `spawn:dns-status` + `spawn:dns-error` when registration fails, specifically so the failure isn't buried in the instance's journal (spawn#435) — and nothing in spawn-ts ever read them. A portal user got an FQDN that never resolves and no explanation, while the diagnostic sat in a tag DescribeInstances had already returned. New pure `src/core/notices.ts` returning structured `{ kind, level, text, detail }` rather than pre-formatted strings, so the CLI, the dashboard and the portal render one source three ways: - lifecycleProtection() — who enforces the deadline, when it falls, and the worst-case *compute* cost to it. The counterpart to accumulatedCost(), which reports only what's been spent; the ceiling is the number that tells a user whether to worry. - dnsNotice() — the failure, with spored's own reason. - sporedUpgrade() + compareSemver() — a port of libs/update's comparison, so the two tools never disagree about whether an upgrade exists. - elasticIpNotice() — an EIP on a *stopped* instance keeps billing (~$3.60/mo) precisely because nothing is using it, which is exactly when the user believes they've stopped paying. Pure; the lookup is src/aws/eip.ts. Absence is never smoothed into reassurance, per #63. Each notice has an input it can't do without, and a missing input omits the notice rather than answering optimistically: no dns-status tag does not mean "registered", and no supplied latest version does not mean "up to date". That principle forces one deliberate divergence from Go. GetInstanceElasticIP (pkg/aws/cleanup.go:219) returns nil,nil on any API error, so a missing ec2:DescribeAddresses permission is indistinguishable from a clean bill of health — in the one check whose entire job is catching an unnoticed charge. Here a failed lookup is its own warn notice naming the error. Two other honesty constraints carried over verbatim and asserted in tests: the out-of-band reaper is described as a backstop "if deployed" (it runs in the infra account and isn't visible from the launch account), and the cost figure keeps its "compute only" label since it excludes EBS and network. Wiring: EC2Provider.lookupElasticIp delegates to the shared lookup with the credentials it already holds, and the CLI falls back to it when the ShellCtx supplies no override — without that the notice would exist but never fire in the real app, since terminal.ts builds a bare ShellCtx. The dashboard renders the two notices its meters don't already cover, escaped (spawn:dns-error is whatever the DNS endpoint returned, and it lands in innerHTML). Also fixes an adjacent line found by driving the built CLI rather than trusting vitest: humanRemaining() already returns "expired", so `status` on an overdue instance read "ttl: 4h — expired left (terminates)". Not ported, and deliberately: the remote `spored status` output these notices are appended *to* (needs a shell on the box), and Go's fallback of parsing the spored version out of that output. Verified: each new guard was checked against the absent/Go-equivalent behaviour (5 mutations → 11 failures; the XSS guard → 1 more) before being kept. Driven end to end through the built CLI and through Chromium, where the notice CSS was confirmed to actually land. 451 → 503 tests; typecheck, build and build:lib all clean. Refs #56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #56.
spawn statusanswered "what did you configure" and nothing else. Go appends four notices after its remotespored statusoutput (cmd/status.go:130-134); none were ported, and every one of them is pure tag reading a browser can already do.The headline gap. spored writes
spawn:dns-status+spawn:dns-errorwhen registration fails, specifically so the failure isn't buried in the instance's journal (spawn#435) — and nothing in spawn-ts ever read them. A portal user got an FQDN that never resolves and no explanation, while the diagnostic sat in a tagDescribeInstanceshad already returned.What's here
A pure
src/core/notices.tsreturning structured{ kind, level, text, detail }rather than pre-formatted strings, so the CLI, the dashboard and the portal render one source three ways.lifecycle-protectiondnselastic-ipspored-upgradelifecycleProtectionis the counterpart toaccumulatedCost(), which reports only what's been spent. The ceiling is the number that tells a user whether to worry.elasticIpNoticematters most on a stopped instance: stopping pauses compute billing, so the user believes they've stopped paying — and an unassociated EIP is charged precisely because nothing is using it.Absence is never smoothed into reassurance
Per #63, each notice has an input it can't do without, and a missing input omits the notice rather than answering optimistically:
spawn:dns-status→ no notice. It does not mean registration succeeded; an older spored simply never wrote the tag.latestSporedVersion→ no notice. It does not mean spored is current. The value is passed in rather than fetched, because Go's release check hits the GitHub API and that's the embedder's call.warnnotice naming the error.That last one is a deliberate divergence from Go.
GetInstanceElasticIP(pkg/aws/cleanup.go:219) returnsnil, nilon any API error, so a missingec2:DescribeAddressespermission is indistinguishable from a clean bill of health — in the one check whose entire job is catching an unnoticed charge.Two honesty constraints carried over verbatim and asserted in tests: the out-of-band reaper is described as a backstop "if deployed" (it runs in the infra account and isn't authoritatively visible from the launch account), and the cost figure keeps its "compute only" label since it excludes EBS and network. It's skipped entirely when the instance has no recorded price rather than shown as $0.00.
Wiring
EC2Provider.lookupElasticIpdelegates to the shared lookup with the credentials it already holds, and the CLI falls back to it when theShellCtxsupplies no override. Without that the notice would exist but never fire in the real app —terminal.tsbuilds a bareShellCtx. New permission:ec2:DescribeAddresses(optional; its absence is reported, not fatal).The dashboard renders the two notices its meters don't already cover, escaped —
spawn:dns-erroris whatever the DNS endpoint returned, and it lands ininnerHTML.Also
Driving the built CLI (not the suite) surfaced an adjacent line:
humanRemaining()already returns"expired", sostatuson an overdue instance readttl: 4h — expired left (terminates).Not ported, deliberately
The remote
spored statusoutput these notices are appended to (needs a shell on the box), and Go's fallback of parsing the spored version out of that output.compareSemveris a faithful port oflibs/update's comparison, so the two tools never disagree about whether an upgrade exists.Verification
npm test,npm run build,npm run build:liball clean.notices.tsto the absent/Go-equivalent behaviour (EIP error collapse, no past-due branch, no cost ceiling, no NaN guard, dropped DNS detail) → 11 failures acrossnotices.test.tsandcommands.test.ts. Removing the dashboard'sescapeHtml→ 1 more..notice.warnred-on-tint with a border,.notice.infovisibly subordinate).