mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
8fb94d28eb
* feat(cms): add verified desktop touchpoint hosts and Test acceptance * fix(cms): keep Test modal scrim above app chrome * fix(cms): load host styles and support hover packages * fix(cms): report mounted host compatibility for restoration * fix(cms): parse Vela actions in shared manifests without executing them * fix(cms): preserve authorized actions and correct modal and hover behavior (#8003) * fix(cms): harden touchpoint host interactions * fix(cms): preserve modal actions and verify effective close controls * test(cms): cover hover exit and above-anchor placement * fix(cms): hide client test controls by default * fix(cms): persist automatic campaign impressions per account and device * fix(web): remove host campaign close button * fix(cms): suppress campaign modals during onboarding * fix(cms): unify test and production campaign lifecycle (#8118) * fix(cms): enable safe campaign navigation in test and production * fix(cms): enforce server-authorized touchpoint lifetimes * fix(cms): refresh production touchpoints when client language changes * fix(cms): stabilize hover refresh and synchronize Test locale * fix(cms): preserve campaigns on focus and remember Test dismissal * fix(cms): discover Test deployments without reloading * fix(cms): keep granted touchpoint display authority through transport failures A refresh that times out or fails in transport is not a withdrawal, but the shared lifecycle treated every failure the same way: `status = "error"` then `revoke()`, which tore down a lease the server had already granted and was still inside its own deadline. One slow poll therefore blanked a running campaign until the next 30s tick, and a second failure could not restore it because the value had been dropped. `abandonAttempt` now names the invariant: cancel the attempt, keep the lease, and let `armExpiry` retire it at the deadline the server actually granted. The lease length is the bound, so no retry budget is needed — Test's sixty-second contract survives exactly one missed poll, and Production survives as many as its authorization covers. Three cases still clear, and one of them is new: - the failure carries the server's own withdrawal. A 410 is authoritative even when its receipt body is unreadable, so `ProductionTouchpointLoadError` now marks that one case and the lifecycle honours it. Without this, a malformed 410 would have been ridden out as if it were transport noise. - nothing is mounted, or the lease was already fenced by `wake`. Page recovery has no evidence the activity is still live, so it stays withdrawn. - the lease has run out on its own. This does not fix OPEND-3237, which is a new activity failing its FIRST load: there is no prior lease to keep and the display stays empty. It fixes the adjacent class where a mounted activity is erased by a transient refresh. * fix(cms): keep a failed refresh recoverable inside its own poll cycle Three changes to how one refresh cycle behaves when it does not go cleanly. None of them fixes OPEND-3237, whose first-load failure is still unexplained; they remove the ways a working campaign is lost to an ordinary hiccup. Bounded retries. A failed attempt used to get its next chance from the fixed 30s tick. For the sixty-second lease the Test runtime is granted, that chance lands exactly when the lease expires, so a single failure put display on the edge of going blank with nothing able to recover it. Retries at 1s and 3s give a fast failure — transport error, 5xx, DNS — several chances while the lease is still comfortably alive. A retry must finish inside the cycle that spawned it. `refresh` declines to start while a request is in flight, so a chain that ran past the next tick would not merely be late: it would swallow that tick entirely. Worst case was 15+1+15+3+15 = 49s against a 30s interval. Scheduling now requires room for the retry AND its full budget, which is also what makes "retries cover fast failures" true in the code rather than only in a comment — an attempt that burned the whole budget leaves no room by construction and is never retried. The retry budget resets per cycle, not per success: otherwise the first cycle to exhaust its retries would leave every later cycle with none. Request budget 10s -> 15s. One refresh fetches a context and every enabled placement, so the budget covers a round, not a request. A real round of 11.5s whose placements all succeeded was being abandoned by the ten-second budget. It stays well under the 30s interval on purpose. Forward `accept-encoding` through the touchpoint runtime proxy. Decisions carry base64 content and run to megabytes, and the proxy built its upstream headers from scratch, dropping the caller's preference — every refresh pulled the payload uncompressed, measured at 383KB against 214KB for the same decision. Because the body is piped through verbatim, upstream's `content-encoding` has to be handed back with it, or the caller decodes gzip as JSON; both halves land together. Verified end to end through daemon and web: 2,149,685 -> 1,606,678 bytes on the wire, byte-identical after decode. An authoritative withdrawal is never retried: a 410 that took a campaign off the screen must not put it back. * fix(cms): present each Test campaign modal once per account and device Test modal presentations now share the production touchpoint-displayed:v1 impression: recorded once visible, read before any new automatic opening, and persisted across reload, remount, and client restart. * fix(cms): give the Test control readout an explicit ladder weight