Conversation
The service worker script was matched by the auth middleware and 307-redirected to /login for visitors without a session, so every page logged "SecurityError: ServiceWorker script is behind a redirect" and PWA/offline never activated. Static service-worker assets now bypass the matcher like manifest.json already does. Present on production too — promoting staging picks up the fix there.
Features page claimed 10,000 pageviews/month; the pricing page and plan data say 5k. Align the copy with the real plan limit.
"Cookie Banner Required: false" (Pulse's headline win) rendered as a
red ✗ while GA's "true" got a green ✓ — the first row of the table
visually awarded GA the win. Cells where the raw boolean inverts
sentiment now use worded values with an explicit good/bad tone
("None" green / "Required" red).
The Popular and category-group headers rendered sentence-length UPPERCASE via CSS (e.g. "STATIC SITES & DOCUMENTATION"), which the label rule reserves for 1-2-word data labels. Drop the uppercase transform and wide tracking; the h2s keep their display font.
TAB_META in useHomePageMeta covered every settings tab except roles, so /settings/organization/roles fell back to "Settings" and the topbar read "Settings › Settings".
Zero importers across all import forms (static, dynamic, require); the marketing funnel mockup uses its own local copy. 2,042 LOC.
Favicons in the authenticated Referrers/Pages panels (and the marketing demo + share-page OG image) loaded straight from Google's s2 service, sending every customer's referrer and page domains plus the user's IP to Google. All consumers now go through a same-origin /api/favicon proxy: the upstream fetch happens server-side behind the CDN cache, so browsers only ever talk to Pulse. CSP drops the google/gstatic allowances so any regression fails visibly instead of silently.
…-empty The sessions request error was swallowed, so a 500 rendered as "No suspicious sessions found" — a failed request looked like a healthy site. The list now has explicit error (with retry) and loading states; the empty state only renders for a genuinely empty successful response.
…vailable Display Name and Email rendered silently empty whenever the vault PII was never provisioned (a sign-in through Pulse's own form never sets the ciphera_pii cookie that Ciphera ID drops on .ciphera.net — same behaviour on prod). The tab now explains why the fields are empty and points at Ciphera ID as the recovery path, instead of pretending the profile has no name and no email.
Local overlays (dialog, tooltip, select dropdown, date-range picker) shipped border-only and read flat against the dark canvas, while Facet's own Modal/Tooltip cast shadow-lg/xl — inconsistent depth within one product. Dialogs get shadow-xl, smaller floats shadow-lg; the command palette composes DialogContent so it inherits the fix.
…rolling Long tabs (Privacy, Notifications) scrolled both away, losing context and the tab switcher. The header sticks to the top of the scroll container; the nav sticks below it and scrolls internally when taller than the viewport.
The page rendered all 18 release sections at once (~29k px desktop, ~53k px mobile) and showed two "Changelog" H1s (page hero + the markdown's own). The five most recent sections render expanded, the rest sit behind a native details disclosure, the markdown H1 is stripped, and heading dates display as DD/MM/YYYY like the rest of the product. Keep-a-changelog link definitions are appended to both chunks so reference links keep resolving.
The default "All" view rendered every integration as a full card (~8.6k px desktop / ~21k px mobile); it now uses compact icon+name tiles and switches to full cards when a category or search narrows the list. Black-only brand logos were styled fill-current invert — the inherited text colour is already light, so inverting it painted the glyphs near-black on the dark tiles (Vercel, GitHub Pages, Eleventy and 17 more); they are now plain white fills, WooCommerce joins them, and Solid.js uses its light-blue brand tone.
…g copy The Privacy tab's ad-hoc ternary recognised only 'pro' and labelled every other plan "Free" — a Pioneer org read "Your Free plan supports up to 2 years". Billing already had the correct mapping; it now lives in lib/utils/plan.ts and both tabs consume it.
Zero-PII members with no stored email showed their 36-char user id as the display name. The signed-in member reads "You", pending invites show their invite email, and the rest fall back to a short member id.
Unmapped actions leaked raw snake_case event names and payloads rendered as a JSON blob. Unknown actions fall back to a sentence-cased form and payload entries render as labelled key/value fields.
Visiting /auth/callback without a code param (stale link, prefetch, direct visit) spun the loading overlay forever. It now shows the existing error card with a way back to login.
The install step promised a snippet but rendered nothing when the setup session had no site. It now says why there's no snippet, offers a way back to the site step, and keeps an explicit continue.
The admin grant-plan quick links (+1 Month / +1 Year / Forever) were blue and the setup done-page tiles used blue/purple/amber — UI chrome, not data-viz, so they follow the system accent. Chart series and telemetry chips keep blue by design.
Five DD/MM ticks collide at 390px (real dashboard and the marketing demo). numTicks is now an upper bound; narrow charts budget ~80px per label and drop to what fits.
…lain engagement percentiles A previous period with fewer than 10 visitors turns every delta into double-digit noise and paints the whole KPI strip red; those windows now show no delta. The cryptic engagement sub-score row (S P77 · T P88 …) gets a plain-language hover explanation of the percentile ranks.
A yearly org landed on a Monthly-defaulted toggle, misquoting their own plan. Ref-guarded so revalidation doesn't stomp a manual toggle.
693 / 100,000 takes arithmetic to read as 0.7%; the bar makes headroom visible at a glance and turns red at 90%.
Every other tool tab keeps its h1 in the empty state; these two dropped straight into a centered connect card with no page identity.
…n helpers
Also routes the sites/new limit toast through it — it interpolated the
raw plan_id ("free plan limit reached").
…ls, site cards - Installation referenced "Settings → Goals & Events"; the tab is Goals. - Invalid-invite page sent anonymous visitors to "Go to dashboard" (which is a login wall) — now "Go to Pulse". - Funnels showed two identical orange create CTAs when empty and mixed "Create Funnel"/"Create funnel" casing. - All-zero 24h site-card stats render as em dashes instead of a wall of 0s that reads like broken tracking.
Two transitions inlined the tuple literally; drift-prone copies of a constant that already exists in lib/motion.
At the min position there is no orange range beside the thumb and the background-coloured square vanished into the canvas (pricing slider read as a sliver). Solid fill plus a ::before overlay that extends the touch area past the visual 20px.
… affordance Empty table cells mixed "-" and blank; the quarantine active-tab underline was white where the app convention is orange; the admin nav cards gained a border hover state so they read as clickable.
The list's date range lifts into ?period/start/end via useFunnelDateRange (the period URL grammar extracts to lib/hooks/periodUrl.ts, now shared with journeys, including a clamp-to-today shift helper — tested). The accordion cards become summary rows: the whole card links to the detail route carrying the current range, edit/delete float as hover siblings (no more nested buttons), step chips show the real page/event targets with kind glyphs, the conversion % is plain white with a tiny-base- guarded delta off a local-date-parts previous range (the toISOString UTC drift dies), and a lazy orange sparkline rides the right rail. A failed list fetch now shows an error card with Retry instead of the fake 'No funnels yet', and the skeleton mirrors the real layout.
funnels/[funnelId] stops redirecting and becomes the real route: quiet back link, name + description + mono window/created meta, URL-synced DateRangePicker, Edit (modal) and Delete (confirm → back to list), and a five-KPI band — Conversion / Visitors / Converted / Biggest drop-off (showing the step's real value, never the default name, and no more green 0%) / Median time — with tiny-base-guarded deltas off the shared previousDateRange helper (local date parts; the summary card now uses it too). 404s get a friendly not-found card, fetch failures an error card with Retry, range changes the Updating chip. The edit route redirects to the detail, the breadcrumb renders the funnel's name under a linked Funnels parent, and the orphaned detail skeleton is rebuilt to the real layout.
Equal-width step columns over vertical level rails filled bottom-up to each step's conversion-of-start (orange fill, 2px solid top edge), with SVG trapezoid bands measured off the real rail rects connecting adjacent levels and red −N · x% drop labels in the gutters. Clicking or arrowing across columns writes ?step= (default stays out of the URL); the selected column carries an orange ring. Zero-visitor periods show flat rails with an inline note, single-step funnels get one column and no bands, and below md the columns stack with short vertical connectors — the emerald bars are gone and the product funnel finally looks like the one the homepage sells.
A two-pane strip under the canvas follows the selected step: exits on the left (kind glyphs, proportional bars, 'Entry step' note on step 1) and a dimension breakdown on the right — the Select offers exactly the keys the backend's ValidDimension accepts, rows carry the dashboard's flag/brand icons with visitors and conversion %. getFunnelBreakdown gains an SWR hook (cached per step+dimension, stale kept while revalidating), panes height-morph on step change, spinners obey the 150ms rule in stable-height boxes, and empties only render settled.
Overall conversion over time as the single orange series on the shared area-chart primitives (they compose cleanly for multi-series — the minimal-SVG fallback wasn't needed). Per-step series toggle on via quiet chips (orange text only when active, showing real step targets) and draw in neutral-400 so the color budget holds. Numeric DD/MM x-axis, % y-axis, tooltip through the chart system, stable-height card with the Updating chip on range changes and an error card with Retry.
The dashboard filter system lands on the detail page wholesale — pills, button, single-surface popover, duplicate guard and edit-via-pill all identical — with committed filters serialized into ?filters= by the dashboard's exact codec and fed to stats (current and previous period), trends and breakdowns. The per-dimension suggestion fetcher extracts from the dashboard page into lib/hooks/useFilterSuggestions (the dashboard now consumes the shared hook; behavior unchanged) so every filtered surface resolves values the same way.
The hand-rolled overlay becomes a real dialog (ui/dialog.tsx — focus trap, focus return, Esc, aria, sanctioned shadow), max-w-2xl with content-fit height and sentence-case copy. The step editor gets square number chips, Page|Event as a segmented control, the app Select for matchers and property operators, 32px reorder targets and height-morph on add/remove. Path and event inputs gain anchored suggestion panels fed once per open from getDashboardPages/getDashboardGoals (kind glyphs, counts, arrow keys, free text always allowed, Escape closes the panel without killing the dialog, 150ms spinner rule). The conversion-window control (number + unit + 24h/72h/7d/14d/30d presets, create default 7d, edit prefilled) submits exactly what it shows — the hardcoded 30-day overwrite that silently rewrote every edited funnel's window is dead. Validation is inline with field errors and focus-to-field; toasts only fire on submit failures. funnels/new forwards ?prefill= and the list page auto-opens a seeded create modal.
The journeys lens chip gains a one-click 'Create funnel from this path' — spineThrough (tested) walks the heaviest hop in both directions from the lens (skipping (other), capped at six steps) and lands on the funnels page with the create modal prefilled via ?prefill=. In the other direction, the funnel step strip links exact page steps to journeys entered from that page.
Six Unreleased → Improved entries: the real funnel detail page, the window-overwrite fix, step suggestions, the one-row journeys toolbar, the shareable lens with cross-links, and the zero-flicker loading discipline.
Radix's escape listener runs on document capture, so it dismissed the whole dialog before the panel's bubble-phase handler could claim the key. An open-panel counter now feeds DialogContent's onEscapeKeyDown: first Escape closes the panel, the next one closes the dialog.
…tion input Blur schedules a 120ms delayed close so row clicks land before the panel unmounts — but refocusing the same input inside that window let the stale timer close the freshly opened panel. Focus now clears it.
The backend filter param threads through lib/api/journeys + the SWR hooks, and useJourneyFilters gains the dashboard ?filters= codec (parsed/serialized identically, cleared by Reset, counted in isDefault). The toolbar gains the dashboard FilterButton and a pills row, driving the same single-surface popover — DimensionStage takes an additive 'allowed' allowlist so journeys offers only Country/Device/Referrer while the dashboard is untouched. Suggestions reuse the shared useFilterSuggestions hook, and a distinct filtered-empty state covers the no-match case. Combined lens+filter state is fully URL-shareable.
The step editor was a jagged stack of bordered cards — the Page/Event toggle and the matcher select sat on different rows at different widths, so the name and path inputs never lined up, and a low-value 'Step name' field (never in the approved design) padded every step. Steps are now single aligned rows — number chip, Page|Event, matcher, value input, then reorder and delete — with property filters indented beneath event steps. The step label auto-derives from its target (custom names on older funnels are preserved on edit). The conversion-window number input drops from full-width to w-20 and sits on one row with the unit select and preset chips behind a divider.
inputClass leads with w-full, which beats a plain appended w-20 (Tailwind resolves same-property utilities by stylesheet order), so the window number input rendered full-width and pushed the unit and presets to a second row. !w-20 forces the compact width; the input, unit select and preset chips now sit on one line.
…ndard The input-variant Select had three latent problems that broke it inside dialogs: min-w-[140px] overflowed narrow slots (overlapping siblings), h-11 didn't match the app's h-10 controls, and the absolute-positioned dropdown was clipped by any scrollable ancestor (so it vanished inside the funnel modal). The dropdown now portals to the body with fixed positioning (measured from the trigger, flips up near the viewport bottom, repositions on scroll/resize), the input variant is h-10 with min-w-0 and the ring-2 brand-orange focus the rest of the controls use, and options/labels truncate. Applies to all 18 Select usages.
Every control is now h-10, border-border, rounded-none, with the same ring-2 brand-orange focus — no more mixed h-11 selects, h-8 chips, and three different focus rings. The window number input loses its native browser spinner, the matcher/operator selects are sized to fit (no overlap now that Select respects its slot), preset chips are h-10 to match the row, and the suggestion panels carry the sanctioned shadow.
… on screen Hovering a row highlighted every occurrence of its path across all steps, because it seeded the chain from chainThrough(path) (all occurrences). Hover now seeds from the specific step:path node (chainThroughNode), so hovering /relay at step 2 lights up only its own flow; the pinned lens keeps its deliberate trace-the-page-everywhere behaviour. The flow tooltip flips to the left of the cursor in the right portion of the viewport so it no longer runs off the last column, and its text truncates.
The columns view stays readable at ten rows per step regardless of the Paths density; the flow view remains the detailed lens for higher counts.
The conversion trend is now just the overall orange series — the per-step multi-series toggles were janky and the step drill-down already lives in the strip above.
…he url date-range hook
…uests column, note all-zero error periods
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
keepPreviousDataeverywhere, first-load-only skeletons, explicit error cards with retry, settled-only empty states, URL-synced state.Changes
Behavior, Search & CDN facet treatment (05-07, 9 commits —
9c71d883..f79ef47a)?page=lens filtering the rage/dead element tables, real daily trend chart (new backend endpoint), scroll-depth rails replacing the radar chart (drops recharts from the route), view-all dialogs on the shared dialog primitive.useUrlDateRange(renamed fromuseFunnelDateRange) on all date-ranged pages;keepPreviousDataon all 12 GSC/Bunny hooks;useSitereplaces full-dashboard fetches for document titles.Funnels & Journeys rebuild (04-07, 24 commits —
8be2614f..ed1cd250)?lens=selection across both views, ambient transition ribbons, declarative React-SVG flow view, country/device/referrer dimension filters, zero-flicker loading.[funnelId]detail route (KPI band, funnel canvas with connecting bands, step drill-down with exits + breakdowns, conversion trend chart, dashboard filters), summary-row list with real step targets and sparklines, modal rebuilt on the dialog primitive with suggestions and a conversion-window control (kills the hardcoded 30d overwrite bug).Dashboard filter builder (04-07, 11 commits —
a098431f..fbf3454e)Settings / admin / billing / marketing polish (June, ~35 commits)
/switchpreselects the current interval, tiny-base KPI delta guard, admin/setup chrome recolored to brand orange, compare-table sentiment colors, sw.js excluded from the auth matcher, facet 0.2.2 + support-widget 0.10.0.Test Plan
f79ef47a: tsc 0 errors · build exit 0 · 226 vitest passingciphera-net/pulse-backendstaging → main alongside this PR (the behavior trend chart reads the new/frustration/dailyendpoint; journeys filters read the extended transitions endpoints)