Promote no longer ships only code. After commit + sync it now:
- waits for target Odoo readiness
- queries source + target for installed-set across the recipe
- pre-checkpoint on target (best-effort)
- ButtonImmediate(install) on target for the delta
Op stays running until install hooks complete; complete means
prods Modules tab matches stages installed-set, not just mount layer.
Odoo 19 image runs Python 3.12 (Bookworm), which enforces PEP 668.
pip install --user is blocked without --break-system-packages, so
every addon with even one external_dependencies.python entry failed
on Odoo 19. Build container is single-shot, so the lock does not
apply — the flag is the correct override here.
- Wizard: default Odoo to latest matrix entry; PG snaps to recommended
on every Odoo change (was sticky-compatible, leaving PG 16 when
switching to Odoo 19 even though 17 is recommended)
- export.go: pgClientImageFor(shape.PostgresVersion) — pg_dump tracks
source instances actual PG version (17/18/whatever ships next)
- ProjectsView: super-admin All-tenants mode now fans out across tenants
(was falling back to platform tenant → empty Projects page)
- waitForJobTerminal: pod log tail spliced into job-failed errors
(export, import, backup, restore — visibility into REAL cause)
- runSpawnEnv: register env BEFORE seed (was after) so a failed seed
leaves a registered-but-empty env instead of a ghost instance
Backend SSE handler already accepts ?tenantId= as an additive filter
on top of canSeeOp (Phase G stays load-bearing); frontend now passes
the global tenant filter chip's value through both NotificationBell
and ActivityTab. Watcher clears + restarts the stream when the
super-admin switches tenant context. Dismissed Set is user-level
and survives the switch.
#2 Dismiss / Clear failed:
- Per-row [×] (text 'Dismiss', shown on hover) on terminal ops
- 'Clear' button in dropdown header when any dismissable rows exist
- Dismissed IDs persisted to localStorage (tower_dismissed_ops)
- Pruned during the 30s sweep when the underlying op falls out
of the recent window — keeps storage from growing forever
- badgeCount + failedCount filter dismissed entries so the red
pill clears the moment the operator acks the failure
#4 Toast popups:
- useToast composable + Toaster.vue mounted at App.vue
- Triggered from NotificationBell.upsert on terminal transition
when the op was running ≥30s AND the bell isn't open
- Success: 5s auto-dismiss; Failure: sticky until clicked away
- Click-through links to the per-instance Activity timeline
(#op_<id>) for any op carrying instanceCode
- Stack of 3, oldest drops on overflow
- No external dep — hand-rolled to match v3's component style
NotificationBell + ActivityTab opened EventSource without auth
(native EventSource API can't set Authorization headers). Phase G's
canSeeOp guard correctly dropped every event for the resulting
anonymous viewer, leaving the bell silent except for the one-shot
backfill on mount.
Backend: claimsFromRequest now falls back to ?token= query param
when the Authorization header is absent. HTTPS-only ingress means
the token stays inside the TLS tunnel; the 15-min access-token TTL
bounds any leakage if it ever surfaces in browser history or proxy
logs.
Frontend: streamOperation + streamAllOperations append the access
token via streamURL(). Plus token-expiry-aware reconnect: on
EventSource error, debounce 5s, close, run authFetch('/me') to let
the 0.61.18 refresh path renew the access token, then re-open with
a fresh streamURL. Without this, the native auto-reconnect would
loop forever with the now-stale token after 15 min.
The grep -oE for instanceCode matches BOTH provenance.instanceCode
AND the v2 root mirror, returning two lines. sed processes each line
but the resulting SOURCE_CODE shell variable was multi-line, which
made the directory check fail (-d "/var/lib/odoo/filestore/odoo16
[newline]odoo16") → rename branch silently skipped → Odoo with
db_name=odoo16v2 looks at /var/lib/odoo/filestore/odoo16v2/, finds
nothing, returns 500 on every asset.
Added head -1 to the pipe so SOURCE_CODE is single-line, plus an
echo so the rename branch's path is visible in Job logs even when
it short-circuits.
Phase C made instance-create tenant-aware for Cloudflare DNS, but
migrate.go and templates_deploy.go kept using the legacy global
*cloudflareClient (zone=odoosky.org). Result: a tenant migrate to
4th.online silently created the A record under odoosky.org as a
literal subdomain ('odoo16v2.tenants.4th.online.odoosky.org' →
right IP) — Tower logged 'DNS A record set' successfully because
the API accepted the call, but the actual hostname the user
browses to was never published to the right zone.
Both flows now use cfResolver.clientFor(tenantID, fqdn) to find
the tenant's CF token + correct zone. If no token covers the
domain, the op fails with a clear 'configure tenant CF token'
message instead of silently writing to the wrong zone.
MigrateDrawer hardcoded '<option value="">Platform server (default)</option>' as
the first picker entry, regardless of tenant scope. A tenant operator
saw it as a selectable default — and selecting it (or just leaving
the default empty) sent the migrate to the platform cluster, which
the operator has no business deploying to.
Now: removed the hardcoded option. Auto-pick the first deployable
non-platform server on load (matches DeployInstanceDrawer pattern).
Picker shows 'Pick a server…' as a disabled placeholder when nothing
is selected.
The export Job was using a stale platform Secret (s3-backup-creds)
and hardcoded bucket/endpoint, so bundles landed in odoosky-v3-backups
while Tower's verify (tenant-scoped after Phase F) read from the
tenant's bucket. Result: 'bundle missing from S3 after job
succeeded' even though the upload itself worked.
Same bug existed in import. Both fixed: keys+region+endpoint+bucket
now come from Tower's resolver view of the tenant, passed directly
into the Job env.
Plus: BackupsView crashed on r.backups.runs.length when runs is
null. Added the missing null guard.