fix(console): make the new Console banner responsive and snoozeable - #3193
Conversation
Three fixes found while testing on production. Responsive: the layout switched to a column at isSmallViewport (max-width 767px) but Button's .is-full-width-mobile applies at max-width 768px. At exactly 768px the row stayed horizontal while the button took the full line, crushing the copy into a ~90px column seven lines tall. Switched the direction to isTabletViewport and dropped fullWidthMobile so the button sizes to its content. 768px goes from 182px tall to 96px. Dismissal: closing the banner wrote a localStorage key that was never cleared, so one click removed the message for the entire migration. Now uses hideNotification with a one week cool-off and exponential backoff, and shouldShowNotification to gate registration. Repeated dismissals still fade it out, but a stray click no longer opts the user out permanently. Resize: AlertStack owns the shell offset for alerts inside it but only re-measured via ResizeObserver and afterNavigate. GradientBanner used to compensate with its own window resize handler, which no longer runs when a stack is in charge, so resizing left the header overlapping the banner. Added the resize listener to AlertStack, which also covers HeaderAlert.
Console (appwrite/console)Project ID: Tip Global CDN and DDoS protection come free with every Sites deployment |
Greptile SummaryThe PR makes the new Console banner responsive and snoozeable while moving viewport-resize offset recalculation into the shared alert stack.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Reviews (5): Last reviewed commit: "revert: drop the local snooze fallback a..." | Re-trigger Greptile |
bun audit --audit-level high started failing on every branch after two advisories were published against browserslist <=4.28.6, reached through svelte-preprocess and @sentry/sveltekit. Neither is a direct dependency, so this pins it in the existing overrides block alongside the other transitive security pins. Resolves to 4.28.8. GHSA-c83g-rgw3-j3cx, GHSA-73wf-gq98-2v4g
hideNotification fired updatePrefs without returning or catching it, so a rejected write was unobservable: the banner hid locally, nothing was recorded, and it returned on the next load with the backoff count lost. updateNotificationPrefs and hideNotification now return the promise. This is backwards compatible, the four existing callers ignore the return value and keep their fire-and-forget behaviour. The banner awaits it and, if the write is rejected, falls back to a local snooze for a week so the dismissal is never silently dropped. Account prefs stay the source of truth: they carry the backoff count and follow the user across devices, and the local key is cleared as soon as a write succeeds. Registration now goes through canShowNewConsoleBanner, which checks both. Adds notifications.test.ts covering the returned promise, the rejection path, the recorded expiry, and the backoff counter.
The local fallback recorded a flat one week with no hide count, so a run of rejected preference writes restarted the sequence every time instead of continuing it. That contradicted the behaviour described one line above it. Extracts the fallback into localSnooze.ts, which reproduces the same curve as notifications.ts: coolOffPeriod * factor ** (hideCount - 1). Repeated failures now escalate 1w, 2w, 4w, 8w, and a successful write still clears the local copy so account prefs remain the source of truth. Reads and writes are guarded, so private mode or a corrupt value fails towards showing the banner rather than hiding it forever. Adds localSnooze.test.ts covering the escalation, the formula parity with account prefs, the expiry boundary, and the corrupt-value case.
|
@greptile ## Fixed — the fallback now escalates on the same curve Correct catch, and it was a real inconsistency: the comment directly above said "a week at first, doubling each time", while the fallback wrote a flat week with no hide count. A run of rejected writes restarted the sequence every time instead of continuing it. Extracted into Repeated failures now escalate 1w → 2w → 4w → 8w, and a successful write still clears the local copy, so account prefs remain the source of truth and continue to carry the cross-device count. Reads and writes are wrapped, so private mode or a corrupt value fails towards showing the banner rather than hiding it forever — the safer direction for a dismissable promo. Proof
Full CI chain
|
| clearLocalSnooze(SNOOZE_FALLBACK_KEY); | ||
| } catch { | ||
| // Prefs rejected the write, so the snooze would be lost on the next load. Hold it in | ||
| // this browser instead, escalating on the same curve so repeated failures continue | ||
| // the sequence rather than restarting at a week. Cleared once a write succeeds. | ||
| writeLocalSnooze( | ||
| SNOOZE_FALLBACK_KEY, | ||
| nextLocalSnooze( | ||
| readLocalSnooze(SNOOZE_FALLBACK_KEY), |
There was a problem hiding this comment.
Backoff history resets across stores
When successful and rejected preference writes occur in the same dismissal history, each path derives its count from only one store and clears or ignores the other. This resets part of the exponential-backoff sequence; for example, after three successful dismissals, a rejected fourth dismissal snoozes the banner for one week instead of eight weeks, causing it to reappear seven weeks early.
Knowledge Base Used: Console application shell
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/components/newConsoleBanner.svelte
Line: 55-63
Comment:
**Backoff history resets across stores**
When successful and rejected preference writes occur in the same dismissal history, each path derives its count from only one store and clears or ignores the other. This resets part of the exponential-backoff sequence; for example, after three successful dismissals, a rejected fourth dismissal snoozes the banner for one week instead of eight weeks, causing it to reappear seven weeks early.
**Knowledge Base Used:** [Console application shell](https://app.greptile.com/appwrite/-/custom-context/knowledge-base/appwrite/console/-/docs/console-application-shell.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.The fallback and its tests were about 210 of the 247 lines in this branch, all to handle the case where an account prefs write is rejected. For a dismissable promo the cost of not handling it is that the banner shows again on the next load, and the user closes it again. Reverting leaves dismissal identical to the four other hideNotification call sites: fire and forget, account prefs as the only store. No second source of truth, so no way for two counters to disagree. Keeps the three fixes this branch is actually for: the 768px breakpoint mismatch, the AlertStack resize listener, and the browserslist pin.

Three fixes found while testing the merged banner (#3192) on production.
1. Broken layout at tablet widths
The row switched to a column at
isSmallViewport(max-width: 767px), butButton's.is-full-width-mobileapplies atmax-width: 768px. At exactly 768px the layout stayed a horizontal row and the button took the full line, crushing the copy into a ~90px column seven lines tall.Direction now keys off
isTabletViewport, andfullWidthMobileis dropped so the button sizes to its content.newDevUpgradePro.sveltehas the identical pairing (isSmallViewport+fullWidthMobile) and the same 768px break. Not touched here to keep this scoped, but worth a follow-up.2. Dismissal was permanent
Closing the banner wrote a
localStoragekey that nothing ever cleared, so a single click on the X removed the message for the whole migration. For a rollout that runs months, one stray click shouldn't be a permanent opt-out.Now uses the existing
hideNotificationhelper with a one week cool-off and exponential backoff, andshouldShowNotificationto gate registration. Someone who keeps dismissing it sees it less and less; someone who mis-clicks gets it back in a week.(Note:
newDevUpgradeProalready stores a timestamp on dismiss and never reads it — the intent was there, unfinished.)3. Resize regression from #3192
AlertStackowns the shell offset for alerts inside it, but only re-measured viaResizeObserverandafterNavigate.GradientBannerused to compensate with its ownsvelte:window on:resize, which #3192 correctly stopped running when a stack is in charge — removing the safety net.Measured, resizing 1440 → 375 without reload:
header.topAdded the resize listener to
AlertStack, which is now the right owner. This also coversHeaderAlert, which has the same gap.Test plan
bun run format/lint/check— clean