Skip to content

Changelog

Every release, dated. If it isn't here, it didn't ship.

2026-10-03 — Launch check: billing refusals and visitor disconnects

  • A refused Stripe call is a sentence, not a crash. Checkout and the billing portal raise BillingUnavailable when Stripe refuses (most likely: no Customer portal configuration saved in live mode, which Stripe reports today), and the routes answer 502 with a line pointing to hello@backwire.io instead of a 500 and a stack trace.
  • A visitor who leaves mid-beacon is a quiet 400. ClientDisconnect while reading a page-view body was logged as a 500 with a full traceback several times a day, burying real errors.

2026-09-26 — Layout pass: tablets, phones, and two charts that told the wrong story

  • Tablets get the full width. The dashboard sidebar stayed open down to 768px and left laptop layouts a third of the screen: the report pane showed one character per line and every multi-column strip collapsed. It is now a menu below 1024px (nav-drawer.tsx, shell.tsx).
  • Live Activity bars are proportional. Each bar was a share of whatever width its label left, so a long page name shortened its bar and equal counts drew unequal bars. Label, bar track and count are now three shared columns; verified at three widths with zero ratio error (charts/bar-list.tsx).
  • Insights → Pages keeps its page names. Five fixed 112px number columns left the path column nothing below ~600px, so rows had numbers and no page. The table now has a floor and scrolls sideways inside its card.
  • The report pane fits. The close button no longer drops to its own row, the visit facts split into two columns by the pane's width rather than the screen's, and the timeline's dots stay on the first line with text wrapping at spaces instead of mid-word.
  • The 404 page's main button has a label (prose link styles were painting it teal on teal), docs code blocks scroll rather than wrap, the Widget settings reference stacks into readable blocks on phones, and marketing tables no longer split words ("Unlimi/ted", "first_show_/delay_ms").
  • Smaller fixes: the site icon falls back to its letter instead of a broken image; display text no longer spaces commas like figures ("12 , 480"); pricing is one aligned column; the footer rule runs full width; Home's attention chips line up; stacked Live feed shows eight rows and "Show all"; bubble previews, demo tab bars and the problems table fit a phone; checkboxes and radios take the accent; the inbox checkbox has a real tap target and the keyboard hint hides on touch screens.
  • A flaky end-to-end assertion (-4.1s vs -4.2s under load) now accepts either.

2026-09-23 — Beacons survive odd values, www is the same site, unknown paths land on a 404

  • A page-view beacon is clamped, never rejected. A backgrounded tab reporting a load time past the 10-minute cap used to fail validation and lose the whole visit — a few an hour on a live site. Out-of-range numbers are clamped to their bounds and over-long strings truncated (api/app/schemas.py::EVENT_BOUNDS, EVENT_LENGTHS); an unknown event type is still refused, because that is a caller bug rather than a stray reading.
  • www counts as the same site. A project that lists example.com now accepts www.example.com, and the reverse. Listing only one of the pair was the commonest install mistake; no other subdomain is implied, and *.example.com still means what it did.
  • The beacon rate limit is 240 a minute per visitor address, up from 60. One person cannot approach either number, but an office, a campus or a mobile carrier puts hundreds of real visitors behind a single address, and they were losing page views to 429s.
  • An unknown path renders a not-found page. The session proxy guards the dashboard (/app, /p/, /settings, /onboarding, /billing/, /api/icon, /api/export) rather than allow-listing the public site, so an old link or a typo no longer bounces a signed-out visitor to the sign-in form. Every dashboard page and route already checked its own session, so the proxy stays defence in depth.
  • robots.txt and sitemap.xml exist. Both were 404s. The sitemap is generated from one list (web/lib/site.ts) that web/test/site.test.ts checks against the routes on disk and against the docs slugs the [slug] page actually renders, so a new page cannot quietly go unlisted.
  • The privacy page matches the widget again. It claimed "no cross-page sessions" while widget 1.5.2 keeps the landing page's campaign tags and referring site in the tab's own session storage. A new section says exactly what a tab remembers, why, that neither value is an identifier, and that both die with the tab.

2026-09-21 — Rating emails can be switched off; a phone step for the type scale

  • Include ratings joins Email the owners under Project Settings → Notifications. Off means a rating earns no owner email anywhere: not the immediate alert for 1 and 2 stars, not its line in the 9am digest. Bug reports and ideas are untouched, and every rating is still in the inbox, the insights and the Monday summary. Stored as notify_ratings (default on), Pro-only like the switch above it, dashboard-only so it never reaches the widget's public config. Slack and the webhook are separate channels and ignore it — a site that mutes the rating emails still sees ratings in its channel. api/app/notify.py::should_email_owner is the one gate, read by the submit path and the digest alike.
  • The trial's "Pro features you used" list no longer counts a notification switch that is simply on: only turning notify_email or notify_ratings off is using the gate.
  • The type scale gained a phone step (@media (max-width: 767px)): the same eight names, each 1–3px smaller — 15px body → 14px, 31px tile number → 28px. It is one scale, not two, so no component opts in. This meant moving the scale out of @theme inline, which bakes literal pixel values into every text-* utility and would have left the media query reaching only the stylesheets. web/test/type-scale.test.ts now holds both the steps and the phone step's bounds.

2026-09-20 — Live Activity: full paths, copy the page, shaded countries, US states

  • The user snapshot rows show the whole path (wrapping instead of truncating and side-scrolling), then who and what they were on, then two actions: Copy URL and Open page (built from the project's first allowed domain). A row also says which state a United States visitor was in and which campaign source (utm_source) or referrer brought them.
  • The world map shades whole countries by page views instead of placing a dot; the five busiest carry a count; every country names itself on hover. When state data is present, a United States inset appears under it, shaded the same way (web/lib/world/us.json, built by scripts/build-us.mjs from Natural Earth 110m admin-1, Albers USA projection).
  • The API's live feed carries source and region per event and a regions breakdown (United States only). The region is the ISO 3166-2 code from Cloudflare's cf-region-code header (X-Region as an override), which the zone adds once Rules → Settings → Managed Transforms → "Add visitor location headers" is on. Nothing is stored beyond the 15-minute in-memory window.

2026-09-20 — Where a visit came from, on every report

  • Most visits carry no utm tags, so the report's Page and campaign block read "Not recorded" four times. It now reads the referrer the way Google Analytics does: a search engine is Source google.com / Medium organic, a social network is social, any other site is referral, and no referrer (or a page on the same site, shown as its path) is (direct) / (none); Campaign says "Not tagged". Tags still win when present. web/lib/visit.ts::attribution, unit-tested.
  • The widget keeps first-touch attribution for the tab (fbw:landing in sessionStorage — the utm tags and external referrer of the page the visitor arrived on, no identifier, gone with the tab), so a report sent two pages later still carries them; a page that arrives with its own tags or from another site starts a new touch. Widget 1.5.2, 15.6 KB gzipped.

2026-09-20 — The owner alert is an HTML email

  • The immediate alert for a bug report or a low rating now goes out in the house layout beside its plain text: the project as a kicker, the kind and path as the headline, the reporter's message quoted, a details table (page, title, device and country, time, whether the reporter can be reached), the Open in dashboard button, and a footer naming the project. No signoff — it is a notification, not a letter — and never the reporter's address. Goldens in api/tests/golden/mail/alert_*.html. mailhtml grew heading, quote, details and a signoff switch on layout.

2026-09-20 — The Account page reconciles with Stripe

  • An org that checked out but was never linked to a Stripe customer now asks Stripe for its subscription when the Account page loads (GET /v1/billing → sync_org): Checkout tags the subscription with the org id, so a lost webhook, or a 100%-off checkout that leaves no payment to notice, no longer strands the account on Free. The adopted subscription is applied like any webhook and recorded in billing_events under sync_<subscription>_<time>. Bounded to free orgs with no customer id, so linked accounts never trigger a Stripe call.

2026-09-20 — Stripe events parse; the widget page saves on Free; the Pro badge is a badge

  • The webhook handler crashed on every real Stripe event with a TypeError: stripe-python 15 hands back an Event object that is neither iterable nor a mapping, and the tests only ever used the fake gateway's dicts. parse_event now returns event.to_dict(), with the verified body as fallback; a test signs a real payload and runs it through the real SDK. Stripe's own retries of the first live checkout will succeed from here.
  • Widget Customization could not be saved on the free plan: the device boxes sit in a disabled fieldset, so they never reach the form data, and validation rejected the save as "no devices" with a message at the foot of a long page. A free save now leaves the Pro-only keys (devices, delay, hide on scroll, colors, footer) as stored, which the API neutralises anyway; the outcome, error or "Saved.", shows beside the Save button.
  • The "Pro" pill is solid accent, and a locked section's banner opens with "Pro plan." on an accent-edged tint.

2026-09-19 — Stripe webhook answers at both paths

  • POST /webhooks/stripe (the path Stripe's dashboard suggests when an endpoint is registered) now reaches the same handler as POST /v1/billing/webhook. The first live Pro checkout's events had been landing on the old path and getting 404s; those deliveries can be resent from Stripe.

2026-09-19 — Home without the Getting started strip

  • The "Getting started" checklist (widget installed, first open, first report, first prompt copied) no longer shows on Home; onboarding covers those steps. The API still computes the signal; the dashboard skips it.

2026-09-19 — Onboarding redesigned

  • The wizard is a tinted step spine (accent markers, ticks for done steps, the exits at the foot) beside a centred 720px pane. Step 1 is the project form in a card. Steps 2 and 3 are numbered cards: Paste the tag (framework tabs and the snippet), Open your site (the live check) and Send a test report (appears once the tag has loaded), with the actions under them. The snippet block wraps long lines and keeps its Copy button in a header bar instead of over the code; the Install page shares it. Works at phone width.

2026-09-19 — Email links survive a preview

  • /auth/confirm is a page that verifies the one-time token on a form submit (it submits itself once loaded, and has a button for no-JavaScript), never on the GET a mail client's link preview performs, so the token is still valid when the person arrives. A token that was already used still lands in the app when a session exists. The error page tells an already-signed-in visitor so and offers Sign in / Reset password otherwise.

2026-09-19 — Sign-up reachable when signed out

  • In production the session proxy sent signed-out visitors to /login from every path outside an allow-list that predated the new pages, so /signup, /forgot-password and the reporter's /r/<token> status page all bounced to sign-in. The allow-list is now lib/public-paths.ts, unit-tested, and includes them. Dev mode skips the proxy, which is why the e2e suite never saw it.

2026-09-19 — The name is Backwire

  • The product name is written "Backwire" everywhere: site, dashboard, widget labels ("Powered by Backwire"), emails, auth templates (re-pushed to Supabase), docs and tests. The webhook headers are X-Backwire-Event, X-Backwire-Delivery and X-Backwire-Signature (no consumers exist yet; the docs page shows the new names). Widget 1.5.1 carries the label change; archived releases keep their bytes.

2026-09-19 — No npm package

  • The widget is delivered by the CDN script tag only. The npm route is gone from the home page, the Install page's React snippet (which loads the CDN script), the docs and the feature inventory; widget/package.json is marked private and the publish scaffolding (types build, README, licence, publish config) is removed. The @backwire scope on npm is owned by another account, and the script tag is what people use.

2026-09-19 — Landing-page claims audited against the code

  • Every claim on the home page, the plan cards and the /pricing table was checked against what is built. Three did not hold and are corrected: npm i @backwire/widget is not published, so the home page's install step and the Install page's React snippet use the script tag (the package stays on the list to publish); "choose who is notified" / "recipient control" described a per-recipient setting that does not exist — the Pro row now says "Slack notifications, and email alerts you can switch off", which is what Project Settings offers; "three tabs" in How it works is "two tabs" since the widget became Rate + Feedback. Everything else checked out: trail caps 5/20/20/50, redaction on both sides, c to copy and .md download, campaign sources, the 15-minute live view, 33 languages, bubble styles, page/device/timing rules, idle loading.

2026-09-19 — The first project is the welcome; one reminder after 30 minutes

  • Sign-up itself sends nothing from us: Supabase's confirmation is the first touch, so nobody gets two emails in ten minutes. The first project's "site is ready" email doubles as the welcome ("Welcome to Backwire — <name> is ready": you're in, the tag, the install page, what the first report brings, Free vs Pro, reply to a person). Later projects get the plain note.
  • An account that signed up over 30 minutes ago and has no site gets one reminder from the hourly jobs, to its owners, once ever ("You haven't added a site yet", ending "This is the only reminder we send"). orgs.welcomed_at is the marker either email sets; existing accounts were marked by the migration, so none of them is reminded. The greeting uses the owner's stored name, read through the Supabase admin API in the job.
  • No welcome at first sign-in any more. Golden files: project_first, project_ready, reminder, text and HTML.

2026-09-19 — One look for every email

  • supabase/templates/ holds the five Supabase Auth emails (confirm sign-up, sign-in link, password reset, email change, invite) in the same layout as the API's own emails; each links to /auth/confirm?token_hash=…&type=…, the route the app verifies. supabase/config.toml maps them for local runs; applied to the hosted project 2026-09-19 with supabase config push from the minimal supabase/templates/hosted.toml, which also adds /auth/reset to the redirect list (README in the folder).
  • The daily digest, the weekly summary and the trial warning now carry an HTML twin of their plain text (api/app/mailhtml.py::plain_to_html): same words, project names as headings, links clickable, the shared layout with a footer that says why and where to turn it off. app/mailhtml.py is the one place the layout lives; the welcome and site-ready emails build on it.

2026-09-19 — Welcome and "your site is ready" emails from hello@backwire.io

  • The first sign-in after sign-up sends a welcome, once per account (orgs.welcomed_at, migration 20260918010000; existing accounts are marked as welcomed so nobody gets one late). It greets the person by their stored name, points at onboarding, says what Free covers and what Pro costs, and invites a reply.
  • Every project created sends "<name> is ready for the widget" to whoever created it: the script tag for their domain, the install page, and the inbox link.
  • Both are sent as "Kevin from Backwire" and signed Kevin, with a light HTML layout beside the plain text: the lockup, one accent colour, a button for the main link and a code block for the tag; no tracking. Text and HTML bodies are golden files.
  • Both go through Resend from hello@backwire.io with replies to the same address (HELLO_EMAIL, CDN_URL settings; defaults are the production values). The bodies are golden files under api/tests/golden/mail/. The mailer takes an optional sender address on the same DKIM domain, and the API's User carries full_name from the JWT for the greeting.

2026-09-18 — Account Settings layout

  • The Billing card says "You're on Free" and lists what Pro adds as ticked bullets from the shared plan config (the same rows as the pricing page), with the Upgrade button under them; the Plan card is one line. Save and Rename sit beside their inputs with the hint under both. The rail's "Create your first project" is a button, not an underlined link.

2026-09-18 — Onboarding has exits

  • The onboarding wizard has no rail, so it now carries its own Sign out button and an Account Settings link. "Do this later" shows only when there is an inbox to go to: an account with no project used to be sent from it straight back to onboarding, with no way out.

2026-09-18 — Start with Pro: pay right after sign-up

  • "Start with Pro" links to /signup?plan=pro. The sign-up page says what that means (Pro, $19 a month, card details after email confirmation, Free still available) and carries the choice into the form and the Google button; the account action stores it in a short-lived cookie, since the confirmation link cannot carry it.
  • The first landing in the app with that cookie goes through /billing/start, which drops the cookie and sends the browser to Stripe Checkout, returning to /app. Already on Pro, no Stripe keys, or a refused Checkout all continue into onboarding on Free with a line saying so; a successful payment lands on onboarding with "You're on Pro. Now the first site."
  • POST /v1/billing/checkout takes an optional return_to dashboard path (validated: same site only); the default is still Account Settings.

2026-09-18 — Monthly only

  • The $180-a-year price is gone from the code and the copy: the pricing table's Price row, the site-limit notice, Account Settings and the Checkout route all know one price, $19 a month. STRIPE_PRICE_YEARLY is no longer read. There is no trial and nothing on the site says otherwise.

2026-09-18 — Account Settings: your name

  • The Account card starts with "Your name": the display name stored as full_name in the Supabase user metadata (sign-up already sets it). The rail's account menu reads it from the session instead of deriving a name from the email, so it shows what you typed; the email-derived name remains the fallback for accounts without one. Saving refreshes the session so the new name appears at once. The account's own name sits under it with a line saying what it is for.

2026-09-18 — Stripe: Checkout, Customer Portal and the plan webhooks

  • Phase 2 billing is unlocked. POST /v1/billing/checkout opens a Stripe Checkout session for the Pro price (monthly or yearly), reusing the org's Stripe customer when there is one and allowing promotion codes; POST /v1/billing/portal opens the Customer Portal; GET /v1/billing tells the dashboard what is configured.
  • POST /v1/billing/webhook verifies the signature and applies checkout.session.completed (Pro, customer id stored), customer.subscription.updated (active → Pro; cancel at period end → paid until then; past_due → seven-day grace; canceled → Free), customer.subscription.deleted (Free), invoice.payment_failed (seven-day grace) and invoice.paid (grace cleared). Each event is recorded once in billing_events by Stripe event id, so retries are no-ops; the stored payload is identifiers only.
  • Account Settings: the Billing card offers Upgrade to Pro (with the yearly price when configured), Manage billing once a subscription exists, and the success / cancelled notes after Checkout. Without Stripe keys it says billing is not set up.
  • Stripe is behind a gateway; the API tests use a fake and cover every event mapping (12 tests). New env: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_MONTHLY, STRIPE_PRICE_YEARLY.

2026-09-18 — CI: the web job typechecks again on a fresh checkout

  • pnpm typecheck had failed on every CI run since at least 2026-09-13: the generated, untracked next-env.d.ts is what declares *.png imports, and a fresh checkout has none, so the logo component's three image imports had no types. web/types/next-globals.d.ts now carries the same references in the repo. Verified in a clean clone: typecheck and lint pass. The e2e step runs in CI for the first time as a result.
  • Sign-in and sign-up show Continue with Google only when the Google provider is switched on in the Supabase project (read from Auth's public settings, cached five minutes); the live project has only email enabled today, so the button is hidden there until Google is configured.
  • Migration 20260918000000_revoke_rls_auto_enable.sql revokes EXECUTE on the Supabase-managed rls_auto_enable() helper from anon/authenticated, which the security advisor flagged. Apply with supabase db push.
  • Railway jobs: the crash emails were the hourly runs before the two migrations were applied (column orgs.trial_ends_at does not exist); the 07:03 UTC run after them completed normally.

2026-09-18 — New projects get the current widget: Rate + Feedback, teal

  • The Widget Customization preview now takes its first settings from the form rather than from what the project stored, so a project that never saved shows the same two tabs and teal the form describes instead of the bundle's own fallbacks. With nothing stored, the type switches read as Rate + Feedback; Idea only comes back from a stored full, and unchecking every type falls back to the pair, never to three.
  • Project Settings: the Delete project card sits in the left column under Widget controls instead of in a row of its own below both columns. Its fields belong to a separate form through the form attribute, so Save still never touches it.
  • The API's default settings were still the old three-tab layout (Rate, Idea, Problem) in the old blue, so a project created today, like the public demo, served three tabs. The defaults are now mode: two with rating and bug, and accent #007F78. A project that stored full keeps its Idea tab until the owner next saves. The dashboard's accent fallback and the dev project match.

2026-09-18 — Sign in, sign up and the create-account flow as a standard SaaS flow

  • /signup is its own page: Google, or name + email + password, with the terms line under the button and a three-step progress strip (Account · Your site · Install) that the onboarding wizard continues, showing "Create your account" as done for a first-time owner. After submitting, a "Check your email" state with a Resend button.
  • /login is password first (with Forgot password? beside the label and a show/hide toggle), Google above, and the email sign-in link one click below instead of a method tab bar. /forgot-password is its own page; an expired reset link lands there with a message.
  • The auth frame is light whatever the OS or saved theme says (.force-light re-declares the light tokens): the lockup above the form on the left, and on the right a centered panel with what the dashboard shows on the demo data (the trail before a report, the pages with errors nobody reported, the page counts). Footer: © 2026 Backwire · Privacy · Terms. Every marketing call to action goes to /signup; Sign in goes to /login.

2026-09-18 — Home review round three: Pro, pill eyebrow, card buttons, footer

  • The paid plan is called Pro everywhere it is shown: the home page, /pricing, the dashboard's upgrade notes and gates, the API's error messages and the trial email. The stored plan value is unchanged (startup), so no migration.
  • Plan cards put the button at the bottom, pinned to the card's edge so both line up: "Try for Free" and "Start with Pro".
  • Hero: the eyebrow is a pill; the two panels still take turns but the Visitor feedback / Page insights cards under them are gone, so the hero is shorter; the caption reads "Start for free with one site. One script tag works on plain HTML, …".
  • Plan rows in plain SaaS wording: "14-day data retention", "Cookieless page analytics and live view", "Webhooks and GitHub integration", and so on. On Free, "Remove the “Powered by Backwire” badge" is listed with a dash and a Pro tag, so nobody assumes it.
  • The trust block's closing line ("Analytics are daily counts, not people…") is gone. The footer's Product and Company columns sit beside the brand block on the left instead of against the right edge.

2026-09-18 — Home review round two: fixed-height hero, section backgrounds, footer

  • The hero's two panels are the same height, so switching never moves the page; the report panel now carries its visit facts (device, browser, screen, language, time zone, timing) rather than empty space. "Start for free" is the wording on the header, the hero and the Free card.
  • The four proof cards are their own section. The prompt block is shorter with a fade. Every section has a different background from its neighbours (a three-tone rotation of white, canvas and surface), so nothing blends. The trust bullet reads "The errors, failed requests and console lines before the report".
  • Plan cards put the button beside the price instead of on its own row. The closing "Put it on your site tonight" section is gone. The footer is a brand line, Product and Company columns, and "© 2026 Backwire. Made in California." — no company name or city.

2026-09-18 — Home review round: the report alone in the hero, no snippet, no yearly, no Coming

  • Hero. The report shows on its own (the inbox list beside it is gone) so it reads at hero size; the stage sizes to whichever panel is showing. "Start free" goes to the pricing section, where the plan cards lead to sign-up; "See it working on this page" is a button. The embed snippet is gone from the hero and the closing call to action, so nobody pastes a placeholder key and expects it to work; the install page and docs carry the real tag.
  • Proof strip is four cards with a stroke icon each. Problems table loses the "Nobody reported it" column, which repeated the Bug reports column; a zero beside errors is marked in amber instead.
  • A Page insights section joins the features, with the dashboard's tiles, chart and pages table on the demo data. Section headings sit on one line where they fit; "What Backwire collects, and what it never does." is a section title above the two columns.
  • Pricing is monthly only, the feature lists carry ticks, the "Coming to Startup" strip is gone, and the FAQ has a title, "Common questions".

2026-09-18 — No Startup trial; the hero shows both faces of the product

  • The 14-day Startup trial is withdrawn. New accounts start on Free; the signup trigger is the one from the init migration again. The trial line is gone from the hero, the pricing cards ("Choose Startup"), the FAQ (which now says there is no trial and why that is fine), /pricing, /privacy, onboarding and Account. The plan model keeps the optional courtesy-period clock for billing to use; nothing starts one on its own.
  • The hero switches between visitor feedback and Page insights. A second panel, built from the dashboard's cards and tiles on the demo site's two weeks of traffic, crossfades with the inbox every six seconds. Hovering, focusing or picking a tab holds the current one; reduced motion turns the automatic switch off. Both panels lay out at phone width.

2026-09-17 — The home page, rebuilt from the landing-page spec

  • / is rebuilt from docs/landing/backwire-landing-page.md (section order, copy and structure) with docs/landing/landing-mockup.html as the wireframe and the site's own tokens, fonts, colors and radii throughout: hero with the doc's headline "Feedback that wires back to you." and the embed snippet; a four-fact proof strip; four alternating feature sections; three numbered steps; the collects / never block; pricing with the monthly/yearly switch, the Startup card marked Most popular, the "Coming to Startup" strip with quarters and no ticks, and the six-question FAQ; the closing call to action; a two-column footer.
  • Every product image is the real component on sample data, not a screenshot: the hero is the inbox's own rows and the report header with a working Copy as agent prompt; §3.1 is the inbox's Before-the-report timeline; §3.2 is the renderer's verbatim prompt; §3.3 is Home's problems table and the lowest-rated pages in the dashboard's cards. The fixtures (lib/marketing/demo-data.ts) are the Northwind Supply set the demo project is seeded with, timed from the request so the inbox reads "2m" rather than ageing since the build; the page renders per request for that reason.
  • The page runs the real widget with the demo key, and "See it working on this page" and the closing line call window.backwire.open(), falling back to the demo inbox where the bundle has not booted.
  • Pricing renders from the shared plan config (lib/marketing/plans.ts), which /pricing shares; the FAQ and the Coming strip come from the same file. Three rows in the doc were not carried because the product does not ship them: "Screenshot with reports", "Linear issues", "scheduled export". The proof strip's "12 KB" and "461 tests" were replaced with the built figures (15 KB, 771), kept as constants next to the plans.
  • The header nav is Features · Pricing · Docs · Changelog; the footer is the doc's Product / Company columns. The interim /v2 page and its styles are gone. The landing e2e suite is rewritten for the new copy.
  • Dashboard components embedded in the light marketing page keep the light tokens (.mk-embed) whatever the visitor's system theme says, and the marketing heading and table rules stop at the embed's edge.

2026-09-17 — A six-section home at /v2, beside the current one

  • /v2 is the proposed home page, built for comparison and marked noindex: one claim and one real screenshot per section — what it is and who it is for (the widget beside the report it makes), the handoff (the report's trail beside the brief in an editor, verbatim from the prompt renderer), the errors nobody reported (Home's problems table with its Copy prompt buttons), analytics in brief, install with the live dot, and a price-and-privacy strip with the closing ask. Plain words throughout, written to get a sign-up rather than to pre-empt a sceptic.
  • Two new marketing captures from the demo fixtures (scripts/marketing-shots.mjs): problems.png and install.png; the seeded errors now carry a sample so the table reads as it would on a real site.
  • The review that led here is in the conversation log, not the repo: the current home leads with the least distinctive screen, its phone hero image is lazy-loaded into a blank card, and a quarter of its length addresses a sceptic rather than a buyer. Nothing on / changed.

2026-09-17 — Full-width homepage pricing and FAQ

  • Removed the narrow internal width caps from the homepage pricing grid and FAQ. Both now align with the surrounding content container; plan cards remain stacked on small screens and FAQ answers retain a readable text measure.

2026-09-17 — Marketing homepage redesign

  • Implemented the centered hero, product-proof sections, dark report-to-prompt workflow, three-step installation, and mint closing CTA. Product imagery is freshly captured from actual dashboard and widget routes with labeled demo data, not generated UI.
  • Retained shared pricing and factual privacy/FAQ content; removed the stale homepage retention number and qualified report diagnostics by capture settings. Added capture provenance and feature evidence in docs/marketing-capture-verification.md.

2026-09-17 — Pricing tiers: Free and Startup, the trial, and gates that never touch a live widget

  • Plans. orgs.plan gains startup_trial with trial_ends_at; every gate reads effective_plan() — a live trial is Startup, a lapsed one is Free, and plan_ends_at lets billing keep a cancelled plan paid to its end. New accounts start on a 14-day Startup trial from the signup trigger. /v1/me and /v1/org return the effective plan plus stored_plan and the dates.
  • One site on Free, five on Startup. POST /v1/projects answers 402 with a plan_limit body; onboarding shows "Free includes one site. Startup includes five." with a link to plans. A site beyond the plan's count (a lapsed trial's second site) keeps collecting and shows "Upgrade to view" on Home, Inbox and Page insights; nothing is deleted.
  • The widget is never degraded. GET /v1/config applies the plan when it is served, so a downgrade keeps the owner's settings in the database and serves the free plan's values: the footer link returns, brand colors and translations pause, the bubble shows on every device. Re-upgrading restores everything instantly.
  • What is free moved. Show/hide by path is free (it was Startup); device targeting, first-show delay and hide on scroll are Startup. The reporter's receipt, "fixed" email and status page are free. GitHub issues, metadata in the prompt and on the report, reports by release and the Monday summary are Startup — the widget still accepts metadata on any plan. The Data card on Project Settings is no longer locked on Free: export is once a day there (429 after that), unlimited on Startup.
  • Retention is 14 days of technical context on Free and a year on Startup, read from the plan by the retention job (it was 30 days for free and nothing for Startup). The report's context panel on Free says so past 14 days instead of showing an empty trail.
  • Trial state machine. run_trials (hourly, with the digest) emails owners "4 days left" once, naming only the Startup settings they actually changed, and flips a finished trial to Free.
  • Pricing page rebuilt: two columns with a monthly / yearly switch ($19 / $180), a "Coming to Startup — included when it ships" block with quarters (MCP, AI triage, teammates — Q4 2026), the row-by-row table, and a FAQ that opens with "What happens to my widget if I stop paying? Nothing." Account › Plan shows the plan, trial days left, the site count and the retention window.
  • Not done: Stripe (checkout, portal, the four webhooks, the grace period) stays Phase 2 and is the last launch-checklist item; screenshots stay Phase 2; the scheduled weekly export and Linear are P2 after launch. The published price has no checkout behind it yet — Account › Billing says to email.

2026-09-17 — Closing the loop, and what nobody reported

  • Owner metadata on reports. data-version="1.4.2" on the script tag, or window.backwire.set({ version, tenant, variant }), rides along as context.meta — at most ten short strings, redacted like the rest of the trail. The report panel lists them under App; the agent prompt has an App section and names the version in its first step. Reports by release: Page insights gains a By release table (GET /projects/{id}/releases) once any report has carried a version.
  • The reporter hears back. With an opted-in email, an optional receipt (reporter_receipt, off by default) and, when the owner marks the report fixed, the email the widget promised (reporter_fixed_email, on by default, once per report). Both go from Backwire's address as "<site> via Backwire" with the owner as Reply-To, and both carry a private link to /r/{status_token}: one page per report that shows the reporter their own words, the page, the reference and the status in plain language — and nothing about the visit. GET /v1/report/{token}; the id is not the token; spam is a 404.
  • A signed webhook per project (Startup). Every non-spam report and every status change is POSTed as JSON with X-Backwire-Event, X-Backwire-Delivery and an HMAC-SHA256 X-Backwire-Signature keyed by a whsec_ secret the API mints when a URL is first saved and shows beside the field. Payload: what the dashboard shows, the app metadata and the agent prompt — never the contact address, the hash or the trail. /docs/webhooks covers the payload, verification and the Zapier / Make catch-hook route.
  • Open GitHub issue beside Copy as agent prompt, once Project Settings names a repository (owner/name). A plain link to issues/new prefilled with a title from the message and the prompt as the body, cut to fit a URL with the report link last; opening it writes an opened_issue event.
  • Week in feedback, Mondays at 09:00 in the owner's timezone: per project, the week against the previous one, the average rating, the lowest-rated pages, pages with errors nobody reported, and what is still new or awaiting a reply. Built from the same aggregation as Home. Quiet weeks send nothing.
  • "3 other people reported a problem on this page today." POST /v1/submit answers with others_today; after a problem report the widget shows the count as one line (two new strings, editable per language). A count only.
  • Rage and dead clicks, once per page view: three clicks on one element inside two seconds, or a link or button that changed nothing within a second (links that leave the page are not judged). Two more counters on page_analytics, a third technical-issue card on Page insights. No selector leaves the page.
  • What went wrong, by message. The widget now sends one sample with the first JavaScript error and the first failed request per view — the redacted message with source and line, or method, URL without query and status — and the API keeps one row per distinct error per project per day (error_samples, capped at 500 a day, 91-day retention, fingerprinted by kind, message, source and line). Page insights lists them most seen first with a Copy prompt on each; Home's "Nobody reported it" rows get the same button, taking the page's most-seen error. GET /projects/{id}/errors, POST /projects/{id}/errors/{fingerprint}/prompt. The "no error messages are stored" wording on /privacy, /docs/data and the claim register is retired accordingly.

2026-09-17 — An account, not an organization; projects go before the account does

  • Account Settings is written for one person: an Account card (name, and the email you sign in with), Plan, Billing, and Delete account. The Members card is gone until teams arrive with the Startup plan. Billing says what will be there — a button to the billing portal — rather than showing one that does nothing; that button is the Stripe integration.
  • A project can be deleted from the foot of Project Settings, with its name typed to confirm. DELETE /v1/projects/{id}, owners only; everything under the project goes with it.
  • The account only goes once its projects have. DELETE /v1/org answers 409 naming the projects that remain, and the Account page lists them with a link to each one's settings instead of showing the delete form.

2026-09-17 — Home's tiles follow the chart; Notifications and Data are Startup-only

  • On Home the five tiles sit under the per-day chart and the attention list, reading as a summary of what is above them rather than a row of numbers the chart then explains.
  • Notifications and Data and retention on Project Settings are Startup-only. On the free plan both cards are shown, badged and inert. The defaults still apply — owners are emailed about problems and low ratings and get the digest — but choosing who hears, Slack, and export come with the Startup plan. Enforced in the API: apply_plan drops notify_email and slack_webhook_url on the free plan, and GET /projects/{id}/export answers 403. /privacy and the claim register say export is Startup.
  • The Startup badge and the inert-section treatment are one shared component now (components/ui/plan-gate.tsx), used by Widget Customization and Project Settings.

2026-09-17 — Overview becomes Home; Install reads in order; Page insights fits its width

  • The Overview is Home. /p/<id> now leads with Site health, then the five tiles, the per-day chart beside the aged attention list, What changed, and the join table. /p/<id>/overview forwards to it, range and all. The old Home's tiles, Lowest-rated pages and Pages with most bug reports are retired — their figures live in the tiles and the join table.
  • Two costs flagged when the Overview was a preview are paid: submission events are fetched in one request per 200 reports (events_for) instead of one per report, and the 90-day submissions window pages past PostgREST's 1000-row cap instead of silently stopping there.
  • Install is two full-width steps: add the tag, confirm it loaded. The side column (allowed domains, where to go next) and the optional pin-a-version step are gone; domains live on Project Settings and the version-pinned tag stays published with each CDN release. The status dot is green and breathing while the widget is live, red and still while it is not — and still for anyone who asked for reduced motion.
  • Inbox is two panels from the start: the list on the left, the report on the right, which waits with "Nothing selected" until one is opened — so the page no longer re-flows from one column to two on the first click. Below the tablet width it is still the list, then the report.
  • --color-up and --color-down were never mapped into the Tailwind theme, so every text-up / bg-up in the app was silently unstyled. Mapped.
  • Page insights opens on page views rather than feedback submissions — the metric every page has, not the rarest one. The Pages card's ring is a fixed 260px column rather than 40% of the card, so paths no longer wrap; the "View all" modal is 1100px rather than 720px. Every data column in the breakdown tables is the same fixed width (60px in the third-width cards, 112px in the full tables), so figures line up down a card and across a row; those cards have one line per row, short headings, and the count alone — a 60px column has no room for the rate beside it, so the rate is the cell's title and is in "View all".

2026-09-17 — Overview: the joins and the deltas, as a preview beside Home

  • A new page at /p/<id>/overview, reached from a "Preview the new Home" link on Home. Home itself is unchanged. It keeps what Home had right — the per-day chart, Needs attention, Site health — and replaces the level-only tiles with what no other page can show:
  • GET /v1/projects/{id}/overview?days= is one aggregation over rows the other pages already store (api/app/overview.py); nothing new is measured.
  • The header's right-hand cluster is vertically centred on every page.

2026-09-16 — One Feedback box, a plan that means something, and one page header

  • Rate and Feedback, not Rate / Idea / Problem. The picker offers two kinds; Feedback is what Idea and Problem both were, stored as bug. A project with an Idea tab keeps it until its owner next saves.
  • Every submission carries the page's trail — the last console lines, failed requests, JavaScript errors and clicks — where it used to be problem reports and low ratings only. A five-star rating brings it too. The capture switches still decide what is in the trail, redaction still happens server-side, and the disclosure now appears on every screen that sends, because every screen sends it. /privacy, the home page and the claim register say so.
  • The free plan's limits are enforced, not just hidden. apply_plan in api/app/models.py normalises settings on the way into storage: the footer link stays on, the bubble shows everywhere on every device, brand colors are dropped, and only the English strings are kept. Hiding a control is not a gate — every one of these reaches the widget through a public endpoint. The dashboard shows each locked area with a Startup badge and a line saying what the free plan does instead.
  • Widget 1.4.4, 14,382 B gzipped.
  • Widget Customization, Project Settings and Account Settings take Home's page header: full width, one rule under it, the title and its line on the left and the actions on the right. Account Settings gets the two-column card layout as well.
  • The Position list drops “No bubble” — window.backwire.open() is still there for anyone who wants it. The two bubble offsets are columns of the same grid rather than two squeezed into one. “Add a language” is a control, not a band. The preview column comes in about 40px.
  • The dev API seeds a second org on the free plan (Free Site, token dev-free), so the locked state is reachable in dev and in the end-to-end tests; set bw_dev_user=free.
  • The site logo is rebuilt from the updated artwork in brand/backwire-logo.png.

2026-09-16 — Dashboard content widths

  • Home, Page Insights, and Live Activity now use full-width headers with centered content capped at 1320px. Project Settings retains its content cap while its header spans the available page width. Widget Customization is capped at 1440px for its three-column editor. Inbox keeps its split-pane workspace.

2026-09-16 — Project Settings organization

  • Added a bounded responsive layout, separate save toolbar and page introduction, visible descriptions and icons for the four settings groups, distinct notification/pause controls, compact retention rows, and a clear export action. Existing field names, save action, and export destination are preserved.

2026-09-16 — Clearer wording editor

  • Separated language tabs from the add-language controls and condensed fallback guidance. Each wording category now has its own bordered group, heading, and explanation. Field columns respond to the editor width rather than the viewport, keeping narrow layouts readable. Translation fields and saving are unchanged.

2026-09-16 — Widget Customization layout

  • Added a separate save toolbar with a working Preview shortcut, compact section navigation, grouped settings, and a sticky live preview. Three columns adapt to the editor’s available width; narrower screens retain every control in a stacked layout. The preview opens on the card and branding settings sit under Look.
  • Preserved settings serialization, translated wording, real widget preview, and saving. Verified typecheck, lint, 194 unit tests and focused browser coverage for settings round-trip, translations, and responsive layout.

2026-09-16 — US English, a picker in the field, and the Widget page redesigned

  • US English throughout: color, organization, gray. The advanced section reads "Advanced Color Setting".
  • The color picker moved inside the field it fills: a swatch on the left opens the system picker, and what it returns is written into the box as a hex code. Typing a hex moves the swatch; a three-digit hex expands for the swatch while the box keeps what was typed; anything else leaves the swatch on a neutral fallback.
  • The Widget page follows the mock-up. The jump list is a card with an icon per section and the accent on the current one; each section is its own card with a line saying what it is for; the feedback types are rows you click anywhere on, with the count of what is on beside the heading; the advanced colors are a collapsible card with Light mode and Dark mode side by side, a compact label-and-field row each, and a Reset colors button. The preview is a card of its own with the Desktop / Phone switch in its head.
  • Fixed while measuring: in the three-column layout the middle column can be under 240px inside the new cards, and the fixed-width language select slid out from under its card and over the preview.

2026-09-16 — Widget Customization: pickers instead of codes, and the plain box gets its top back

  • The plain box opens on its question. With no tabs, the row holding the close button still took its full height, pushing the only thing on the card 58px down. The row keeps its place in the flow, so it still scrolls with a long card, but takes no height; the question reserves the button's width so a long one never runs under it.
  • Colours have a picker. Accent and every advanced colour pair their hex box with a swatch that opens the system colour picker. The box still accepts a pasted brand hex — neither is the only way in — and it is still the field that is submitted.
  • The bubble can take its own colour. colors.light.bubble_bg and bubble_fg, per theme, for the bubble's background and its icon. Blank keeps today's behaviour, where the bubble follows the card.
  • Languages are picked by name. A list of thirty-three common languages replaces a box that wanted a raw BCP 47 tag, and the tabs read "Portuguese (Brazil)" rather than pt-BR. Anything outside the list still goes in by hand under "Another language…", which now sits beside the list rather than replacing it.
  • The language box was inside the form, so every character typed into it marked the page unsaved and rebuilt the preview. Its changes no longer bubble.
  • The language tabs wrap instead of scrolling, so a lone English tab no longer sits above a scrollbar track.
  • "Powered by Backwire" leads the page instead of closing it under Privacy and data. It is the one switch that changes what every visitor sees on every screen.
  • Widget 1.4.3, 14,400 B gzipped.

2026-09-16 — Pinned widget installs survive the next release

  • Every published bundle now lives in widget/releases/<version>/, committed, and scripts/cdn.mjs publishes that directory rather than only the version being built. Pages serves one deployment and nothing else, so each release had been taking the previous /v<x.y.z>/ path down with it — the path still answered 200, with the 136-byte index.html, which a pinned tag loads as a script and which fails its integrity check. The Install page offers exactly that pinned tag, under copy promising it keeps working.
  • 1.4.2's bytes were recovered from the live CDN and verified against the SRI it publishes, so pages pinned to 1.4.2 keep working across this release. 1.4.0 and 1.4.1 are gone — they were dropped by the releases after them, before the archive existed, and their bytes are not recoverable.
  • pnpm release archives a build, and both scripts refuse to publish bytes that disagree with what is already archived: a published version is immutable, so the fix is a version bump, never an edit.

2026-09-16 — Insights tables: the column edge is the figure its heading names

  • The conversion rate sat inline to the right of its count, so the column's right edge — the edge the heading is aligned to — was the rate, and the count floated between two columns. Under "Opens", 1 5% read as a 1 belonging to Page views and a 5% belonging to Opens. The rate is under its count now, in every breakdown card and in the Pages table, and "Fast load rate" stacks its average load time the same way.
  • Hovering a rate says what it is a share of ("14.6% of page views"), which nothing on the page said before.
  • Rows are 16px taller and the Countries card's horizontal overflow drops from 48px to 34px, since each cell no longer reserves five characters for the rate.

2026-09-16 — The export says what it carries

  • "Export all data (JSON)" became "Export reports and history (JSON)". The export carries the project, its reports and every owner action on them — not the automatic analytics, whose 91-day retention the same card lists two rows above the link. /privacy and the home page's owner-actions list said the same thing and now match, and the claim register has a row for it.
  • The two notification switches an owner actually touches are covered end to end for the first time (api/tests/test_notify_settings.py): a bug report emails the owners, unchecking the box suppresses it, a 5/5 rating is left for the digest, and a saved Slack webhook is posted to while no webhook means no call. Each gate was broken in turn to confirm the tests bite.

2026-09-16 — Project Settings fills the page; the live feed fades at its edge

  • Project Settings is one page again. The three tabs — Project, Notifications, Data — hid two thirds of a page that fits comfortably, and left the rest of the screen empty while you clicked between them. All four groups are cards in a two-column grid now, under the same sticky Save bar the Widget page uses, and the page runs the full width rather than a 1152px column with canvas to its right. Nothing about what is submitted changed: the tab panels were hidden, not unmounted, so the form always carried every field anyway.
  • The two columns end on the same line: each stretches to the row and its last card takes up the slack, which goes into that card's own rows rather than pooling under them — retention is three even bands over a footer holding the export link.
  • The user snapshot's list is cut mid-row wherever the column ends. It now fades into the card's edge there, so the clipped row reads as more below rather than a broken edge.

2026-09-15 — Live Activity: ranked breakdowns and a readable feed

  • The user snapshot now matches the height of the map and chart beside it instead of collapsing to its content: side by side the card is taken out of flow and stretched to the track, so a quiet fifteen minutes no longer leaves a column of empty canvas, and a busy one no longer makes the row taller than everything next to it. Stacked below 1280px it is back in flow and grows with the page.
  • The rail's logo sat 8px to the left of the project icon and the nav icons and crowded the switcher. It is 28px now — the top of the brand kit's header range — indented to the same column as everything else in the rail, with room above and below. The loading skeleton moves with it so nothing jumps on navigation.
  • The four rows of pills under the chart — pages, countries, devices, referrers — become four ranked cards across the full width, each a label, a proportional bar and a count. It is the row Home and Page insights already use, so a share is readable without doing arithmetic; a list cut short says “Top 5 of 9”. The pill rows wrapped raggedly, showed no proportion, and left most of the card's width empty.
  • The user snapshot leads with the page. Country, device and what the visitor did follow on one line that truncates as a whole, with widget opens and feedback submissions in the accent. Every row used to open with the same thirty characters — “A visitor from … on desktop …” — and wrap to three lines. That sentence is still each row's screen-reader text, since it is what makes the “nothing here identifies a visitor” promise concrete.
  • The map and the per-minute chart stack in the left column so both columns end together, and the feed is a capped, sticky panel only where it is a sidebar: below 1280px it grows with the page instead of nesting a scroll area inside one.
  • Found while measuring: the feed's scroll area was adding roughly 900px of empty scroll to the page, because its card never clipped its own overflow.

2026-09-14 — The new lockup, with a wordmark that follows the theme

  • backwire.io and the dashboard use the approved lowercase gradient lockup. brand/backwire-logo.png is the source; web/scripts/brand-assets.mjs derives the light and dark lockups, the mark, and the app icons from it, so the assets can always be rebuilt from the artwork rather than edited by hand. The script takes the artwork either with real transparency or flattened onto white: a flattened export is un-matted from its own coverage, and the compression noise a shade off white is dropped rather than baked in.
  • The mark and the dot over the “i” are fixed — the gradient reads on any ground. Only the wordmark's ink changes: the artwork's navy on light surfaces, #ECF3F5 on the dark rail and in dark mode. The derivation rewrites the ink channel and keeps the alpha, so nothing is traced or re-rendered.
  • The favicon and the Apple touch icon become the mark on a dark rounded square, and every product screenshot on the marketing site is re-taken so the rail in them carries the new logo.
  • The brand kit's logo section is updated: it had deferred this lockup as a future identity revision and forbidden gradients, which the approval supersedes.

2026-09-14 — The widget footer reads “Powered by Backwire”

  • The default footer label changes from “Feedback by Backwire” to “Powered by Backwire”, in the widget, the API's English defaults, the Wording tab, the dashboard's footer switch, the docs, the pricing rows and the marketing FAQ. It is a label like any other: a project that has already written its own footer string keeps it, and every locale can override it.
  • Widget 1.4.2, 14,275 B gzipped. 1.4.1 stays where it is — each version is served from an immutable path with its own published SRI, so a changed bundle always takes a new number.
  • The marketing widget screenshot is re-taken from the new bundle, and scripts/marketing-shots.mjs gained proper section skipping so a single shot can be re-captured without re-seeding the demo project.

2026-09-14 — Widget card spacing

  • The rating faces, the message box, the disclosure line and the error were appended beside the padded body rather than inside it, so they ran to the card's edges and were clipped. They now sit in the body with everything else, on one 16px inset.
  • "Anything to add? (optional)" is a visible label above the box instead of a placeholder that disappears as soon as someone types. It reuses the existing note_optional string, so every translation and the Wording tab already cover it.
  • Controls are 44px: the rating faces, Send, Done, Skip and the email field. Send and Skip fill the row; Done keeps its place beside the email field and matches its height. The card is 336px wide, up from 312.
  • Widget 1.4.1 — the CDN publishes each version at an immutable path with an SRI hash, and 1.4.0 was already live, so the changed bundle took its own version rather than overwriting one that pinned installs verify against. 14,271 B gzipped, 69.7% of the 20 KB budget.

2026-09-14 — Landing hero: copy and product shot on one row

  • The hero is two columns on wide screens — headline, promise and calls to action on the left, the dashboard on the right — instead of a headline with an empty right half and the screenshot beneath it. The shot bleeds to the viewport edge, which buys back the width the split costs, and the measure widens from 1200 to 1320px.
  • The hero screenshot is captured at a 1240px dashboard rather than 1440, so the Pages table is still readable at 61% in half a row. Below 1200px the hero stacks and the shot takes the full measure; below 900px it becomes the dashboard's own phone layout, captured at 390px, rather than a desktop screenshot shrunk to a third.
  • Landing page weight is down from ~420 KB to 187 KB: one hero image instead of three, and a denser capture.

2026-09-14 — Marketing site on the brand kit: real screenshots, teal, and claims that check out

  • backwire.io follows docs/backwire-marketing-brand-guidelines.md. Scoped --mk-* tokens (light ground, teal actions, one dark section) live under .mk, so nothing on the marketing site can reach the dashboard palette. 1200px measure, 68px sticky header, 44px controls with a 48px hero pair, and a keyboard-accessible menu below 900px instead of hidden links.
  • The page now leads with the whole product: Understand your site. Fix what matters. Analytics, reporting, the report-to-prompt workflow, installation, what is collected, pricing and an FAQ, in that order.
  • Real product imagery replaces the animated reconstructions. scripts/marketing-shots.mjs seeds a demo project and captures Page insights, a problem report, the Inbox, the real widget bundle and a 1200×630 social preview; every shot is captioned as example data. The loop stage, agent panels, live panel, report panel and widget stage are retired along with their scheduler and simulator.
  • The agent prompt on the page is the renderer's own output: test/prompt-example.test.ts pins it to api/tests/golden/prompt/bug.md, so the page cannot advertise a brief the product does not produce.
  • Claims that could not be verified are gone, not restated — the 9 KB figure, "no layout shift", "two minutes", the consent-banner assurance, the exhaustive never-collected list, and the Status and Source footer links (one host does not resolve, the repository is private). docs/backwire-marketing-claim-register.md records each one with its source of truth.
  • MCP, AI triage, duplicate grouping and teammates are tagged Planned on both plan cards and the comparison table; the Startup plan has no price and no purchase path. The two retention windows are stated separately: report context 30 days on the free plan, automatic analytics aggregates 91 days.
  • Found while checking the headline: the long-standing "Schibsted Grotesk spaces full stops oddly" was font-variant-numeric: tabular-nums giving the period a numeral's advance width. Display headings now set proportional figures and "Fix what matters." sets correctly.
  • Also corrected: the report context panel said the widget keeps 20 console lines; it keeps 50. /docs/settings was missing the eleven widget keys added last week.
  • Checked at 320, 390, 768, 1024 and 1440px with no sideways scrolling on any page, and every text/background pair on the marketing pages is at least 4.5:1.

2026-09-13 — Wider rail, fuller names, a project timezone, Install page rearranged

  • The rail is 256px (was 216), the logo a fifth larger, the project box 52px tall with a 28px site icon and roomier menu rows; nav items say Live Activity, Widget Customization and Project Settings, and the page titles match.
  • Project timezone. Onboarding asks for it (pre-set to the browser's zone) and Project Settings can change it. Data is recorded in UTC as before; the zone only changes how times are shown: report times in the Inbox and its history, hourly chart labels, and the Home subtitle ("Times in Hong Kong (GMT+8)"). Daily totals stay UTC days, and the subtitle says so. Stored as settings.timezone; the API rejects a zone it does not know and never sends it to the widget.
  • Install is a two-column reference page: the snippet and the optional version pin on the left, "Confirm it loaded", the allowed domains and where to go next on the right — the three steps numbered in the order they happen.

2026-09-13 — Production fix: HTTP/1.1 to Supabase; Page insights layout restored

  • The API now talks HTTP/1.1 to Supabase. Supabase closes idle HTTP/2 connections with GOAWAY and httpx only found out on the next request, which failed with ConnectionTerminated; every worker thread failed once after idling, so several dashboard loads in a row came back "A server error occurred". HTTP/1.1 connections are checked for a peer close before reuse and expire after 5 s idle.
  • Page insights goes back to its previous composition (KPI explorer, Feedback engagement funnel, By page ring, breakdown cards) at the owner's request; the redesign's tokens, dark rail and controls stay, as does the API's page-stats?previous=1. The automatic-only recomposition below is kept in history (a8d1b55) for when the layout question is settled.

2026-09-13 — Dashboard redesign: tokens, the dark rail, and Page insights on the automatic endpoint (reverted the same day, see above)

  • The dashboard takes the redesign spec (docs/backwire-dashboard-redesign-spec.md): a teal accent, a dark rail on every dashboard page, a canvas one step off white behind white panels, 40px controls and 8px corners. The marketing site keeps its black-and-white look.
  • Page insights is automatic measurements only. Four headline metrics — page views, slow load rate, views with JS errors, timing coverage — each with a sparkline, an info tip and a change against a real preceding window: page-stats?previous=1 answers for the window that ends where the current one starts, and refuses (422) when that would reach past the 91-day retention, in which case the page says the comparison is unavailable rather than inventing one. Rising traffic is neutral; rising slow loads or errors read as adverse. Rates show one decimal, and a rate with no denominator shows "—".
  • One tabbed traffic chart (views / slow load rate / JS errors) with the previous period dashed, a hollow last point while the current bucket is still open, and a screen-reader table behind it. Pages: searchable, sortable on every column with aria-sort, ten rows in the panel and the full list in a dialog; clicking a path scopes every panel through the URL, and the header's "All pages" picker and "Clear filter" do the same. Referring websites, then Browsers and Operating systems behind tabs, then Devices, Countries and Campaign sources; the JavaScript errors and Failed requests tables stay. A failed request shows a retry, never a zero.
  • The Feedback engagement funnel (page views → widget opens → submissions) moves to Home, beside the page cards, because it is feedback data. Fast load rate stays on Home's Site health.
  • Not in this pass: CSV export, and the demo-data badge; both are in the spec and wait on a decision about what the demo project should look like.
  • Removed: the KPI explorer, the by-page ring and the cut-breakdown tables, which mixed feedback counts into the automatic page.

2026-09-13 — Insights strip and tables, tidied

  • The type scale steps down one point across every page: 13, 14, 15, 17, 19, 23, 27, 31.
  • On the Page insights strip the change badge sits on the value's row instead of the corner, so every title fits on one line and the strip is a third shorter. The Fast load rate no longer repeats its explanation under the number; the info icon carries it.
  • In the breakdown tables the rate beside a count sits in a fixed five-character box with a gutter between columns, so the counts line up down a column instead of drifting with the width of the percentage next to them.
  • Found while checking on a phone: the card grids on Page insights had no explicit column below the desktop breakpoint, so an implicit column sized itself to the widest table and pushed the page 7px sideways. Every card grid now declares its column, and the tables scroll inside their card.

2026-09-13 — Brand colours, and the Widget page is complete

  • Colours, advanced under Look: surface, text, muted text, hairlines and fields, for light and for dark, blank by default. The widget applies them as --fbw-* overrides scoped to exactly the theme the base sheet applies, so a light override never leaks into dark mode. A note beside the fields gives the text-on-surface ratio and says when it falls under 4.5:1. Only hex values are stored or written into a style rule. New key colors.
  • This closes the six-step plan: the Widget page now carries eleven runtime settings the widget did not have a week ago, each defaulting to what it did before. Spec §4.8, the embed guide (#feedback), STATUS and the feature inventory are updated.
  • Widget 1.4.0, 14.2 KB gzipped of the 20 KB budget.

2026-09-13 — Capture and analytics switches

  • Kept with a problem report: console lines, failed requests and clicks are each a switch under *Privacy and data*. A trail switched off is unhooked as soon as the settings load and its buffer emptied, so nothing from it can leave the page. JavaScript errors are always kept: they are what a problem report is for. New key capture.
  • Automatic page analytics on or off per site. Off means no load beacons and no error counters; Page insights then says so at the top instead of showing an empty chart. Widget opens and submissions are still counted, being part of the feedback flow. New key analytics.
  • The buffer starts before the config round trip, which is why "off" means "gone within it" rather than "never installed". A boot test proves the console hook is already restored by the time the widget mounts.
  • Widget 1.4.0, 13.9 KB gzipped.

2026-09-13 — Where the widget shows, and what happens after sending

  • Devices. Desktop, tablet and phone switches under *Where it shows*. The bubble stays off an excluded device; page views are measured on every device regardless, as with path rules. New key devices.
  • Test a path. Type a path under the show and hide rules and the page answers "Shown" or "Hidden" as you type, using a line-for-line port of the widget's matcher with the widget's own golden table behind it.
  • Open from a link. With open_on_hash on, a page arriving at or changing to #feedback opens the card, so a footer link or an email can send people straight to it.
  • After sending is a new section: ask for an email always, only after a problem report, or never. New key contact_nudge; the widget skips the nudge and closes with the thank-you when the rule says so.
  • Widget 1.4.0 carries all of it, 13.7 KB gzipped.

2026-09-13 — Widget 1.4.0: icon, size, offset, font and rating scale

  • Feedback types are switches. Rate, Idea and Problem each on or off, with "One plain box instead" for the single-question mode. The stored mode and intents are derived from them (all three is full, only Rate is rating-only, any other subset is two-tab), so nothing changes for existing projects and the API is untouched.
  • Look: *Font* can match the site (font: inherit on the card) instead of the system stack; *Rating scale* can be faces, stars or numbers 1 to 5. Stars light up to the chosen one in the accent; numbers turn the accent when picked.
  • Bubble: four icons (speech bubble, star, megaphone, question mark), regular 44px or compact 36px, and a distance from the side and from the bottom, 0 to 120px, so the bubble can step away from a cookie banner or a chat window in the same corner. The card and the Turnstile box move with it.
  • Five new settings keys, each defaulting to today's behaviour: font, rating_style, bubble_icon, bubble_size, offset. The API lists them so a saved choice reaches /v1/config; the widget validates enums and clamps the offset.
  • Widget 1.4.0, 13.5 KB gzipped of the 20 KB budget.

2026-09-13 — Every string the widget shows, per language

  • The Wording section on the Widget page now offers all twenty of the widget's strings, not ten: the Send, Skip, Done and Close buttons, the two placeholders, the nudge after a good rating, the disclosure under a problem report and the footer link join the rest. They are grouped the way a visitor meets them: bubble and tabs, questions and placeholders, sent screen, buttons, disclosure and footer.
  • Languages are tabs. English first, then each added language, each tab carrying the number of strings still left in English, with a "Reset this language" on every tab. One Save stores every language at once.
  • The API's default label table names all twenty, so /v1/config always carries a complete English set beneath any translation. The form drops unknown label keys instead of storing them.
  • Not yet: the preview speaks the browser's language, so a translation shows up there only when the browser matches it. A preview language switch is a follow-up.

2026-09-13 — Widget gets its own page

  • Widget is a new page in the rail, between Live and Settings: everything a visitor sees in one place, with a section list beside the form and a larger live preview. Settings keeps the project, notifications and data. The Widget, Words and Pages tabs are gone from Settings; their fields moved across unchanged.
  • The preview shows every screen. Three buttons switch it between the bubble, the open card and the sent screen, so an owner can check the thank-you wording without sending anything. The preview also follows the section being edited: focus in Bubble shows the bubble. A phone toggle renders the same bundle in a 390px frame.
  • Each page saves only the settings it owns, laid over what is stored, so saving the widget can never wipe the Slack webhook and saving notifications can never reset the accent.
  • Widget 1.3.x gains a preview-only demo(state) on its handle for the sent screen; it does nothing outside preview.
  • Design: docs/superpowers/specs/2026-09-13-widget-customization-design.md. This is step 1 of 6; the new settings arrive in the following steps.

2026-09-13 — The report panel can be closed

  • A close control in the report panel's header on desktop, and Escape closes it too, the way it closes the widget's own card. Before this the only way back to a full-width list was the browser's back button; the phone-only "Back" link was hidden above md.
  • Closing keeps the list's filters and search. Escape is ignored while typing in the reply box or a field, so it cannot throw away a half-written reply.

2026-09-14 — The scale moves up two points

  • Every step of the type scale gains 2px: 14, 15, 16, 18, 20, 24, 28, 32. The root moves to 16px with it, so rem-based padding and gaps widen by about a seventh; the pages read airier rather than merely larger.
  • Three things the larger size broke, and their fixes: the report panel's fact labels truncated to "Page load…", so that column is sized from the scale now and follows any future change; the ring on Page insights pushed a phone 7px sideways, and scales down to fit; the delta badge on the insights strip landed on the info icon on a phone, so the label reserves more room.
  • Chart axis labels, drawn in SVG units rather than tokens, move with the smallest step to 14.

2026-09-14 — One type scale, and the whole app a size up

  • The dashboard was set at a 13px root with UI text at 11, 11.5, 12, 12.5 and 13px: fifteen distinct sizes across 150 places, none of them a decision. There is now one scale of eight steps in app/globals.css — 12, 13, 14, 16, 18, 22, 26, 30 — each with a line height and a stated job, and every one of those 150 sites uses it.
  • Everything reads a size up. Body and list text 13 → 14, secondary text and chips 12 → 13, micro labels 11 → 12. Page titles were 20, 22, 26 and 28 depending on the page; they are 26 everywhere now. Chart axis labels came off the 11px floor to 12.
  • The marketing site, the landing-page graphics and the settings preview follow the same tokens. The hero clamp and the price on the pricing page stay their own display sizes, named as exceptions in the test.
  • test/type-scale.test.ts fails on any text-[13px] in the markup or any raw font-size in a stylesheet, so the scale cannot quietly grow a sixteenth value.
  • Fixed while checking the result: the numbers in the Page insights and Live strips did not share a baseline. A <button> centres its own content, so cells with a shorter note floated down; the cells are flex columns now, and the label reserves two lines so a wrapped label cannot push its number off the row.
  • The widget keeps its own type. It renders inside a customer's page, where matching our dashboard would be the wrong instinct.

2026-09-14 — Inbox filters: two rows, live counts, stars with ratings

  • The filter bar was ten chips wrapping in one flat row. It is now two segmented controls: status (New, Open, Done, All) above, category (All, Bug reports, Ideas, Ratings) below, in the same visual language.
  • Every chip carries a live count, and each row counts the other's selection. Pick Ratings and the status row shows how many ratings are new; pick New and the category row shows how that queue splits. One grouped read behind it: new migration 20260914020000 adds project_status_intent_counts, and /v1/projects/{id}/counts returns by_status_intent. The flat by_status is now summed from the same matrix, so nothing is counted twice and the round-trip count is unchanged.
  • Stars replace five chips and appear only with Ratings, where a star can exist. Picking a star selects Ratings; moving to a category that cannot carry one takes the star filter with it, instead of leaving a filter that can only return nothing.
  • Notes joins the row when a project running the single general box has actually received some. Those reports were previously reachable only through All.
  • Fixed a pre-existing flake in the keyboard e2e test: it pressed j before the list had hydrated and lost the press. Ten repeats, green.

2026-09-14 — The report panel shows the whole visit

  • From this visit is a new block on the report panel: page and campaign, device, page speed and report groups, four labelled columns instead of six facts in a stretched row. Nine fields that were collected but never shown now appear: page title on its own line, campaign medium, screen and pixel ratio, language, time zone, country by name, online or offline, the three page-speed timings and the full report id.
  • A field the widget never recorded reads Not recorded rather than disappearing, so the panel shows the shape of a complete visit and where this one is thin. The spam check says Passed, Failed or Not checked instead of appearing only on a failure.
  • Before the report now renders even when nothing was captured, saying so in a sentence, and states the caps underneath: the last 5 clicks, 20 failed requests, 20 JavaScript errors and 20 console lines. A rating used to leave most of the panel blank with no explanation.
  • The panel body is capped at 900px and centred, so a wide screen no longer stretches a one-line report across the whole pane. A long error source in the timeline wraps instead of pushing the phone layout sideways.
  • lib/visit.ts holds the grouping, unit-tested; environmentSummary is gone, its four cells absorbed into the new groups.

2026-09-13 — Automatic analytics live

  • Both migrations applied to the live project: page_analytics with its write RPC, and project_queue_health. Widget 1.3.0 is serving from the CDN, so referrers, browsers, operating systems, load timing, JavaScript errors and failed requests are being recorded from real traffic.
  • New Railway jobs service runs scripts/run_jobs.py all every hour: the 9am owner-local digest, the 30-day context retention, the 8-day hourly-stats prune, the 91-day automatic-aggregate prune, and the Turnstile hostname sync. It picked up fxvps.io on its first run, so that project is now covered by Turnstile.
  • Fixed: the Dockerfile copied only app/, so the jobs script was missing from the image and every cron tick failed with "No such file or directory". Found by running the service rather than assuming it worked.

2026-09-14 — Home shows both halves of the product

  • New Site health card on Home: page views, fast load rate, views with JavaScript errors and views with failed requests for the selected period, with a link to Page insights. Home previously showed nothing at all from the automatic side, so an owner could not see that the site had slowed down or started throwing errors.
  • The polling Live strip is gone from Home. The header chip already says who is on the site and now links to Live, which does this properly with a map, a feed and per-minute activity. Home no longer polls every ten seconds.
  • Site health sits with the summary, above the page-level report cards, so Home reads as: what came in, what needs attention, how the site behaved, which pages are worst.
  • share and errorRate were the same function under two names; there is now one.

2026-09-14 — Report insights folded into Home and the Inbox

  • Report insights is gone. Home already carried its whole strip (feedback submissions, bug reports, average rating, prompts copied), its per-day chart by kind, its ratings histogram and its lowest-rated pages, so the page was a near-duplicate that showed empty charts at real report volumes. The rail is five items now.
  • Dropped with it: average rating per week, the kind donut and the campaigns-bringing-bug-reports table. All three need volume a feedback widget does not produce, and the inbox filters answer the same questions exactly.
  • The Inbox gains what neither Home nor a chart could give. Every status chip carries its count (New, Open, Done), and a line above the filters shows the two signals that change what you do next: how many people left contact details and are still waiting to hear back, and how long the oldest unresolved report has sat. The waiting count links straight to those reports.
  • API: /v1/projects/{id}/counts adds unresolved, awaiting_reply and oldest_unresolved_at, served from one new Postgres aggregate (project_queue_health, migration 20260914010000) inside the existing 15-second cache, so the inbox costs no extra round trip.

2026-09-14 — One vocabulary, and a mobile drawer

  • The data vocabulary from the Page insights handoff now reads the same everywhere: Home, Inbox, Live, Report insights, the pricing table, the daily digest email and the agent prompt. Page views, widget opens, feedback submissions, feedback rate and bug reports, in sentence case.
  • The rail is a drawer below md: a compact top bar with a menu button, a dimmed backdrop, focus moved to the close button, Escape to close, body scroll locked while open, and it closes on navigation or on tapping any link. Above md nothing changes. It used to stack the whole rail above the content and cut the nav off mid-word.
  • Settings no longer scrolls sideways on a phone: the form grid can shrink below its widest child, and the paired fields stack under sm.
  • Live rates use the same over-100% guard as Insights instead of printing an impossible percentage, and the Home legend takes singular forms ("1 bug report", not "1 bug reports").

2026-09-14 — Referring websites, browsers and operating systems

  • Page insights keeps its layout. Three breakdown cards join the existing grid: Referring websites, Browsers and Operating systems, each showing page views and share, in the same card shape as campaign sources, countries and devices. A failure to load them leaves the rest of the page intact.
  • New page_analytics aggregate (project, UTC hour, page, referrer, browser, OS, device, country, source) written by bump_page_analytics, read through GET /v1/projects/{id}/page-stats. Referrers keep the hostname only, same-host referrers are dropped, and browser and OS are coarse families parsed server-side; no full user-agent string, no visitor identifier.
  • Two technical-issue cards close the page, side by side at half width: JavaScript errors and Failed requests. Each ranks pages by the views that hit the issue, against the views that reported monitoring. A page without monitoring shows a dash rather than a zero, and a card explains itself while no installed widget reports coverage yet.
  • Failed requests counts the first 4xx, 5xx or unreachable request per page or route. The widget's diagnostic buffer already saw these; it now also increments a counter, carrying no URL, status, method or timing.
  • Widget 1.3.0 sends the external referrer on the document view and a js_error counter at most once per page or route, carrying no message, stack or selector.
  • Fixes found in review: an SPA route change inside the timing window no longer cancels the document page view, its timing and the visit's referrer; the chart series buckets by index so a non-UTC database session cannot invent a phantom point; path targeting again decides only where the pill appears, not what is measured; the migration is re-runnable; pnpm e2e in the widget now builds first, so it can no longer pass against a stale bundle.

2026-09-13 — Automatic Page Insights (implementation, not yet deployed)

  • Page Insights now focuses on automatically recorded page views, slow load rate, average load timing, views with observed JavaScript errors, and timing coverage. Ratings, reported bugs, and feedback analysis stay on Report Insights.
  • Added historical referring websites, browser and operating-system families, alongside country, device, and campaign-source breakdowns. All cuts share the chart’s UTC window and selected-page filter.
  • Added page search, column sorting, automatic-data CSV export, and explicit unavailable-measurement states.
  • Widget 1.3.0 sends at most one anonymous JS-error counter per included page or SPA route; it sends no automatic error messages or stacks. Targeted-out pages do not send automatic analytics.
  • Added an RLS-protected hourly aggregate table with bounded cardinality, paginated reads, and 91-day scheduled retention. Migration and deployment order are documented in docs/automatic-page-insights.md.

2026-09-13 — Page insights vocabulary

  • Headline row is now Page views · Widget opens · Feedback submissions · Feedback rate · Fast load rate, each with an info icon explaining what it counts (read out with the cell for keyboard and screen-reader users).
  • Funnel card is Feedback engagement, its stages carry the same names, and the steps read "Opens per page view" and "Submissions per widget open". The overall rate is Feedback rate.
  • Sections renamed to Pages · Campaign sources · Countries · Devices; their details buttons say what they open ("View all pages", "View all sources", …). Compact columns are Views / Opens / Submissions, and the problem column is Bug reports.
  • An absent campaign tag is Untagged, never "direct" — the analytics cannot tell how a visitor arrived. Empty sections say "No data for this period."; dashes carry "Rate unavailable"; missing timing says "No performance measurements".
  • Fast load rate states its sample and keeps the threshold inclusive: "13 of 21 measured page views at 2.5 s or less · average 2.4 s · target 75%". Deltas name the window they compare with ("Compared with the previous 30 days").
  • CSV headers follow the same vocabulary. Rail, page titles and Report insights move to sentence case; Live and Report insights use the same names for the same measurements. API fields and database columns are unchanged.

2026-09-13 — Hourly page views and page speed

  • Page Insights 24h–168h ranges draw real hourly page views, interactions and submission rate. A new widget_stats_hourly table keeps one row per project, hour and page (about 8 days, pruned by the retention job); source, country and device cuts stay per UTC day.
  • New Fast Loads metric: the widget's first load beacon carries the navigation load time and, when the browser reports it, Largest Contentful Paint. The API counts, per page and per hour, timed views, views at or under 2.5 s (Google's "good" LCP line) and views over 4 s. Insights shows the share of fast loads with the count, the average and the 75% target under it, a line per bucket, a Fast Loads column in By page (share plus average) and the counts in the CSV. Delta is in percentage points against the previous period.
  • Widget 1.2.0 (12.4 KB gzipped). API: POST /v1/event accepts load_ms and lcp_ms; bump_widget_stats_v3 upserts both tables in one call (migrations 20260913000000 and 20260913010000, applied live; v1/v2 functions can be dropped once nothing calls them).
  • Account menu: hydration flag via useSyncExternalStore (lint clean again).

2026-09-07 — Account menu at the foot of the rail

  • The rail footer is one account row (initials, given name, chevron). Opening it shows the full name and email, then Account settings, Appearance (light/dark), Contact us, Terms, Privacy policy, and Sign out. Install stays above the row.

2026-09-07 — 24h Page Insights draws UTC-day page views

  • The 24h range still stores loads and opens per UTC day, but it no longer hides the line. Page View, Interaction and Submission Rate plot the UTC days the window touches (today and yesterday). Submissions stay hourly.

2026-09-07 — Flag images use a 4:3 flagcdn size

  • Live Activity (and every other 14px flag) asked flagcdn for 28×22, which 404s. Flags now snap to a documented 4:3 size (32×24 for that display size).

2026-09-07 — Card subtitles as info tooltips; larger By page ring

  • Every card head is a title plus an info icon; hovering or focusing the icon shows the former subtitle as a tooltip. Card heads are one line again.
  • By page: the ring is 20% larger with a 50% larger total in the middle.

2026-09-06 — Stage-bar funnel, period deltas on the strips, By page ring on the left

  • The funnel is three horizontal stage bars (page view, interaction, submission) proportional to the top stage, with the count at each bar, the step rate between stages and the overall rate at the foot.
  • Page and Report Insights strips show the change against the previous period top-right of each cell: green up, red down, grey flat, "new" when the previous period had none. Rates compare in percentage points. The pages fetch the double range alongside the current one to get the previous period. The note line is gone. --up / --down tokens are the only colour besides the greys, reserved for these deltas.
  • By page: the ring sits on the left, larger, with ten rows by default.

2026-09-06 — Bold By page ring, flag images, Title Case metrics

  • By page: the ring takes 40% of the card with a thicker stroke, in the card and its modal.
  • Countries show a flag image (flagcdn.com, only the two-letter code leaves the browser) before the full country name in By country and on Live; unknown countries get a grey placeholder flag. No more emoji flags.
  • Metric labels are Title Case and singular everywhere: Page View · Interaction · Submission · Submission Rate, and on Report Insights and Live.

2026-09-06 — Page Insights strip down to four KPIs; By page matches it

  • The strip is Page views · Interactions · Submissions · Submission rate (CSAT lives on Report Insights). Strips size their columns to the number of metrics.
  • By page: the columns are those four metrics, and the ring takes about a quarter of the card.

2026-09-06 — Page Insights: Source, Country and Device three across

  • The breakdown row is By source · By country · By device side by side, tables only (rows still ordered by the KPI in the strip, "Show details" opens the full table). By campaign is gone; campaigns stay on Report Insights under "Campaigns bringing problem reports". By page keeps its ring.

2026-09-06 — Rings follow the KPI strip; analytics vocabulary

  • Page Insights: the KPI chosen in the strip drives every breakdown card — rows are ordered by it and the ring shows it (page views, interactions, submissions; the rate KPIs fall back to submissions, CSAT to ratings). The footer says which metric ranks the rows.
  • Hover: the linked rows now light up with a background instead of dimming the others. The divider between table and ring is gone.
  • Vocabulary across Insights and Live: Page views · Interactions (widget opens) · Submissions (reports sent) · Submission rate (conversion) · CSAT (share rated 4 or 5). Table headers and captions follow.

2026-09-06 — Linked hover between tables and rings

  • Breakdown cards and By page: hovering a row highlights its slice in the ring and hovering a slice dims the rows outside it (rows past the top four map to "Other"). "Show details" opens the ring with its legend above the full table, with the same linked hover. By page now shows its top five with the same footer.

2026-09-06 — Instant skeleton on row filters, filter pill, card footers

  • Page Insights: clicking a By page row swaps the body to its skeleton at once (client transition) instead of holding the old numbers until the server answers. The active page filter shows as a black pill beside the range; clicking it clears the filter. The By page card offers "Open its reports in the Inbox" while filtered.
  • Breakdown cards keep a five-row height, draw a divider between table and ring, and end with "Top 5 of N · Show details".
  • Every project page has more room at the bottom.
  • Fix: the global link rule was unlayered and overrode text-* utilities on links (white-on-black links rendered black). It is in the base layer now.

2026-09-06 — Row click filters Page Insights; breakdown rings without legends

  • The page filter box is gone. Clicking a row in By page narrows the whole page to that path; a notice bar under the header offers "Show all pages" and "Open its reports in the Inbox".
  • By source / campaign / country / device: table on the left, the views ring on the right with no legend (names on hover).

2026-09-06 — Page filter for the whole page; breakdown cards side by side

  • API: GET /v1/projects/{id}/stats?path=/pricing narrows every number — the cuts, series, rating counts and actions — to one page (paths normalised like widget_stats). The response echoes path.
  • Page Insights: the page filter sits in the header and drives the whole page; the By page card's own filter is gone (CSV stays). Range links keep the filter.
  • By source / campaign / country / device: donut beside the top five rows; "Details · N" opens every row in a modal.
  • The project menu is taller (44px) with roomier rows; its skeleton matches.

2026-09-06 — Designed project menu, site icons, per-card donuts

  • The rail's project switcher is a designed menu (site icon, name, domain, a check on the current project, "New project" at the foot) with keyboard support; its loading skeleton is the same 36px so the rail does not shift. Site icons come from GET /api/icon?host= — our server reads the site's <link rel="icon"> / apple-touch-icon / og:image (or /favicon.ico) and proxies it with a day of caching; no third-party icon service sees the domain. Sites without one get a monogram.
  • Page Insights: the KPI line fills its card to the funnel's height; the ratings histogram moved to Report Insights (taller there). By source / campaign / country / device each open with a donut of views (top four plus Other).

2026-09-06 — Insights in the rail; Live renames

  • The rail lists Page Insights and Report Insights as their own entries; the in-page Pages / Reports switch is gone.
  • Live: "Activity per minute" is "Live Activity", "Just now" is "User Snapshot". The strip's Pages seen and Countries cells name the busiest page and country.

2026-09-06 — Page Insights and Report Insights; 24 hours on Home

  • Insights is two pages. Page Insights (/insights): the KPI strip with its line, the funnel and ratings, By page, and the source / campaign / country / device tables. Report Insights (/insights/reports): a reports strip (reports, problems, ideas, average rating, prompts copied), reports per day or hour with the modal table, the breakdown donut, average rating per week with the histogram, lowest-rated pages and problem campaigns. A Pages / Reports switch sits beside the range on both.
  • Home gains the 24-hour range, compared with the 24 hours before (the API now accepts any hourly range up to 168h; Home asks for 48h and splits it).
  • The KPI line card stretches to the funnel card's height and the chart fills it. The funnel is one narrowing silhouette: three stacked segments, each tapering from its own count to the next, with a floor so the shape stays a funnel when counts are equal or tiny.

2026-09-06 — Insights explorer, 24-hour range, full-width Inbox

  • API: GET /v1/projects/{id}/stats?range=24h. Reports, ratings and actions are bucketed by the hour from the submissions table (by_day keys become YYYY-MM-DDTHH:00Z, bucket: "hour"); page loads and opens stay per UTC day, so their totals cover today and yesterday and the hourly series carries none. Day ranges return bucket: "day".
  • Insights: 24 hours joins 7/30/90 days. The KPI strip is tappable — the chosen metric (page views, widget opens, reports sent, conversion, happy visitors as average rating) draws as a line beside a redrawn funnel (centred bands with tapering connectors and step rates). Reports per day sits beside a donut of the period's breakdown by kind with its legend table. "Show as table" opens a modal instead of stretching the card. The inverted strip cell is gone; selection is a top rule.
  • Charts draw at 1:1 (viewBox width follows the container) so axis labels and gaps no longer scale up on wide cards. Line charts gain hover points and tooltips.
  • Inbox: the list uses the full content width until a report is selected, then splits into list and detail.

2026-09-06 — Dashboard refresh: louder titles, headline strips, full-width Insights and Live

  • Card titles are set in Schibsted Grotesk at 16px with the subtitle stacked beneath, so section names read before their content. Home stat values move to the display face too.
  • Insights and Live use the full content width and open with a metric strip: page views, widget opens, reports sent, conversion and happy visitors (ratings of 4 or 5) on Insights; page loads, widget opens, reports sent, pages seen and countries on Live. The strip's key cell is inverted.
  • Charts draw from a five-step greyscale ramp (--chart-1 … --chart-5, light and dark), so stacked columns, the funnel, the ratings histogram and legends separate cleanly; the histogram darkens from one star to five.
  • Hover: every table row, list row, bar-list row, chip, stat tile and map dot has a wash or lift on hover (150 ms, off under reduced motion).
  • Live: the world map leads, with rings sized by page loads, count labels on the five busiest countries, a hover highlight on land and dots, and the feed pinned beside it. The By source / campaign / country / device tables on Insights are cards.

2026-09-06 — Brand logo

  • The supplied Backwire mark and lockup (web/public/brand/, black and white colourways) replace the inline wire glyph everywhere: marketing header and footer, dashboard rail and its loading skeleton, sign-in, onboarding. The theme picks the colourway with the same :root.dark / prefers-color-scheme rules as the tokens. New favicon, icon.png and apple-icon.png: the white mark on a black rounded tile so it reads on light and dark tabs.

2026-09-06 — Faster pages: streaming dashboard, threaded API

  • API: routes no longer block the event loop — pure-sync handlers run in FastAPI's thread pool and async handlers wrap repo calls in run_in_threadpool; SupabaseRepo holds one client per thread. Dashboard tokens are verified locally against the project's JWKS (ES256, cached 10 min, refetch on unknown kid; HS256 falls back to the Auth call). /v1/projects/{id}/counts collapses to two RPCs (project_status_counts, project_loads_since, migration 20260906231358_project_counts.sql) with a 15 s cache on status counts invalidated by every submission write.
  • Dashboard: the shell paints first. Each /p/[id] route ships a loading.tsx skeleton; the rail's Inbox count and Live dot stream in; Home's tiles, daily chart, needs-attention, page lists and live strip each resolve independently behind their own skeleton; the Inbox list and Insights body stream behind the header. vercel.json pins functions to sfo1.

2026-09-06 — Home, insights charts, onboarding wizard

  • API: /v1/projects/{id}/stats gains by_day (N calendar days including today), rating_counts and actions (copied_prompt, replied); new /v1/projects/{id}/counts; status filter accepts a comma list; email and Slack deep links open /p/{id}/inbox?s=.
  • Dashboard: Home at the project root with stat tiles (reports, problems, average rating, prompts copied), reports per day, needs attention, page lists and a live strip; the inbox moves to /p/{id}/inbox (old links redirect) with status groups, chips, a sticky action bar, timeline markers and history; Insights adds the funnel, daily chart with a table twin, rating histogram, weekly average and a searchable By page table with CSV download; Live and Settings restyled, Settings in tabs with a "Widget paused" switch.
  • Onboarding: a three-step wizard that waits for the first page load and the first report, then opens the inbox; Install is a reference page.

2026-09-06 — Black-and-white redesign

  • Marketing site, sign-in, onboarding and dashboard move to one monochrome system: white, true black, hairlines, Schibsted Grotesk headlines, Geist body, Geist Mono for code. The indigo accent is gone.
  • Landing page rebuilt around five animated graphics made from the real widget and inbox: the hero loop (report → wire → inbox → agent prompt), the report with its redacted request, the widget stage, the agent prompt paste, and a live fifteen-minute view with the world map. All honour prefers-reduced-motion.
  • Dashboard: intent emoji replaced by monochrome glyphs; pills are outlined; the first-load indicator no longer pulses; the settings preview defaults the widget accent to black.

0.3.0 — 2026-09-06

  • Dashboard: new Live page — the last 15 minutes of widget activity: page loads, opens and reports per minute, referrers, countries on a world map, devices, top pages and a feed of what just happened. Counted in the API's memory only and dropped after 15 minutes: no cookies, no visitor ids, nothing written to the database.
  • Widget 1.1.0: the page-load beacon carries the external referrer's hostname (never the full URL, never same-site). Bundle 12.2 KB gzipped.
  • API: GET /v1/projects/{id}/live; POST /v1/event accepts ref.

0.2.1 — 2026-09-06

  • API: Turnstile is enforced only on hostnames configured on the Cloudflare widget (TURNSTILE_HOSTNAMES). Other hosts get no site key from /v1/config, and a submission without a token from them is stored as skipped instead of spam. Previously every customer domain outside the widget's 10-hostname list would have had all its feedback filed as spam.
  • API: the Turnstile widget's hostname list is now kept in sync with project domains through the Cloudflare API (CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID): on project create/update and hourly via run_jobs.py turnstile. Reserved hostnames come first, then projects in creation order, up to the plan's 10 slots. The live list also decides where Turnstile is enforced.
  • API: SupabaseRepo was missing bump_stats and stat_rows, so the first live project's Install and Insights pages returned 500 and no widget stats were being recorded. Added, with a test that every Repo protocol method exists on the Supabase implementation with the same signature.
  • Dashboard: the Install page reads the live release from cdn.backwire.io/index.json and shows a version-pinned tag with its integrity hash.
  • Ops: live Supabase project Backwire carries both migrations; hosting refs recorded in STATUS.md and docs/env.md.

0.2.0 — 2026-09-06

Changed

Added

  • Named: Backwire (backwire.io), tagline “Feedback that wires back to you.” Public surface renamed: data-backwire, window.backwire, bw_pk_ keys, @backwire/widget, __backwire_skip_net; hosts api. / cdn.backwire.io; sender feedback@backwire.io.
  • Widget visuals reconciled with docs/widget-redesign.html: exact light/dark tokens, divided top row, boxed faces, outline icons, accent check circle, brand footer.
  • Widget redesigned: a quiet 44 px pill in the card's surface colour, one 312 px card with a segmented Rate / Idea / Problem control, faces for ratings, a single-box mode, edge-tab and no-bubble positions, per-locale labels, accent with automatic contrast, corner radius, first-show delay, hide-on-scroll, footer link, and window.backwire.open/close/identify.
  • Analytics are now cookieless daily aggregates (widget_stats) by page, UTM, country and device, fed by POST /v1/event; /v1/config no longer counts. Country comes from the CF-IPCountry header or an optional MaxMind lookup; the IP is discarded.
  • Insights is a funnel: lowest-rated pages, campaigns bringing problem reports, then views → opens → submits by page, source, campaign, country and device.
  • Settings preview runs the real widget bundle in an iframe with no network.
  • Slack incoming webhook, daily 9am owner-local digest, 30-day retention job for the free plan, PATCH /v1/me timezone, public demo feed.
  • Marketing site: home with the live widget, pricing, privacy, terms, changelog, demo inbox, docs.
  • Password sign-in, account creation and password reset.

0.1.0 — 2026-09-05

Added

  • Repo scaffold: command-center files, CI workflow, spec + M1/M2 plan under docs/.
  • Supabase schema: all Phase 1 tables + Phase 2 stubs, RLS via private.* helper predicates, increment_impression RPC, new-user org trigger, pgTAP RLS checks, dev seed.
  • FastAPI service: public /v1/config, /v1/submit, /v1/open, /v1/submit/{id}/contact with per-project CORS, origin allow-list (wildcards), honeypot, sliding-window rate limits, Turnstile verification, golden-tested redaction, 64 KB / 32 KB size limits, owner email notifications; private project/submission routes with audit events and the deterministic "Copy as agent prompt" renderer; in-memory repo for tests/dev and Supabase repo for prod.
  • Widget: zero-dependency TypeScript IIFE (8.96 KB gz), closed shadow DOM, CSP-safe styling, rolling context buffer (console/network/errors/clicks, reversible, redacting), bubble → picker → form → thanks/contact nudge → done state machine, SPA route watching, lazy Turnstile, window.backwire.init, 20 KB size gate, Playwright fixtures for strict CSP / SPA / reset CSS.
  • Docs: docs/embed.md.
  • API: /v1/me, /v1/org (get, rename, delete), /v1/submissions/bulk, rating and has_contact filters.
  • Dashboard (web/, Next.js 16): magic-link + Google sign-in via @supabase/ssr with a no-Supabase dev mode, onboarding, install page with framework snippets and the live "first load" check, inbox with filter chips, search, bulk actions and j/k/e/c keys, detail drawer with the merged "before the report" timeline, status changes, email replies with history, Copy as agent prompt + download, insights, project settings with preview and export, organisation settings; Playwright e2e against the real API.