The instance row's '<code>.tenants.odoosky.org' was being computed
client-side from the code alone, so a tenant whose domain is
'4th.online' still saw 'odoo16.tenants.odoosky.org' in the list +
the Open button — wrong zone, no cert, scary Firefox warning.
Backend: Argo App now carries an 'odoosky.io/domain' annotation
written at create time from req.Domain (the values.yaml domain),
read back in argoApplicationSummary.Domain. Delete handler reads
the same annotation so DNS cleanup hits the right Cloudflare zone
instead of the platform default.
Frontend: Instance.domain field, used by InstancesView, Vitals,
ActionBar, with a fallback to the legacy pattern for any pre-Phase-F
Argo App that hasn't been backfilled yet.
Backfill for live odoo16: kubectl annotate done out-of-band.
DeployInstanceDrawer + MigrateDrawer were hardcoding
'<code>.tenants.odoosky.org' as the auto-suggested instance domain,
even when the operator's tenant has its own domain set in Settings.
A tenant whose wildcardHost is '*.tenants.4th.online' would still see
the wizard pre-fill 'odoo16.tenants.odoosky.org' in the Domain field —
the suggestion landed in the wrong zone, instance create would fail
DNS write.
Both drawers now fetch /api/tenants/<id>/settings on mount and use
'tenants.<domain>' (with leading '*.' stripped from wildcardHost) as
the suffix, falling back to odoosky.org only if the call errors or
the field is empty.
UpsertCluster was not setting the odoosky.io/tenant-id label on the
ArgoCD cluster Secret. Result: handleListServers tenant filter
attributed every freshly-connected cluster to no-tenant, making the
just-connected box invisible to the operator who registered it.
Threads ownerTenantID from the connect token through to the body of
the POST /api/v1/clusters call. Backfill of any pre-fix cluster
Secret is a one-line kubectl label.
cloudflare_resolver.go reads data['token'] but the writer (and the
test endpoint) stores under data['api_token']. Result: every fresh
tenant's CF resolver returned no token even when one was saved,
killing DNS records view AND any instance lifecycle DNS write.
Caught while smoke-testing the multi-tenant signup flow.
3 bugs caught while smoke-testing the multi-tenant signup → first
deploy flow:
1. /api/me returned capabilities: null when scope.role was nil
(a tenant member viewing the platform tenant context). Frontend's
useAuth.can() does for-of on the array → TypeError. Fix: backend
returns []string{} not nil; frontend defends against null.
2. WelcomeView slug input pattern '[a-z][a-z0-9-]{2,39}' is rejected
by Firefox's HTML5 form validation under the v-flag (treats trailing
'-' as a class-range start). Move hyphen to start of the class.
3. Domain & DNS Save button is genuinely separate from the Cloudflare
token Save (by design — token rotates independently). Documenting
here so the next person doesn't think they saved when they didn't.
Refactor s3Resolver from a single-global-creds reader into a
tenant-scoped factory. Each tenant brings their own S3 endpoint,
region, three named buckets (backups + templates + audit), and
access keys (in Vault at v3/tenants/<id>/s3-credentials).
Touches:
s3.go — s3Resolver becomes factory; tenantS3 wraps
one minio.Client + bucket per tenant
audit.go — events grouped by tenantID per flush, written
to the tenant's audit bucket
backups.go — fleet view fans out one S3 LIST per tenant;
per-instance handlers resolve via Argo App
export/import/migrate — tenant resolved from Argo App label
or scope.TenantID
templates_* — per-template tenant lookup via templateTenantID
(platform tenant for OwnerPlatform manifests)
vitals.go — last-backup probe pulls tenantID before list
Adds AllTenants() to PlatformStore so the templates orphan sweep
can iterate every tenant configured with a templates bucket.
Build: tower:0.61.1 — pushed to registry.odoosky.cloud
Backend rebuild for the route I added in 0.60.2 + ui:0.61.2 cycle —
forgot to rebuild Go binary alongside the chart bump. 0.60.2 binary
didn't carry the handler, hence the 404 on Provision now click.
POST /api/tenants/{id}/settings/s3-buckets/provision uses stored
creds to HeadBucket+MakeBucket the three derived buckets idempotently,
then persists their names to YAML. Surfaces in Settings → Backups
panel as 'Provision now' button next to the bucket list. Lets the
operator create buckets without rotating keys.
handleTestS3Credentials no longer requires bucket names in YAML —
testTenantS3Buckets derives from slug, same as Save's auto-create.
Test stored creds now works whether the buckets have been created
yet or not. Per-bucket result still surfaces 404 vs 403 vs other.
Tenant pastes endpoint + region + access keys. Tower auto-derives
3 bucket names from slug (<slug>-backups, -templates, -audit) and
HeadBucket+MakeBucket each at credential-save time. UI removes the
3 bucket-name inputs entirely; shows what will be created instead.
Removes the 'go to MEGA dashboard, click new bucket × 3' toil.
One credential save = three buckets ready.
Data model: PlatformTenant.S3 = { Endpoint, Region, Buckets: { Backups, Templates, Audit } }
Vault: legacy v3/data/s3{,-templates,-audit} paths wiped (decision in
docs/decisions/0001 path is bring-your-own only; per-tenant only).
UI: 3 bucket fields (Backups / Templates / Audit), single endpoint +
region + credential pair. Test does HeadBucket on each configured
bucket and reports per-bucket pass/fail.
Note: writers (audit/templates/backups handlers) still read from old
paths. Phase F.2 (next) sweeps the ~30 call sites onto a tenant-scoped
s3Factory. Tower compiles + serves API; backups+audit+templates writes
are non-functional until F.2 lands. v3 has no customers, so the
breakage window is tolerable per memory feedback_v3_disposable_no_customers.
Per docs/decisions/0001-platform-fallback-deferred.md, instance DNS
automation no longer silently falls back to v3/platform/cloudflare-token.
Tenants without a configured CF token get a clear error at instance
create instead of pretending to work via shared infrastructure.
The platform Vault entry stays seeded for future revival.
Backend: UpdateTenantSettingsReq → pointer fields. Each card saves
only its own keys without clobbering the other card.
Frontend:
- 'Save Domain & DNS' button inside the Domain & DNS card
- 'Save Backups target' button inside the S3 card
- Wildcard host hidden behind 'Advanced — customize wildcard host'
disclosure. Default is shown as read-only display under Domain so
the operator sees what URLs their instances will land at.
- Removed the global Save settings button (each card now self-saves)
Replace event-based 'touched' flag with computed isCustomWildcard.
Empty wildcard or wildcard==derived → 'auto-derived', auto-fills
on domain change. Different from derived → 'custom', sticks.
Fixes the empty-after-delete trap that kept touched=true forever.
- new GET /api/tenants/{id}/dns/records endpoint lists A+CNAME records
in the tenant's CF zone matching the wildcard pattern (read-only)
- TenantSettingsTab.vue: 'Live DNS records' panel with refresh button
- wildcard host auto-derives from domain (visible value, not placeholder)
- placeholder text now generic *.tenants.example.com
Placeholder text (*.tenants.acme-erp.com) was prescriptive and
indistinguishable from the saved value. Now wildcard auto-derives
from the domain field as a real value (visible, savable, editable).
'reset to default' button surfaces when user customises.
Tenant owners had no nav link to their own tenant settings page.
Adds a 'Tenant' workspace link visible to authenticated members
of any tenant. Super-admins still use /admin/tenants list (which
shows all tenants and lets them switch).
Fixes signup race: verify→/me 401 because activate ran async-reload
and lost the race with the very next /me call. Sync reload eliminates
the window. ~50ms slower per write, much cleaner.
0.57.0 had a Go ServeMux ambiguity between /api/servers/{name}/capacity and /api/servers/connect-token/{token}. Moved the new endpoints to /api/connect-tokens/* to break the wildcard collision.
Adds SSH-key + token-installer auth methods to Connect Server (188d). Three-tab drawer; one-time URL primitive shared with Teardown (188e). All previous behavior preserved — password tab is the default.