- partial-install visibility: error path now carries the modules
installed before the failure, instead of silently dropping the list
- introspect log line: shows source-installed/target-installed/delta
counts so the bell row narrates the comparison
- Authenticate calls: 15s timeout context (matches addon_lifecycle
pattern; hung Odoo no longer pins the parent op)
- source readiness probe: 30s waitForOdooReady before introspect so
source-down fails fast with clear message instead of cryptic auth
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.