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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
The Plug records a pageview with a known ID (plug_ref) into the ETS
buffer. When JS connects, the LiveView hook supersedes that event by
ID and records its own with full data (screen_size from connect params).
If JS never connects, the Plug's event flushes normally after 10s.
Also fixes: admin browsing no longer leaks product_view events — the
Plug now sets no analytics session data for admins, so all downstream
visitor_hash guards naturally filter them out.
Replaces the previous time-based skip logic which was brittle and
race-prone. The supersede approach is deterministic and handles both
the ETS buffer and already-flushed DB cases.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Click any row in pages, sources, countries, or devices tables to
filter the entire dashboard by that dimension. Active filters show
as dismissible chips. Filters thread through all queries including
previous-period deltas. 1050 tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each stat card now shows the percentage change vs the equivalent
previous period (e.g. 30d compares last 30 days vs 30 days before).
Handles zero-baseline with "new" label and caps extreme deltas at
>999%. Seed data extended to 2 years for meaningful 12m comparisons.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Event call sites (product_view, add_to_cart, checkout_start, purchase)
were only passing visitor_hash and pathname, leaving browser, OS, screen
size and country nil. Add AnalyticsHook.attrs/1 helper to extract common
analytics fields from socket assigns, and use it in all LiveView event
call sites. Checkout controller reads the same fields from the session.
Also fix plug analytics test to clear stale events before assertions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three-layer pipeline: Plug for all HTTP requests (no JS needed), LiveView
hook for SPA navigations, JS hook for screen width. ETS-backed buffer
batches writes to SQLite every 10s. Daily-rotating salt for visitor hashing.
Includes admin dashboard with date ranges, visitor trends, top pages,
sources, devices, and e-commerce conversion funnel. Oban cron for 12-month
data retention.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Same pattern as the Printful work: wire up base_options/0 so tests can
inject a Req.Test plug, fix unreachable 204 clause in delete, add
HTTP-level client tests and provider integration tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire up Req.Test plug for the Printful HTTP client so tests can stub
responses. Adds HTTP-level tests for the client, provider integration
tests, and mockup enricher tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
BenchRepo for isolated concurrency testing against temp DB files.
Correctness tests prove WAL concurrent reads, IMMEDIATE transaction
mode vs DEFERRED upgrade failures, and PRAGMA application. Benchmark
tests (tagged :benchmark, excluded by default) measure throughput.
mix bench.sqlite runs HTTP load scenarios against the full Phoenix
stack with --prod, --scale, --pool-size, and --busy-timeout options.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests that sending a test email sets the verified flag, saving config
clears it, and disconnecting clears it. Also adds unit tests for
email_verified?/mark/clear in the Mailer module.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The login page now only shows the magic link form when a test email has
been sent successfully, not just when an adapter is configured. Saving
email settings or disconnecting clears the flag so the admin must
re-verify after config changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When email isn't configured, the login page now hides the magic link
form and shows a recovery link. The /recover page logs the setup secret
to server logs and lets the admin reset their password with it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Settings keys like api_key were shared across providers, so switching
from e.g. Postmark to SendGrid showed the old API key. Now each
adapter gets its own namespaced key (email_postmark_api_key, etc.)
so credentials persist independently and switching back pre-fills
previously saved values.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Card radio component for picking email providers (SMTP, SendGrid, Mailjet, etc.)
with instant client-side switching via JS hook. Adapter configs are pre-rendered
and toggled without a server round-trip. Secrets are preserved when re-saving
with blank password fields. Includes from address field, test email sending,
and disconnect flow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Setup wizard no longer requires email delivery. Admin account is
auto-confirmed and auto-logged-in via token redirect. Adds setup
secret gate for prod (logged on boot), SMTP env var config in
runtime.exs, email_configured? helper, and admin warning banner
when email isn't set up. Includes plan files for this task and
the follow-up email settings UI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 4: admin components and utilities now reference --t-* theme
tokens directly. Status colour tokens added to theme-semantic.css.
Bridge file (admin/themes.css) deleted.
Phase 5: removed duplicated .preview-frame CSS block (~160 lines).
Admin components and icons wrapped in @layer admin. Layer order
updated in admin_root to include admin layer.
Phase 6: added prefers-reduced-motion support (zeroes all durations
and disables animations). Migrated physical properties to logical
equivalents (text-align start/end, margin-inline, padding-inline,
inset-inline-end) across shop and admin CSS.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 1: Replace hex_to_hsl with hex_to_oklch in CSSGenerator, output
--t-accent-l/c/h instead of --t-accent-h/s/l. All 46 HSL accent
references across theme-semantic.css, theme-layer2-attributes.css, and
shop/components.css replaced with oklch/color-mix equivalents. Dead
style*= attribute selectors for button variants replaced with proper
class-based selectors. Added color-scheme: light/dark to mood output.
Phase 2: Add LoadTheme plug to admin pipeline, extend AdminLayoutHook
with theme_settings and generated_css assigns, add font preloads and
generated CSS injection to admin_root.html.heex. No visual changes to
admin yet — .themed wrapper added in next phase.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Increase DBConnection queue tolerance to stop requests being dropped
under load, and flip 3 write-heavy test files to async: false since
SQLite's single-writer model doesn't play well with concurrent writes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show a lightweight error page using admin.css when the shop isn't
live yet, avoiding broken theme dependencies. Also tidied up copy
to sentence case and shorter descriptions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
phase 1 (no admin): show only the email form
phase 2 (admin created, not logged in): "check your inbox" gate with
"wrong email? start over" link that deletes the unconfirmed user
phase 3 (logged in via magic link): show provider + stripe steps
removes the confusing redirect to /users/log-in after account creation.
users now stay on /setup throughout the entire setup process.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- new /setup page with three-section onboarding (account, provider, payments)
- dashboard launch checklist with progress bar, go-live, dismiss
- provider registry on Provider module (single source of truth for metadata)
- payments registry for Stripe
- setup context made provider-agnostic (provider_connected, theme_customised, etc.)
- admin provider pages now fully registry-driven (no hardcoded provider names)
- auth flow: fresh installs redirect to /setup, signed_in_path respects setup state
- removed old /admin/setup wizard
- 840 tests, 0 failures
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move the setup stepper out of the dashboard into its own LiveView.
Dashboard now redirects to setup when site isn't live, and shows
stats-only view once live. Also cleans up button component variant
handling, fixes alert CSS, and removes stale demo.html.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
replace Tailwind CLI with esbuild for admin CSS bundling. admin now uses
hand-written utility classes (admin/utilities.css), static heroicon CSS
generated by mix generate_admin_icons, plain CSS colour themes extracted
from DaisyUI plugin config, and minimal resets. rename app.css to admin.css
for clarity alongside shop.css. delete vendor/daisyui-theme.js and
vendor/heroicons.js. no Tailwind dependency remains in the project.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move all image URL logic into ProductImage.url/2 and thumbnail_url/1,
remove dead on-demand generation code from Optimizer, strip controller
routes down to SVG recolor only, fix mockup startup check to verify all
variant formats, and isolate test image cache directory.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Read-mostly admin views for synced products: filterable/sortable list
with inline visibility toggle, and detail page with images grid,
variants table, storefront controls form, and provider edit links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tag product images with their colour during sync (both Printful and
Printify providers). Printify images are cherry-picked: hero colour
keeps all angles, other colours keep front + back only. Printful
MockupEnricher now generates mockups per colour from the
color_variant_map.
PDP gallery filters by the selected colour, falling back to all
images when the selected colour has none. Fix option name mismatch
(Printify "Colors" vs variant "Color") by singularizing in
Product.option_types.
Generator creates multi-colour apparel products so mock data matches
real sync behaviour.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Provider form accepts ?type= query param (printify/printful)
- Conditional setup instructions per provider (API key steps, login URLs)
- Dynamic labels, titles, and config handling (shop_id vs store_id)
- Provider index shows dropdown with both provider options
- Settings page renamed from @printify to @provider (generic)
- Fix Printful shipping rates: add default state codes for US/CA/AU
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New POST /webhooks/printful route with VerifyPrintfulWebhook plug
(shared secret token via header or query param). Handles package_shipped,
order_failed, order_canceled, product_updated, product_synced, and
product_deleted events. Webhook registration via Printful v2 API with
token appended to URL. 19 new tests (819 total).
Also marks task #28 as done — Printful sync products already include
preview mockup images handled by the existing ImageDownloadWorker
pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add blueprint_id and print_provider_id to Printful provider_data so the
generic shipping calculator can look up rates. Fix v2 API request format
(order_items key) and response field names. Fetch one representative
variant per product to get accurate per-item rates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Printful HTTP client (v2 + v1 for sync products), Provider behaviour
implementation with all callbacks (test_connection, fetch_products,
submit_order, get_order_status, fetch_shipping_rates), and multi-provider
order routing that looks up the provider connection from the order's
product instead of hardcoding "printify".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Shipping rates fetched from Printify during product sync, converted to
GBP at sync time using frankfurter.app ECB exchange rates with 5%
buffer. Cached in shipping_rates table per blueprint/provider/country.
Cart page shows shipping estimate with country selector (detected from
Accept-Language header, persisted in cookie). Stripe Checkout includes
shipping_options for UK domestic and international delivery. Order
shipping_cost extracted from Stripe on payment.
ScheduledSyncWorker runs every 6 hours via Oban cron to keep rates
and exchange rates fresh. REST_OF_THE_WORLD fallback covers unlisted
countries. 780 tests, 0 failures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
FTS5 prefix matching misses mid-word substrings (e.g. "ebook" in
"notebook"). When FTS5 returns zero results, fall back to LIKE
query on title and category with proper wildcard escaping. 4 new
tests, 757 total.
Also marks completed plan files (search, admin-redesign,
setup-wizard, products-context) with correct status.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>