Skip to content

fix(wave-d): dead routes, tier-rank inversion, invoice + limit drift#96

Merged
mastermanas805 merged 2 commits into
mainfrom
fix/bugbash-2026-05-18-wave-d
May 19, 2026
Merged

fix(wave-d): dead routes, tier-rank inversion, invoice + limit drift#96
mastermanas805 merged 2 commits into
mainfrom
fix/bugbash-2026-05-18-wave-d

Conversation

@mastermanas805
Copy link
Copy Markdown
Member

Fixes the Wave-D frontend findings from BUGHUNT-REPORT-2026-05-18.md.

Fixes

D1 — /get-token dead route (P1-W5-18, L-02)
PublicShell.tsx nav CTA Get token → pointed at /get-token, a route that doesn't exist — the SPA catch-all silently redirected the funnel's headline button to /, breaking it on ~11 PublicShell pages. Repointed at /login (the same route MarketingPage's "Get a token" CTA already uses; confirmed registered in App.tsx).

D2 — /security footer dead link (P2-W5-02)
Footer "Security" link pointed at /security (no route → 404 → catch-all to /). Repointed at /docs/public/security.md, the real doc (public/docs/public/security.md exists), matching how ChangelogPage links /docs/public/dpa.md.

D3 — frontend TIER_RANK inverted (P1-W4-10)
ChangePlanModal and TierChangeModal each had a private TIER_RANK with the inverted order growth:4, pro:5 — the admin console showed "DEMOTE" for a pro→growth upgrade. Added one canonical exported TIER_RANK (pro:4, growth:5) in src/api/index.ts, aligned with the backend common/plans/rank.go; both modals now import it so they can't re-diverge. Added a unit test (index.test.ts) pinning the ladder ordering.

D4 — invoice shape mismatch (P1-W4-09)
listInvoices did a blind cast. The API emits the Razorpay wire shape {id, amount, currency, status, date, pdf_url} but the Invoice type expected Stripe-style {period_start, period_end, plan, amount_cents} → every row rendered Invalid Date / $NaN / blank tier. Confirmed against billing.go::ListInvoicesAPI and the Razorpay Invoice struct: amount is already in the currency's smallest unit (paise/cents) so amount → amount_cents is a direct copy (NO ×100); date is a single charge timestamp. Added an explicit mapInvoice mapper. period_start/period_end/plan are not on the wire and stay optional — BillingPage now renders the charge date and only shows a tier pill when one is actually present, never fabricated.

D5 — BillingPage LIMITS had no growth row (P1-W2-06)
A growth-tier team fell through to LIMITS.hobby → red over-quota bars on a plan they were within. Added the growth row with plans.yaml values (postgres 20480MB, redis 1024MB, mongodb/webhooks unlimited, 5 deployments, 10 seats).

D6 — OverviewPage stale tier table + MB/MiB mismatch (P2-W2-17/18)
Dropped the hardcoded TIER_LIMIT_GB table (drifted from plans.yaml) in favour of summing the server-supplied per-resource storage_limit_bytes. Fixed the unit mismatch — the storage tile mixed a decimal-MB numerator with a binary-MiB denominator; all storage math now uses one binary base (GiB tile, MiB usage bars). MetricsPanel.formatBytes relabelled KB/MB/GB → KiB/MiB/GiB to match its /1024 divisors.

Gate

  • npx tsc --noEmit — clean
  • npx vite build + prerender.mjs — clean (120 static files)
  • npx vitest run633 passed, 3 skipped (35 test files); updated the now-stale listInvoices shape tests + FIXTURE_INVOICES, added TIER_RANK pinning tests

🤖 Generated with Claude Code

… drift

Wave-D frontend findings from BUGHUNT-REPORT-2026-05-18.

D1 (P1-W5-18, L-02): the nav "Get token →" CTA pointed at /get-token, a
route that doesn't exist — the SPA catch-all silently redirected the
conversion funnel's headline button to /. Repointed at /login, the same
route MarketingPage's "Get a token" CTA already uses.

D2 (P2-W5-02): the footer "Security" link pointed at /security (no such
route → 404 → catch-all to /). Repointed at /docs/public/security.md, the
real security doc, matching how ChangelogPage links /docs/public/dpa.md.

D3 (P1-W4-10): ChangePlanModal and TierChangeModal each carried a private
TIER_RANK with the inverted order growth:4, pro:5 — the admin console
showed "DEMOTE" for a pro→growth upgrade. Added one canonical TIER_RANK
table (pro:4, growth:5) to src/api/index.ts, aligned with the backend's
common/plans/rank.go; both modals now import it. Added a unit test pinning
the ladder ordering.

D4 (P1-W4-09): listInvoices did a blind cast of the API response. The
invoices endpoint emits the Razorpay wire shape {id,amount,currency,
status,date,pdf_url} but the Invoice type expected Stripe-style
{period_start,period_end,plan,amount_cents}, so every row rendered
"Invalid Date"/"$NaN"/blank tier. Added an explicit mapInvoice mapper:
amount→amount_cents (direct copy — already smallest-unit, no ×100),
date→issued_at. period_start/period_end/plan are not on the wire and stay
optional; BillingPage renders only what is present.

D5 (P1-W2-06): BillingPage LIMITS had no growth row, so a growth-tier team
fell through to LIMITS.hobby and saw red over-quota bars on a plan they
were within. Added the growth row with api/plans.yaml values.

D6 (P2-W2-17/18): OverviewPage's hardcoded TIER_LIMIT_GB table drifted
from plans.yaml, and the storage tile mixed a decimal-MB numerator with a
binary-MiB denominator. Dropped TIER_LIMIT_GB in favour of summing the
server-supplied per-resource storage_limit_bytes; all storage math now
uses one binary base. MetricsPanel's formatBytes relabelled KB/MB/GB →
KiB/MiB/GiB to match its /1024 divisors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mastermanas805 mastermanas805 merged commit 1a7dab9 into main May 19, 2026
2 checks passed
@mastermanas805 mastermanas805 deleted the fix/bugbash-2026-05-18-wave-d branch May 19, 2026 03:10
mastermanas805 added a commit that referenced this pull request May 19, 2026
)

BugBash 2026-05-18 P2 remediation scoped to instanode-web. Off origin/main,
no overlap with PR #96 (invoice mapper / TIER_RANK / MetricsPanel MiB /
BillingPage growth row / OverviewPage tier limit / dead-route fixes).

API layer (src/api/index.ts, sseStream.ts):
- P2-19: mapBillingState no longer infers razorpay_configured from
  subscription_status — a freshly-claimed paid team is no longer denied
  checkout. Reads an explicit wire flag, defaults true.
- P2-W2-16: APIError now preserves agent_action / upgrade_url (402/403
  tier-wall envelope) and Retry-After (429) instead of dropping them.
- W3 T5: createDeploy / createStack 401s now run the shared handle401
  (token clear + /login redirect) — the multipart path bypassed the
  central interceptor. SSE log stream surfaces a distinct SSEStreamError
  with status so a 401 mid-stream shows "session expired", not the
  misleading "deploy too old" copy.
- /stacks/new slug: accept slug / stack_id / stack_slug so the polling
  loop always has a key.

Pages:
- P2-29: PricingPage downgrade FAQ reworded to support-only (policy).
- P2-30 / L-03: pricing comparison grid corrected to 5 tracks (1 feature
  + 4 tiers); Team coming-soon CTA no longer renders an empty pill.
- P2-31: MarketingPage gets a CSS-only mobile disclosure menu ≤880px —
  phone visitors had no nav at all.
- W3 T5: StackCreatePage stops polling a deleted stack (404 → null) and
  surfaces an error instead of spinning for the full 5-minute window.
- L-01: useDashboardCtx skips the counts/billing fetches when /auth/me
  401s — a stale token no longer fires 5 failed requests on public pages.
- W5 T10: ForAgentsPage MCP package name corrected to instanode-mcp
  (was unpublished @instanode/mcp); claude/cursor mcp add gets the `--`
  separator; playground response sample includes the echoed `env`.
- ChangelogPage: 2026-05-18 entry added; 2026-05-17 entry gets the
  auto-deploy CI milestone.

SEO (scripts/prerender.mjs, src/lib/routeMeta.ts):
- Per-route <head>: each pre-rendered page now ships its own title,
  meta description, canonical, and OG/Twitter tags — subpages no longer
  self-canonicalize to the homepage.
- routeMeta.titleForPath wired into RouteTracker so SPA soft navigation
  updates document.title (WCAG 2.4.2).
- sitemap.xml generated at build time from the prerender route set —
  121 URLs incl. all blog/use-case detail pages (was a stale static 7).

public/llms.txt: Tiers section synced with content/llms.txt (Hobby Plus
+ Growth were missing).

Tests: APIError envelope parsing + routeMeta.titleForPath added.
Gate: tsc clean · vitest 639 passed / 3 skipped · vite build + prerender OK.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mastermanas805 added a commit that referenced this pull request May 19, 2026
…drift, SSE, 429 hint (#98)

Covers every remaining instanode-web P3 finding from BUGHUNT-REPORT-2026-05-18
not already handled by PR #96 (Wave D) or PR #97 (P2 wave):

- Global :focus-visible ring in tokens.css — keyboard/AT users had no
  consistent focus indicator anywhere (W3 T10, WCAG 2.4.7).
- favicon.ico + site.webmanifest — browsers' implicit /favicon.ico
  request 404'd; no web manifest existed (W5 T10).
- P3-08: MarketingPage Pro card "multi-env (dev/staging/prod + custom)"
  drifted from PricingPage's "dev/staging/prod" — aligned to the honest
  framing.
- P3-09: MarketingPage Team card rendered an empty <a href=""> dead CTA
  pill and the "talk to us" teaser had no contact link — Team CTA now a
  disabled "Coming soon" pill, teaser links mailto:hello@instanode.dev.
- P3-02: getResource() fired a /credentials fetch that 400'd for
  webhook/storage/queue resources — gated to db/redis/mongo via a named
  CREDENTIALED_RESOURCE_TYPES set.
- streamSSE only matched `data: ` (with space); the SSE spec makes the
  space optional — no-space `data:` lines were silently dropped.
- markdown renderer: content-repo cross-links written as `/use-cases/x.md`
  hit the SPA catch-all and dead-ended on the homepage — normalizeInternalHref
  strips a trailing `.md` from internal links.
- TeamPage unguarded Promise.all — no .catch(), no cancellation guard;
  a failed load left the page silently empty. Now mirrors the
  ResourcesPage/DeploymentsPage load pattern with an error banner.
- _headers: rewrote the comment to make unambiguous that the file is a
  non-operational migration artifact on GitHub Pages.
- 429/Retry-After user-facing hint (P2 deferred as P3): new retryHint.ts
  helper turns the retry-delay into a human "retry in Ns" string; wired
  into TeamPage's error banner. Reads the delay defensively so it works
  with or without PR #97's APIError.retryAfter field.

Tests: markdown .md-strip cases, retryHint unit tests added.
Gates: tsc --noEmit clean · vite build clean · vitest 645 passed, 3 skipped, 0 failed.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant