Commit Graph

411 Commits

Author SHA1 Message Date
jamey
f479be5728 show proper 404 page for missing custom pages instead of redirecting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 08:35:01 +00:00
jamey
ad2f2517e5 add custom page LiveView with catch-all routing
Shop.CustomPage handles /:slug catch-all for CMS pages. Restructured
router so the catch-all is last — all admin, auth, setup, and SEO
routes defined before the shop scope to prevent interception.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 02:21:11 +00:00
jamey
cf627bd585 add custom page data model with split changesets and CRUD context
Stage 1 of custom CMS pages. Adds type/published/meta/nav fields to
pages schema, splits changeset into system vs custom (with slug format
validation and reserved path exclusion), adds create/update/delete
functions with auto-redirect on slug change, and warms custom pages
in ETS cache. 62 pages tests, 1426 total.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 01:56:19 +00:00
jamey
356e336eef plan custom CMS pages feature with catch-all routing
All checks were successful
deploy / deploy (push) Successful in 1m22s
Six-stage plan for user-created content pages at top-level URLs.
System pages keep dedicated LiveViews, custom pages use a single
generic LiveView with portable blocks. Includes navigation
management, SEO, and auto-redirects on slug change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 01:40:37 +00:00
jamey
2c634177c4 add image picker to page editor and fix thumbnail layout
Wire up image field in block settings with a modal picker that
browses the media library. Fix picker thumbnails collapsing to
14px by replacing overflow:hidden with overflow:clip on grid
items (hidden sets min-height:0 in grid context). Polish media
library mobile sheet with scrim overlay and tighter spacing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 01:00:48 +00:00
jamey
847b5f3e5e add admin media library with image management and block picker integration
- Schema: alt, caption, tags fields on images table with metadata changeset
- Context: list_images with filters, find_usages, used_image_ids, delete_with_cleanup
- Admin UI: /admin/media with grid view, upload, filters, detail panel, metadata editing
- Block editor: :image field type for image_text and content_body blocks
- Page renderer: image_id resolution with legacy URL fallback
- Mobile: bottom sheet detail panel with slide-up animation
- CSS: uses correct --t-* admin theme tokens, admin-badge colour variants

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 22:20:51 +00:00
jamey
a039c8d53c add live page editor sidebar with collapsible UI
All checks were successful
deploy / deploy (push) Successful in 6m49s
Admins can now edit pages directly on the live shop by clicking the
pencil icon in the header. A sidebar slides in with block management
controls (add, remove, reorder, edit settings, save, reset, done).

Key features:
- PageEditorHook on_mount with handle_params/event/info hooks
- BlockEditor pure functions extracted from admin editor
- Shared BlockEditorComponents with event_prefix namespacing
- Collapsible sidebar: X closes it, header pencil reopens it
- Backdrop overlay dismisses sidebar on tap
- Conditional admin.css loading for logged-in users
- content_body block now portable (textarea setting + rich text fallback)

13 integration tests, 26 unit tests, 1370 total passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 16:22:35 +00:00
jamey
b340c24aa1 add live preview pane to page editor
All checks were successful
deploy / deploy (push) Successful in 4m55s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 08:06:17 +00:00
jamey
6fbd654d57 add SettingsField struct and repeater field type for block settings
All checks were successful
deploy / deploy (push) Successful in 1m23s
Introduces typed settings schema with SettingsField struct, replaces
the read-only JSON textarea with a full repeater UI for info_card items.
Supports add, remove, reorder and inline editing of repeater items.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 00:54:13 +00:00
jamey
3f97742c0b add inline block settings editing to page editor
All checks were successful
deploy / deploy (push) Successful in 3m40s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:47:24 +00:00
jamey
660fda928f add admin page editor with block reordering and management
Stage 6 of the page builder: admin UI at /admin/pages for managing
page layouts. Page list shows all 14 pages grouped by category.
Editor supports reorder (up/down), add, remove, duplicate, save,
and reset to defaults. DirtyGuard JS hook warns on unsaved changes.
ARIA live regions announce block operations for screen readers.

Also: regenerate admin icons (81 rules via mix task with @layer
wrapping), add gen_smtp dep for SMTP email adapter, add :key to
page renderer block loop for correct LiveView diffing.

1309 tests, 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:15:01 +00:00
jamey
21f57e39e2 fix stage 3 checkboxes in page builder plan
All checks were successful
deploy / deploy (push) Successful in 1m0s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 19:33:45 +00:00
jamey
24ad3b8b60 wire order pages and theme preview to page renderer, remove old templates
Some checks failed
deploy / deploy (push) Has been cancelled
All 14 pages now render through PageRenderer. Theme editor preview
unified from 10 preview_page clauses to one function + page-context
helpers. PageTemplates module and 10 .heex template files deleted.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 19:32:50 +00:00
jamey
16ebc29fa9 wire collection, PDP, cart, and search pages to page renderer
Stage 4 of the page builder: all shop pages now render via
PageRenderer instead of inline templates or PageTemplates.

- Collection: full filter bar moved to renderer (category pills,
  sort dropdown, CollectionFilters hook, empty state)
- PDP: related_products and reviews loaded via block data loaders
  instead of manual queries
- Cart: page definition loaded in mount, subtotal computed in render
- Search: page definition loaded in mount, handle_params unchanged
- Added Phoenix.VerifiedRoutes to PageRenderer for ~p sigil
- Net -55 lines (128 added, 183 removed)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 19:13:00 +00:00
jamey
914e0661a1 update url-redirects plan status to complete
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:32:26 +00:00
jamey
c69e51051f wire simple pages to PageRenderer (stage 3)
Home, Content (about/delivery/privacy/terms), Contact, and ErrorHTML
now render through the generic PageRenderer instead of hardcoded
templates. Block wrapper divs enable CSS grid targeting. Featured
products block supports layout/card_variant/columns settings for
different page contexts. Contact page uses CSS grid on data-block-type
attributes for two-column layout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:29:20 +00:00
jamey
ca9f32fa42 update progress and plan for page builder stages 1-2
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:08:57 +00:00
jamey
32f54c7afc add generic page renderer with block dispatch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:07:57 +00:00
jamey
35f96e43a6 add page builder data model, cache, and block registry
Stage 1 of the page builder: Pages schema with 14 valid slugs,
BlockTypes registry (26 block types with settings schemas and data
loaders), Defaults module matching existing templates, ETS-backed
PageCache GenServer, and Pages context (get_page/save_page/reset_page
with cache -> DB -> defaults lookup). 34 tests, zero visual change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 17:45:07 +00:00
jamey
0c54861eb6 add analytics-powered 404 monitoring with FTS5 auto-resolve
All checks were successful
deploy / deploy (push) Successful in 9m48s
BrokenUrlTracker now queries real analytics pageview counts instead of
hardcoding 0, so broken URLs with prior traffic are distinguished from
bot noise. For /products/ 404s with a single FTS5 search match, auto-
creates a redirect and marks the broken URL resolved. 1232 tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 16:08:25 +00:00
jamey
6e57af82fc add URL redirects with ETS-cached plug, broken URL tracking, and admin UI
All checks were successful
deploy / deploy (push) Successful in 3m30s
Redirects context with redirect/broken_url schemas, chain flattening,
ETS cache for fast lookups in the request pipeline. BrokenUrlTracker
plug logs 404s. Auto-redirect on product slug change via upsert_product
hook. Admin redirects page with active/broken tabs, manual create form.
RedirectPrunerWorker cleans up old broken URLs. 1227 tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 14:14:14 +00:00
jamey
23e95a3de6 make PDP variant selection work without JS
All checks were successful
deploy / deploy (push) Successful in 1m3s
Variant options (colour, size) are now URL params handled via
handle_params instead of phx-click events. Swatches and size buttons
render as patch links in shop mode, so changing variants works as
plain navigation without JS. Quantity is now a number input that
submits with the form. Unavailable variants render as disabled spans.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 01:08:36 +00:00
jamey
a61adf4939 prevent search/cart link navigation when JS modal is active
Some checks failed
deploy / deploy (push) Has been cancelled
Without JS the header icons are plain <a> links to /search and /cart.
With LiveView connected, the hooks now preventDefault on those links
so only the modal/drawer opens.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 01:07:51 +00:00
jamey
d0ea9d59f5 add no-JS fallback for cart country selector
All checks were successful
deploy / deploy (push) Successful in 59s
The delivery country form now has action="/cart/country" with a
noscript submit button. Without JS, changing the country and clicking
Update POSTs to a new CartController.update_country action that saves
the country to session and redirects back to /cart.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 23:14:48 +00:00
jamey
79764c7766 add test asserting order tracking form has no-JS action
All checks were successful
deploy / deploy (push) Successful in 36s
Checks the form renders with action='/contact/lookup' and method='post'
so it works when JS is unavailable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 23:11:12 +00:00
jamey
6289c678f7 add no-JS fallback for contact page order tracking form
Some checks failed
deploy / deploy (push) Has been cancelled
Both order tracking forms now have action="/contact/lookup" so they
POST to a new OrderLookupController.lookup action when JS is off.
The controller mirrors the LiveView handler: checks for paid orders,
sends the verification email, and redirects with a flash message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 23:10:15 +00:00
jamey
6ee8a31716 add test asserting add-to-cart button renders as submit
All checks were successful
deploy / deploy (push) Successful in 48s
Checks the button is type="submit" inside the phx-submit form.
This would have caught the :shop vs :live mode mismatch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 23:00:02 +00:00
jamey
c0427d6956 fix add-to-cart button not submitting in shop mode
All checks were successful
deploy / deploy (push) Successful in 1m5s
The button type condition checked @mode == :live but ThemeHook sets
mode to :shop, so the button rendered as type="button" (doing nothing).
Changed to @mode == :preview to match the existing phx-click pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 22:58:41 +00:00
jamey
0b0adba0fe add no-JS progressive enhancement for all shop flows
All checks were successful
deploy / deploy (push) Successful in 1m23s
Every key shop flow now works via plain HTML forms when JS is
unavailable. LiveView progressively enhances when JS connects.

- PDP: form wraps variant/qty/add-to-cart with action="/cart/add"
- Cart page: qty +/- and remove use form POST fallbacks
- Cart/search header icons are now links with phx-click enhancement
- Collection sort form has GET action + noscript submit button
- New /search page with form-based search for no-JS users
- CartController gains add/remove/update_item POST actions
- CartHook gains update_quantity_form and remove_item_form handlers
- Fix flaky analytics tests caused by event table pollution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 22:56:19 +00:00
jamey
f788108665 add favicon and site icon generation from uploaded images
All checks were successful
deploy / deploy (push) Successful in 1m26s
Upload a source image (PNG, JPEG, or SVG) and get a complete favicon
setup: PNG variants at 32, 180, 192, 512px served from DB via
FaviconController with ETag caching, SVG favicon for vector sources,
dynamic site.webmanifest, and theme-color meta tag. Theme editor gains
a site icon section with "use logo as icon" toggle, dedicated icon
upload, short name, and background colour picker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:22:15 +00:00
jamey
12d87998ee make entire product card image area clickable
All checks were successful
deploy / deploy (push) Successful in 1m18s
The stretched-link::after overlay (z-index: 0) was blocked by
product-card-image-wrap (z-index: 1), so only the title text was
actually clickable. Wrapping the image area in a <.link> component
directly fixes this — taps/clicks bubble up to the link naturally,
and touch-scroll on the image carousel still works on mobile.

Also corrects the mode check: ThemeHook sets mode: :shop on shop pages,
not :live, so the condition is now mode != :preview (consistent with
how the title link already worked).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 15:22:43 +00:00
jamey
e7656cf0b3 fix social link items stacking vertically due to cascade conflict
All checks were successful
deploy / deploy (push) Successful in 1m23s
.themed-button-outline:where(a) { display: inline-block } (line 2147)
was beating .social-link-card-item { display: flex } (line 1796) —
same layer, same specificity, later wins. Used the parent selector
.social-link-card-list .social-link-card-item to give display: flex
higher specificity (2 classes vs 1) so it wins.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 14:49:13 +00:00
jamey
dffc0eeb44 remove font-size override from social link card items
All checks were successful
deploy / deploy (push) Successful in 1m0s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 14:42:09 +00:00
jamey
14dc41efcc remove font-size override from card-inline-form button
All checks were successful
deploy / deploy (push) Successful in 1m1s
Follows the same fix as inputs — the --t-text-small override was making
inline form buttons smaller than other buttons on the page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 14:40:19 +00:00
jamey
c3cc911c5c make themed-input font size consistent across all contexts
Some checks failed
deploy / deploy (push) Has been cancelled
card-inline-form was overriding input font-size to --t-text-small, making
inline inputs noticeably smaller than contact form inputs. Removed the
override so all inputs inherit the same base size from the themed container.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 14:39:26 +00:00
jamey
7121752402 fix inconsistent font size on contact form inputs
All checks were successful
deploy / deploy (push) Successful in 59s
themed-input was inheriting font size from its container rather than
being set explicitly, so contact form inputs appeared larger than
the compact inline inputs in cards like "Track your order".

Added font-size: var(--t-text-base) to the contact form context rule
so both inputs are intentionally sized rather than relying on inheritance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 14:37:32 +00:00
jamey
cf87c3ff03 add base padding to themed-input and fix contact form selector
All checks were successful
deploy / deploy (push) Successful in 1m0s
themed-input had no padding in the base style, so inputs were rendering
with browser-default 1px/2px padding. Added 0.5rem 0.75rem as the base.

Also fixed the contact form CSS which was targeting .shop-input (a class
that doesn't exist on rendered elements) instead of .themed-input.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 14:35:54 +00:00
jamey
781ebc8cd8 fix privacy policy niggles and add last-updated date to legal pages
All checks were successful
deploy / deploy (push) Successful in 1m19s
- Capitalise lead sentence regardless of shop_name value
- Add stripe.com/privacy URL when mentioning Stripe in payment section
- Remove mention of logout from session cookie description
- Make third-party sharing text provider-agnostic (no longer names Printify etc.)
- Add :updated_at block to privacy, delivery, and terms pages showing when
  content last changed — auto-tracked via content hash, so the date advances
  automatically whenever relevant settings change

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 14:33:01 +00:00
jamey
933f685b63 add legal page generator for privacy, delivery, and terms
All checks were successful
deploy / deploy (push) Successful in 1m23s
Replaces hardcoded PreviewData placeholders with generated content
derived from real shop state: connected providers (production lead
times), shipping countries (grouped by region), shop country
(jurisdiction language and governing law), and feature flags
(abandoned cart recovery section, newsletter, VAT clause).

Returns policy correctly cites Consumer Contracts Regulations Reg
28(1)(b) for POD exemption and Consumer Rights Act for defective goods.
Cart recovery section uses jurisdiction-specific wording: PECR Reg 22
for UK, GDPR Art 6(1)(f) for EU, generic otherwise.

About page unchanged — shop owner's story to tell.

26 new tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 13:48:49 +00:00
jamey
fd355c3397 use shop_name and email_from_address settings for all outbound emails
All checks were successful
deploy / deploy (push) Successful in 1m22s
All three notifier functions (order confirmation, shipping, cart
recovery) now read from the same two settings rather than using
hardcoded values or duplicating the lookup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 13:19:54 +00:00
jamey
61887b9d5b improve cart recovery: product links in email, persistent session cookie
All checks were successful
deploy / deploy (push) Successful in 3m32s
- add product_id to order_items (migration + schema + create_order)
- cart recovery email now includes a direct product link per item
- extend session cookie max_age to 7 days so carts survive browser restarts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 13:12:41 +00:00
jamey
2f4cd81f98 add abandoned cart recovery
When a Stripe checkout session expires without payment, if the customer
entered their email, we record an AbandonedCart and schedule a single
plain-text recovery email (1h delay via Oban).

Privacy design:
- feature is off by default; shop owner opts in via admin settings
- only contacts customers who entered their email at Stripe checkout
- single email, never more (emailed_at timestamp gate)
- suppression list blocks repeat contact; one-click unsubscribe via
  signed token (/unsubscribe/:token)
- records pruned after 30 days (nightly Oban cron)
- no tracking pixels, no redirected links, no HTML

Legal notes:
- custom_text added to Stripe session footer when recovery is on
- UK PECR soft opt-in; EU legitimate interests both satisfied by this design

Files:
- migration: abandoned_carts + email_suppressions tables
- schemas: AbandonedCart, EmailSuppression
- context: Orders.create_abandoned_cart, check_suppression, add_suppression,
  has_recent_paid_order?, get_abandoned_cart_by_session, mark_abandoned_cart_emailed
- workers: AbandonedCartEmailWorker (checkout queue), AbandonedCartPruneWorker (cron)
- notifier: OrderNotifier.deliver_cart_recovery/3
- webhook: extended checkout.session.expired handler
- controller: UnsubscribeController, admin settings toggle
- tests: 28 new tests across context, workers, and controller

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 10:02:37 +00:00
jamey
758e66db5c add analytics CSV export
All checks were successful
deploy / deploy (push) Successful in 1m25s
Downloads a ZIP with one CSV per report (overview, trend, pages, entry/exit
pages, sources, referrers, countries, devices, funnel). Export button lives
next to the period selector and picks up the current period and any active
filters using a JS hook + JS.set_attribute, so the downloaded data always
matches what's on screen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 09:37:45 +00:00
jamey
01ff8decd5 add order status lookup for customers
All checks were successful
deploy / deploy (push) Successful in 1m17s
Magic link flow on contact page: customer enters email, gets a
time-limited signed link, clicks through to /orders showing all their
paid orders and full detail pages with thumbnails and product links.

- OrderLookupController generates/verifies Phoenix.Token signed links
- Contact LiveView handles lookup_orders + reset_tracking events
- Orders and OrderDetail LiveViews gated by session email
- Order detail shows thumbnails, links to products still available
- .themed-button gets base padding/font-weight so all usages are consistent
- order-summary-card sticky scoped to .cart-grid (was leaking to orders list)
- 27 new tests (1095 total)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 08:40:08 +00:00
jamey
4e36b654d3 add JSON-LD structured data
Product pages: Product schema (name, description, image, price/currency,
availability) + BreadcrumbList (Home > Category > Product). Home page:
Organization schema (name, url). Uses Jason with html_safe escaping so
the JSON is safe to embed in <script> tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 22:37:34 +00:00
jamey
0f1135256d add canonical URLs, robots.txt, and sitemap.xml
Canonical: all shop pages now assign og_url (reusing the existing og:url
assign), which the layout renders as <link rel="canonical">. Collection
pages strip the sort param so ?sort=price_asc doesn't create a duplicate
canonical.

robots.txt: dynamic controller disallows /admin/, /api/, /users/,
/webhooks/, /checkout/. Removed robots.txt from static_paths so it
goes through the router instead of Plug.Static.

sitemap.xml: auto-generated from all visible products + categories +
static pages, served as application/xml. 8 tests.

Also updates PROGRESS.md: marks tasks 55, 58, 59, 61, 62 as done.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 21:47:35 +00:00
jamey
b11f7d47d0 add open graph and twitter card meta tags
Product pages get og:type=product, og:url, og:image (hero image), and
twitter:card=summary_large_image. All other shop pages get og:type=website
and twitter:card=summary. og:title and og:description mirror the existing
page title and meta description.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 21:37:50 +00:00
jamey
c6da3b3d2b add meta descriptions to all shop pages
All checks were successful
deploy / deploy (push) Successful in 1m38s
Product pages: first 155 chars of description, stripped of HTML, truncated on word boundary.
Collections: contextual description based on collection type.
Content pages (about, delivery, privacy, terms): from hero_description text.
Contact: static description.
Home falls through to site_description from settings (already in layout).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 21:31:35 +00:00
jamey
45f05c8bb7 add site name separator to shop page titles
All checks were successful
deploy / deploy (push) Successful in 1m19s
All shop pages now render as "Page · Store Name" using live_title suffix.
Home stays as "Home · Store Name" for consistency with live navigation.
Updated cart test to use regex for whitespace-tolerant title matching.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 21:23:09 +00:00
jamey
9b78793701 add entry/exit pages panel to analytics dashboard
All checks were successful
deploy / deploy (push) Successful in 1m28s
ROW_NUMBER() window function picks first/last pageview per session.
Both tables live in the pages tab and support the pathname filter.
6 new tests, 1061 total.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 21:14:24 +00:00