The Sovereign Console at /sovereign/deployments rendered every row's FQDN
as a Link to=`/dashboard` regardless of which row was clicked. On contabo
(mother) this resolved to /sovereign/dashboard (the CURRENT user's
Sovereign), so clicking ANY row in the deployments list always
navigated to the same dashboard — breaking the operator's expectation
that "click row X to see deployment X's pages."
Fix: route each row to /provision/<row-id>/dashboard on the mother view
(Catalyst-Zero), and to /dashboard on the chroot Sovereign view (where
each Sovereign sees only its own deployment, so /dashboard is correct).
Mode resolved via the existing DETECTED_MODE singleton.
Bumps bp-catalyst-platform chart 1.4.40 → 1.4.41.
Co-authored-by: Hati Yildiz <hatiyildiz@openova.io>
Live on otech124 right now: /api/v1/sovereign/self returns 503
deployment-id-not-yet-stamped because:
- CATALYST_SELF_DEPLOYMENT_ID env is empty (orchestrator never patches
it, and #984's cutover-step-09-graduate idea wasn't merged either)
- The handler doesn't fall back to the local store
The deployment record IS imported on Sovereign (verified — POST
/api/v1/internal/deployments/import returns 200, persisted log
confirmed). Once the handler scans the store, /sovereign/self
returns the deploymentId and every chroot-aware UI Link
(/dashboard, /jobs, /apps, /cloud) finally renders correctly.
Without this, every <Link> built via useResolvedDeploymentId on
Sovereign mode produces /provision//<page> with empty id segment,
which the route validator rejects with 'Deployment id in the URL
is malformed' (founder report).
Closes the live regression on otech124.
Co-authored-by: hatiyildiz <hatice.yildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #1021 was supposed to ship this code fix but the chart-version bump
landed first and the actual sed didn't apply (sed quoting mishap). The
debug-error fix never reached main. Re-shipping now as a clean Edit-
based commit. Captures git push stderr into push_err and prints it on
FATAL so the next iteration's failed Job logs include git's actual
rejection (auth / branch protection / hook).
Co-authored-by: hatiyildiz <hatice.yildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Billing's dispatchOrderPlaced enriches the order.placed NATS event by
calling /tenant/internal/tenants/<id>/subdomain over the in-cluster
ClusterIP. routes.go registers that path with the comment "Internal —
unauthenticated service-to-service", but main.go wraps everything
under /tenant/ in JWTAuth except /tenant/check-slug/. So billing got
401, returned "" for the subdomain, published order.placed with
subdomain="", and provisioning rejected every paid checkout with
"invalid subdomain expected=[a-z][a-z0-9-]{2,30}".
Add /tenant/internal/ to the public-paths bypass. Both gateways
already 401 the path externally, and subdomain values are public DNS
names — the documented threat model.
Co-authored-by: hatiyildiz <hatice@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per founder report on otech122, the Sovereign Console /jobs page showed
all 'Pending' status — the imported deployment record's job snapshot
captured at mother's phase1-watching state, frozen forever.
The fix is small: useLiveJobsBackfill on Sovereign mode (DETECTED_MODE
=== 'sovereign') prefers /api/v1/sovereign/jobs which sovereign.go
already exposes — it reads HelmRelease history + recent K8s Jobs from
the local cluster's apiserver via in-cluster config and returns LIVE
status. The /api/v1/deployments/<id>/jobs path stays the default for
contabo monitor surface (mother view of an in-flight provision —
that's where the imported record IS the canonical view).
Also added credentials:'include' so the cookie reaches the endpoint.
Closes the user-reported 'all jobs Pending forever' on Sovereign
Console.
Co-authored-by: hatiyildiz <hatice.yildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Last leftover from PR #983's URL contract that PR #992 reverts undid.
PR #996 caught the auth_handover.go + router.tsx /console/dashboard
references but missed AuthCallbackPage.tsx:80. The Sovereign-side
PKCE callback after Keycloak login was navigating to a route that
doesn't exist in the consoleLayoutRoute tree.
Found while verifying otech124 mid-Phase-1.
Co-authored-by: hatiyildiz <hatice.yildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two problems surfaced live on otech122 (founder report):
1. SovereignSidebar.tsx still has /console/X paths.
PR #983 originally fixed this. PR #984 introduced the same fix in a
different shape. PR #992 (revert of broken redirect chain) reverted
#984 and accidentally reverted #983's SovereignSidebar fix too —
both PRs touched the same nav literals. PR #998 re-fixed
Sidebar.tsx (mother) but missed re-fixing SovereignSidebar.tsx.
Symptoms: clicking Settings on console.<sov-fqdn> goes to
/console/settings (route doesn't exist → 'Not found'); other nav
items fall through to wizard-side /provision//<page> handlers.
2. AppsPage.tsx app card row link is not chroot-aware.
On the mother monitor surface, the row link to <Link to='/app/$id'>
escapes /sovereign/provision/<dep-id>/ to /sovereign/app/<id>.
Fix: same DETECTED_MODE-aware pattern as PR #1000 used for JobsTable
and FlowPage.
3. SovereignConsoleLayout's settings dropdown navigate also still
pointed at /console/settings — fixed inline.
Co-authored-by: hatiyildiz <hatice.yildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same architectural reasoning as PR #1008 (subdomains/check). The wizard's
StepCredentials, StepDomain, StepCloud-creds and StepSSH all run BEFORE
the operator authenticates. Gating those endpoints on a session cookie
returned 401 to every anonymous visitor and blocked the only flow that
matters.
Move from rg (session-gated) to r (unauthenticated):
- /api/v1/credentials/validate (Hetzner token + project id)
- /api/v1/credentials/object-storage/validate (S3 creds)
- /api/v1/sshkey/generate (read-only ephemeral keypair)
- /api/v1/registrar/{r}/validate (Dynadot key+secret)
All four are read-only probes — they call the upstream API
(Hetzner/S3/Dynadot) with the operator-supplied credential and return
200/400 based on whether it works. No state change on success. The
upstream API itself is the auth gate (a wrong credential simply gets
rejected at the upstream).
/api/v1/registrar/{r}/set-ns stays in rg (session-gated) — it's
called from CreateDeployment which is itself post-auth.
Closes the wizard 401 the founder hit on Domain (BYO Dynadot) +
Credentials (Hetzner) steps trying otech with omantel.biz.
Co-authored-by: hatiyildiz <hatice.yildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The marketplace checkout login surface diverged from the canonical
Sovereign wizard sign-in (console.openova.io/sovereign/wizard) on two
fronts. (1) Continue-with-Google was still rendered above an "or use
email" divider — founder wants email + PIN only. (2) The 6-digit PIN
row used 6 separate <input maxlength=1> boxes; paste only worked after
clicking inside a box first because no input was focused when verify
mounted.
Port the canonical PinInput6 (products/catalyst/bootstrap/ui/src/
components/PinInput6.tsx) to Svelte 5 — one hidden <input maxlength=6>
overlaid on 6 decorative boxes, auto-focused on mount AND on
visibilitychange + window focus. Paste-anywhere just works, mobile
SMS one-time-code suggestion still routes to the focused input.
Drop the inline ~80 LOC PIN handlers (codeDigits / codeRefs /
focusBox / setDigitAt / onDigitInput / onDigitKeyDown / onDigitPaste)
in favour of the new component. Remove the Google button, divider,
handleGoogleAuth / handleGoogleCallback, and the google_auth=1
URL-param $effect. Strip getGoogleAuthUrl / googleCallback from
imports. Simplify auth/callback.astro to a passive redirect to
/checkout — the route stays alive in case any old Google-issued
redirect URI fires.
API surface unchanged: /api/auth/magic-link + /api/auth/verify already
work as a PIN flow, only the UI shell changes. api.ts Google exports
are kept (dead code, but no backend coupling churn).
Co-authored-by: hatiyildiz <hatice@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* deploy: re-bump chart literal :b45a49f → :8ec8c01 (mistake-rollback fix)
PR #1006 rolled back to :b45a49f because the catalyst-api pod was
ImagePullBackOff for ~30s while pulling :8ec8c01. The image was IN
GHCR; the pull just took time. Pod recovered to Running on :8ec8c01,
THEN my rollback kicked in and reverted to :b45a49f — losing the
wizard credentials fix from PR #1004 that the founder needed.
Re-bump forward. :8ec8c01 contains useSubdomainAvailability's
credentials:'include' fix that closes the wizard 401 → false-502.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(catalyst-api): make /api/v1/subdomains/check public (no session required)
The wizard's Domain step renders BEFORE the operator authenticates —
PIN issue + verify happen AFTER they pick a subdomain. Requiring a
session cookie on /api/v1/subdomains/check forced 401 on every
anonymous visitor and trapped logged-out operators in a 'check
unavailable' state.
Move the route from rg (session-gated) to r (unauthenticated). Same
model as /auth/pin/issue: read-only public-facing endpoint with no
state change. Information disclosure is negligible — 'is this
subdomain taken?' is what DNS itself answers to anyone with a
resolver.
The handler routes to PDM (managed pool) or DNS (BYO); both are
read-only. PDM has its own rate-limiting middleware on the public
ingress, so anonymous spam is bounded by that.
Closes the wizard 401 the founder hit on otech119 Domain step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: hatiyildiz <hatice.yildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #1006 rolled back to :b45a49f because the catalyst-api pod was
ImagePullBackOff for ~30s while pulling :8ec8c01. The image was IN
GHCR; the pull just took time. Pod recovered to Running on :8ec8c01,
THEN my rollback kicked in and reverted to :b45a49f — losing the
wizard credentials fix from PR #1004 that the founder needed.
Re-bump forward. :8ec8c01 contains useSubdomainAvailability's
credentials:'include' fix that closes the wizard 401 → false-502.
Co-authored-by: hatiyildiz <hatice.yildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chart 1.4.35 referenced :8ec8c01 before the catalyst-build for that
SHA finished pushing to GHCR. Flux applied → catalyst-api pod stuck
ImagePullBackOff → wizard breaks ('worked few seconds then failed').
Roll the literal back to :b45a49f (the previous working SHA from
chart 1.4.34). Chart version stays 1.4.35 to avoid re-publishing
churn. The wizard credentials fix in :8ec8c01 will land when the
build catches up — at which point we manually re-bump the literal.
Co-authored-by: hatiyildiz <hatice.yildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(bp-catalyst-platform): bump 1.4.33 → 1.4.34 + literal :11dd19e → :b45a49f (#1000 cloud chroot + wizard banner)
* fix(wizard): include credentials on subdomain availability check fetch
The Domain step's POST /api/v1/subdomains/check was firing without
`credentials: 'include'`, so the catalyst_session cookie wasn't sent.
catalyst-api's RequireSession middleware returned 401, which the
wizard surfaced as 'Availability check failed (HTTP 401)' —
indistinguishable from a true upstream PDM failure.
Add credentials:'include'. Other session-gated wizard fetches already
have this; this one was missed.
Repro: open /sovereign/wizard signed-in, type a subdomain, see
'Availability check unavailable'. catalyst-api access log shows POST
.../subdomains/check → 401.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: hatiyildiz <hatice.yildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two operator-reported bugs:
1. Cloud sub-pages still escaped chroot. PR #998 closed Sidebar/JobsTable/
FlowPage but missed CloudPage (4 navigate sites), CloudListView (2),
UserAccessEditPage (2). Apply the same DETECTED_MODE-aware target
construction so /provision/<id>/cloud paths stay scoped under the
chroot on the mother monitoring view.
2. WizardPage auto-redirected signed-in operators with an inflight
deployment to /provision/<id>/dashboard, blocking the legitimate
case of starting a SECOND provision while the first is still in
flight (founder: 'maybe I'll provision one more').
Replace the auto-redirect with an inline banner at the top of the
wizard pointing at the inflight monitor. The wizard stays
interactive — operator can step through and Launch a second
deployment if they want, OR click 'Open monitor →' to resume the
first one.
Co-authored-by: hatiyildiz <hatice.yildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>