← Back to directory

Changelog

All changes to SheetDropper and associated components, newest first.

Web App Flask app & catalog website
Desktop Electron desktop app
Template Excel spreadsheet template
Multiple Touches two or more systems
Template Added the new catalog_layout_mobile setting to the downloadable spreadsheet template (template_generator.py), so it can be set/edited via xlsx like every other config field, not just in the web admin. Config tab gets a catalog_layout_mobile row defaulting to grid, with cell notes (grid recommended on phones since the table forces sideways scrolling; computers use the Default Layout above; visitors can still switch on their device) and the same table/grid dropdown as catalog_layout. The User Guide tab documents it under Body Settings, and the existing catalog_layout note was reworded to 'on computers' to make the pairing clear. Round-trips automatically: _parse_config_sheet reads it on upload (no allowlist) and converter.products_to_xlsx pre-fills it on download/backup since the row now lives in the template. Verified the generated template parses back catalog_layout_mobile=grid with the dropdown attached. No web behaviour change beyond the new config key already shipped earlier today; desktop app field still pending.
Web App Admin Settings>Body: swapped the new Mobile Layout toggle order from [Grid][Table] to [Table][Grid] so it matches the Default Layout toggle directly above it. Cosmetic only -- grid is still the default and the warning-on-table behaviour is unchanged.
Web App Added a separate Default Mobile Layout to Site Settings > Body, defaulting to grid, so phones no longer land on the product table (whose columns force awkward sideways scrolling on small screens). The existing Default Layout still governs desktop. New per-tenant config catalog_layout_mobile (validated table/grid, defaults grid). Server picks the first-paint default by device: a new _resolve_layout(config) helper returns grid for phones (UA match via _is_mobile_request -- iPhone/iPod/Android-Mobile/etc., deliberately NOT iPads/tablets which have room for the table) and the desktop catalog_layout otherwise; an explicit ?layout= (the visitor's own toggle, carried by HTMX) always wins, so anyone can still switch and the embed's layout persistence is unchanged. Wired into all three render paths (standalone, embed, products partial). The standalone catalog response now sends Vary: User-Agent since its default varies by device (embed is already no-store). Admin UI: a Mobile Layout grid/table toggle under Default Layout in the Body group; choosing table opens a warning modal (reusing the existing sset-modal pattern) that grid suits mobile and visitors can still switch -- Keep grid reverts, Use table anyway proceeds. catalog_layout_mobile added to the save-settings + upload-confirm field lists (with validation) and to _CONFIG_EDITABLE so it reads/round-trips via the desktop /api/config without being wiped. Desktop Configure-screen parity (a Mobile Layout control) is a follow-up build. No CSS change.
Multiple Updated the user-facing help pages (web /guide and the desktop app's Help screen) to match the current feature set. Web guide: Settings tab now mentions the theme preset, Cart Settings lists the optional return-to-shop link, the Account tab lists connected devices (with revoke), and the desktop-app section explains that the API key is not stored but exchanged once for a revocable device token; desktop Cart Settings card adds the return-to-shop link. Desktop Help: Quick Start step 1 reworded for the pair-once device-token model, Cart Settings gains a Return-to-shop link bullet, and Troubleshooting > Can't connect adds a note about reconnecting after revoking the device. (gst_rate was already correctly documented as a percent on both, and the desktop Theme help bullet was fixed earlier today.) Web guide ships now; the desktop Help changes ship in the rebuilt installer.
Web App Audited and corrected the superadmin /docs page, which had drifted as features were added. Fixes: (1) the Config fields list (claims to be the exact _CONFIG_EDITABLE set) was missing 'theme' -- added. (2) Authentication section rewritten to document the new device-token model -- a Bearer can be the account API key OR a per-device token, plus POST /api/device/pair (key-only, returns the token once, server stores a SHA-256 hash, pairing gated so a device token cannot mint more). (3) Multi-tenant model now notes the device_tokens table in tenants.db. (4) Admin tabs table: Account row adds 'connected devices (revoke)', Settings adds 'theme preset', Cart Settings adds 'return-to-shop link'. (5) Desktop app section corrected from 'connects with the tenant API key' to pair-once + encrypted revocable per-device token (raw key never stored). (6) Security notes add the device-token/encryption facts. (7) Minor: added second_level_cleaner.py to the file tree, app.py line count ~6,000 -> ~6,200, /api/cart-config description adds the return-to-shop URL. Verified the backups path (uploads/<slug>/backups/) and gst_rate percent note were already correct, left unchanged. Docs template only, no behaviour change.
Multiple Desktop credential security reworked end to end, since the app now handles money (refunds, customer PII, Stripe connect). Before, the long-lived account API key was stored in plaintext on disk and survived uninstall. Now the desktop pairs ONCE: at setup it sends the API key to a new POST /api/device/pair, receives a per-device token, and stores ONLY that token, encrypted via Electron safeStorage (Windows DPAPI, bound to the OS user). The raw API key is never written to disk. Server: new device_tokens table in tenants.db (stores a SHA-256 hash of each token, never the token itself; auto-migrated like connect_nonces); tenant_api_key_required now accepts either the account API key (g.auth_method='api_key') or a valid non-revoked device token (g.auth_method='device_token', bumps last_seen_at), so existing API-key callers keep working unchanged. /api/device/pair is gated to api_key auth so a device token cannot mint more tokens. Tokens do not expire; they are revocable. Web admin Account tab gained a Connected Devices card (label = the PC hostname, last-active time) with a CSRF-protected Revoke button (POST /c/<slug>/admin/devices/<id>/revoke) for killing a lost or retired machine. Desktop Disconnect now just wipes the local token (works offline); revocation is the deliberate web-admin action. package.json: deleteAppDataOnUninstall=true and app.setName('Catalog Manager') pinned so the saved login lives in one predictable folder and uninstall actually clears it (this also stops the orphaned user-data folders that caused a reinstall to come pre-logged-into a previous account). Also fixed: on Disconnect the Configure and Preview tab now resets to the blank default template instead of leaving the previous tenant's settings populated in the form. No migration (single operator); existing installs simply re-pair.
Desktop New Catalog Manager installer built (electron-builder, 78.7 MB) and deployed to https://sheetdropper.com/static/downloads/catalog-manager-setup.exe, replacing the 2026-05-01 build. Ships the theme-parity rewrite (real preset list classic/bulletin/market/minimal/sheetdropper/ledger with apply-colours prompt + reset button, dead Light/Dark theme_mode removed), the Cart Settings Return-to-shop link (shop_return_url), and the gst_rate percent help-text fix. Unsigned (no code-signing cert) so Windows SmartScreen will warn on first run, same as prior builds.
Multiple Desktop Catalog Manager parity pass (themes + cart return link), bringing it up to the website's current state. (1) Themes: replaced the stale, dead Theme section (it offered non-existent 'Clean'/'Bold' presets and a Light/Dark Mode select the server ignored entirely) with the website's real preset list -- Classic, Bulletin, Market, Minimal, SheetDropper, Ledger -- and wired it to behave like the web admin: picking a preset prompts to fill the colour fields with that theme's palette (live preview updates), plus a 'Reset to theme defaults' button. Themes are not finalised yet; this is parity so both can be reworked together later. Removed theme_mode from the desktop (web dropped it 2026-05-25). Server: added 'theme' to _CONFIG_EDITABLE so /api/config persists and returns it (validated against THEME_PRESETS), since the desktop saves config via the API -- previously theme could not be saved from the desktop at all and the sheetdropper data-theme dark look would never apply. (2) Cart Settings: added the optional Return-to-shop link (shop_return_url) field, the desktop follow-up flagged when that feature shipped 2026-05-29. (3) Help/docs: gst_rate column now documented as a percent (10 = 10%) not a decimal (0.1), matching the 2026-05-18 format change; Theme help bullet rewritten. Also repaired changelog.json itself, which was missing its opening '[' and failed json.loads, so the /changelog page was silently rendering empty. Desktop renderer changes ship in the next installer build.
Web App Stripe webhook now accepts both live and test signatures. Stripe emailed warning about 36 failed test-mode webhook deliveries to /stripe/webhook since 2026-05-29; nginx logs confirmed 400s. Cause: server runs in live mode (STRIPE_TEST_MODE=0) so STRIPE_WEBHOOK_SECRET = live, but the demo tenant uses per-tenant test mode for cart checkout and Stripe fires test-mode events (signed with the test secret) to the same URL, so signature verification rejected them. Fix in app.py: load STRIPE_TEST_WEBHOOK_SECRET unconditionally alongside the live secret, and have stripe_webhook() try each configured secret in turn -- treat SignatureVerificationError as continue, ValueError as bad payload -> 400. No env changes needed (STRIPE_TEST_WEBHOOK_SECRET already set in .env).
Web App Fixed the mobile '☰ Categories' button (.embed-cat-btn) leaking onto the desktop embed toolbar beside Cart, where it shouldn't render and tapping it does nothing (the drawer isn't mounted on desktop). Root cause was a CSS source-order/specificity collision: the button carries both classes (cart-btn embed-cat-btn), and the global hide rule .embed-cat-btn { display:none } sat just before .cart-btn { display:inline-flex }, equal specificity, so .cart-btn won and forced it visible on desktop. The mobile show rule (body.embed .embed-cat-btn) was already higher specificity and was doing its job. Fix: raised the hide selector to .cart-btn.embed-cat-btn so it beats .cart-btn on its own, while staying below the body.embed descendant selector so mobile still shows the button. CSS-only one-liner in static/style.css, bumped ?v=119 -> ?v=120 in base.html. No template changes, no JS, no impact on standalone (the button is gated behind {% if embed %}) or on mobile embed.
Web App Docs (/docs Developer tab) correction: the Deploy section claimed templates take effect without a restart. That's wrong on this prod setup -- Jinja auto-reload is off, so compiled templates are cached for the worker lifetime and a template (.html) edit only takes effect after systemctl restart sheetdropper. Updated the text to say templates need a restart like Python files; only static files (CSS/JS) take effect without one.
Template Embed cart now clears after a completed order. Returning via 'Return to shop' (or any path that isn't closing the Stripe tab) previously left the just-purchased items in the cart, because the cart lives in the iframe's partitioned localStorage that only iframe-context code can clear, and the existing watchStripeTab reconcile only fires on tab-close (and not at all on iOS, no tab handle). Fix: checkout.html stashes the Stripe session id in the iframe partition (sd_pending_session_<slug>) when the checkout session is created; on the next embed catalogue load, catalog.html checks /cart/check-payment for that session and, if paid, clears the cart (cartClear + cartShippingClear) and drops the stash. Deterministic on the return path for both desktop and iOS since the iframe reloads. Abandoned (unpaid) stashes self-expire after 2h so they can't later wrongly clear a cart. The new catalogue code is a separate embed-only IIFE beside the cart.js include; it does NOT touch the Back/scroll reconstruct, hx-push-url, anchor-scroll, no-store, or _set_embed_headers. No server changes.
Web App Embedded stores: added an optional 'Return-to-shop link' so customers can get back to the host site after checkout instead of landing on a sheetdropper URL. New per-tenant cart-config value shop_return_url, set in Admin > Cart Settings (and via /api/cart-config), mirroring auspost_from_postcode. The embed order-confirmation page now shows a prominent 'Return to shop' button to that URL when set, and falls back to a 'you can close this tab' message when blank (the host tab stays open underneath). URL is validated to http(s) only on save (_clean_shop_return_url) to prevent a javascript:/data: href on the confirmation page, and the template also guards on startswith('http'). Optional field, no required marker. Auto-detecting the host URL isn't reliable cross-origin (Referrer-Policy strips the referrer; ancestorOrigins gives only the origin and not in Firefox), so the tenant sets it once. Desktop Catalog Manager parity is a follow-up. Non-embed checkout confirmation unchanged.
Multiple Mobile (iOS) embed checkout fixed properly. iOS Safari blocks BOTH script-opened popups and script navigation of the top window from inside a cross-origin iframe (even on a genuine tap), so window.open and the top-redirect fallback both failed and Place order showed the blocked-popup error. The one thing iOS allows is a real link tap. Fix: in checkout.html handlePayEmbed, the desktop/Android path is unchanged (sync window.open + watchStripeTab); when the popup is refused (win===null), instead of erroring we now reveal a real anchor styled as the pay button (#pay-link, target=_blank rel=noopener) via new showPayLink() and hide the Place-order button, so the customer's tap opens Stripe in a new tab. Removed the temporary DIAG text and the broken top-redirect attempt. Also set Cache-Control: no-store on the embed checkout DOCUMENT only (directly in tenant_checkout, NOT in _set_embed_headers, so it does not touch the cacheable /products fragment or the embed catalogue document -- respects the scroll-restore guardrails) so the iframe always runs current checkout JS. Did not touch tenant_embed no-store, hx-push-url, the embed reconstruct/anchor-scroll, or _set_embed_headers.
Template Mobile embed checkout: iOS Safari blocks popups from inside an iframe even on a genuine tap (Block Pop-ups is on by default), so the synchronous-open fix alone wasn't enough and Place order still showed the blocked-popup error. Added a graceful fallback: when window.open returns null (popup refused), navigate the whole page to Stripe Checkout via (window.top||window).location instead of dead-ending on the error. Desktop/Android keep the seamless new tab (window.open succeeds, branch not taken); iOS falls through to the full-page redirect and returns via Stripe's success_url. Also clear the client cart on the order confirmation page (order_confirmation.html) so the redirect path, where no opener runs afterwards, doesn't leave a stale cart; idempotent for the existing flows. Embed confirmation link changed from a 'close this tab' note to a 'Return to shop' link. No server changes.
Template Mobile embed checkout: fixed the Stripe payment tab being blocked ('Your browser blocked the payment window. Allow popups...') on Place order. handlePayEmbed() opened the new tab inside the fetch callback, i.e. after the server replied, by which point the tap's user-activation had expired, so mobile browsers (iOS Safari) blocked it as an unsolicited popup. Now the tab is opened synchronously the instant Place order is tapped (window.open('','_blank')) while activation is still held, left blank for the ~1s the checkout-session request takes, then pointed at the Stripe URL (win.location.href = d.url). Error/empty-URL paths close the pre-opened blank tab and re-enable the button. No popups-permission prompt, mostly seamless. Standalone (non-embed) flow with Stripe Elements is unchanged.
Web App Fixed mobile embed checkout failing with 'Something went wrong' on Place order. Root cause confirmed by live logging: the public checkout POSTs rely on a cookie-based CSRF token, but iOS Safari drops the session cookie inside the cross-origin embed iframe (diag showed session_cookie_present=False, header_token_present=True, result=403), so every mobile order was rejected with a 403 before reaching the handler; desktop browsers still send the cookie so they worked. Fix: exempt the three unauthenticated customer checkout endpoints (/c/<slug>/cart/payment-intent, /cart/checkout-session, /cart/confirm-order) from the cookie-based CSRF check, same as the /api/ routes already are. These carry no login/ambient authority for CSRF to protect, so nothing real is weakened, and the embed must not depend on visitor cookies. Removed the temporary CSRF-DIAG logging added earlier today.
Web App TEMP diagnostic (remove after diagnosis): added CSRF-DIAG logging to the _check_csrf before_request for any /cart/ POST. Logs device (iOS/Android/desktop), whether the session cookie arrived, whether the session held a _csrf_token, whether the X-CSRFToken header was present, and pass/403. Investigating mobile embed 'Place order' failing with a generic error: nginx logs show iOS Safari POSTs to /cart/checkout-session return 403 (185-byte default HTML page = abort(403) from the CSRF check) while desktop returns 200. No behaviour change; purely logging to confirm the session cookie is being dropped in the cross-origin iframe on iOS before deciding the fix.
Multiple AusPost checkout: clearer message when an item is too large to quote, instead of the misleading 'Could not get rate for this postcode'. Root cause of the reported breakage: AusPost rejects any parcel over 105cm, and the demo catalog's 3m threaded rods (length_cm=300) made every postcode 404 once one was in the cart. _auspost_rate now returns (rate, error_message) and surfaces AusPost's own reason; size/weight-limit breaches now show 'Too large for AusPost - choose another shipping option' (via _auspost_error_message), while genuine lookup failures still say 'Could not get a rate for this postcode'. Manual shipping options (Click & Collect, Standard, etc.) remain selectable so the order can still proceed. Updated both checkout-rate call sites (payment-intent + confirm) to the new tuple return, and checkout.html to display opt.auspost_message. No change to rate maths for in-limit parcels.
Web App Rewrote the superadmin /docs page from scratch and reversed the tab order to Developer, API Reference, Admin Guide, Overview (Developer now leads and is the default-open tab). All four tabs' prose was rewritten developer-first: Developer covers stack, file structure, the file-per-tenant multi-tenant model and catalog.db tables, upload pipeline, HTMX patterns and the slug/clearFilters/IIFE gotchas, the CSS variable system, deploy workflow with SCP path table, and security notes. Corrected stale facts against the live code: _CONFIG_EDITABLE now lists the actual set (added contact_font_size/contact_font_color, removed theme), the cart gate is tenant_manager.cart_is_active(), and app.py is ~6,000 lines. No app behaviour change, docs template only.
Web App Removed the temporary embed-back diagnostic scaffolding now that the iOS swipe-back breakout + scroll-on-Back are fixed and confirmed working on iOS Safari and desktop Chrome: deleted templates/partials/_diag_logger.html and its two includes, the demo-only /c/<slug>/_diag sink route, and its CSRF exemption in app.py. No behaviour change to the actual fixes.
Web App Embed scroll-on-Back, real fix (anchor restore). Reverted the embed document Cache-Control back to no-store -- the diag log proved iOS never bfcaches the cross-origin iframe (every swipe-back is a full reload, pageshow persisted=false), so the cache header was never the lever. Instead: when a product is opened in the embed we now remember its slug (sessionStorage sd-embed-anchor); on the Back reload, after the category list rebuilds, we scroll that product's row back into view via scrollIntoView({block:'center'}). Inside the iOS-expanded iframe that scrolls the HOST frame (the only scroll that works cross-origin, since the iframe's own scrollTop reads 0); on desktop it scrolls .main-content. Covers all three return paths (filter rebuilt via flag, filter already in URL, and All Products). Pixel-scroll restore kept as the desktop fallback when no anchor is stored. Returns you to the exact product you tapped. Files: app.py + catalog.html. Logger still on.
Web App TEST: embed document Cache-Control switched from no-store to no-cache. The diagnostic log showed iOS Safari paints the correctly-scrolled catalog on swipe-back, then no-store forces a fresh network reload that jumps to the top -- i.e. our header was defeating Safari's native bfcache scroll restore. no-cache still revalidates on real network use (so Chrome can't serve stale code, the original reason for no-store) but ALLOWS bfcache, so Safari's in-memory restore (scroll intact) can stand. Reconstruct block left in place; it doesn't re-run on a persisted/bfcache restore so it won't interfere. Verifying Back on iOS Safari + desktop Chrome with the logger before settling. Known-good restore point: git commit b453cc1.
Web App Fix for the intermittent iOS embed swipe-back 'breakout' (diagnosed from the server logs): the breadcrumb/cart/checkout 'Back to catalogue'/'Browse products'/category links in the embed were built with tenant_catalog + embed_qs, producing /c/<slug>/?embed=1 -- a SECOND URL serving content identical to the iframe's /c/<slug>/embed src. Those duplicate history entries are what iOS Safari was promoting to the top window on the back gesture (landing on sheetdropper.com directly). Added a catalog_url(slug, embed, **params) template global that routes catalogue links through the /embed route in embed mode (standalone unchanged), so the iframe history now only holds a single /embed-family URL. Output is byte-identical (tenant_catalog?embed=1 just delegated to tenant_embed); only the URL the links target changed. Logger left running to verify. Files: app.py + product_page.html, cart.html, checkout.html.
Web App Diagnostic (demo only): the embed-back breakout logger now ships each event to the SERVER via sendBeacon instead of only localStorage, so the logs can be read directly over SSH (no manual copy/paste, which was failing on iOS). Added a demo-gated endpoint /c/demo/_diag (POST appends a JSON line to diag_log.jsonl; GET returns it as plain text, ?clear=1 wipes it). sendBeacon survives the navigation that triggers the breakout. Still touches no back/history/scroll code. Remove after diagnosis.
Web App TEMPORARY diagnostic (demo tenant only): added an on-page event logger (templates/partials/_diag_logger.html, included in catalog.html and product_page.html behind slug=='demo') to investigate the intermittent iOS swipe-back 'breakout', where the cross-origin embed iframe's URL gets promoted to the top window (lands on sheetdropper.com directly). It logs each load/pageshow/pagehide/popstate/htmx swap with a per-load id, nav type, the 'sd-embed-return' flag, scroll position, referrer, and crucially whether the document is running as the TOP window (the breakout signal). Writes to localStorage and shows a copyable pane via a red LOG button. No changes to the back/history/scroll code. Remove after diagnosis.
Web App Fixed the embed toolbar Categories button not opening the drawer. The document 'tap outside to close' handler only whitelisted clicks inside #mobile-bar; now that the trigger moved to the toolbar (.embed-cat-btn), the same click that opened the drawer was immediately treated as an outside click and closed it. The handler now also treats .embed-cat-btn as a drawer trigger, so the drawer opens and stays open.
Multiple Refined the embed mobile controls. Removed the duplicate Cart button and the Filter button from the embed inline bar, and hid that bar entirely in embed. The Categories drawer trigger now sits inline on the toolbar row next to the existing Cart button (same .cart-btn height/styling), so the controls are compact and on one line instead of the oversized full-width buttons. Filters remain reachable via the Filter tab inside the drawer. The toolbar Categories button is embed-mobile only (hidden on desktop embed, which shows the real sidebar). Standalone is unchanged. CSS v=118 -> v=119.
Multiple Fixed the mobile Categories/Filter drawer being completely absent in the embed. Two problems: (1) the mobile bar AND drawer were inside the same {% if not embed %} block as the footer, so they were never rendered in embed at all -- that's why 'nothing was there'; (2) even rendered, position:fixed can't work inside the embed iframe on iOS, which expands the iframe to its full content height and scrolls the host page, so a fixed bottom bar pins to the bottom of all 1640 rows, off-screen. Fix: the mobile bar + drawer now render for embed too, moved to the top of the catalog content. In embed they are laid out INLINE (no position:fixed) -- the bar sits at the top and the drawer expands in flow, pushing products down, so it works regardless of the iOS iframe expansion. Standalone keeps its fixed bottom sheet unchanged. Also: selecting a category/subcategory now auto-closes the drawer (it stays open only when a tap just expanded a category to reveal its subcategories). CSS v=117 -> v=118.
Web App Fixed the mobile Categories/Filter drawer not appearing inside the embed iframe on iOS. iOS Safari ignores an iframe's fixed height and expands it to its full content height (the host page scrolls), which breaks position:fixed inside the iframe. The bottom bar survived because it's purely position:fixed, but the drawer also had a transform (translateY) for its slide animation -- and on iOS, position:fixed + transform makes the transform the containing block, so the drawer was positioned off-screen and never visible. Removed the transform: the drawer now slides up via the `bottom` property (bottom:-85vh -> 0) instead of translateY, keeping fixed positioning intact. Standalone unchanged. CSS v=116 -> v=117.
Web App Mobile bottom-bar Cart button text is now centered. The Cart control is an <a> anchor (text-align defaults to start) while the Categories/Filter controls are <button> elements (center by default), so Cart's label sat left-justified and out of line with the others. Added text-align:center to the shared .mobile-bar-btn rule so all three match. CSS v=115 -> v=116.
Template Embed Back fix CONFIRMED working across categories (Bolts, Drill Bits, chained switches -- scroll + category + nav all restore on Back, no refresh needed). Removed all debug scaffolding: the demo event logger, the server-rendered debug pill, the diag() logging and recon-start/recon-apply/recon-sample samples. Left in place the actual fix: no hx-push-url in embed (URL stays /embed so Back is a clean reload), Cache-Control: no-store on the embed document (Back always loads current code), and the localStorage filter+scroll reconstruct (flag set on product click, re-applied on the Back reload with nav re-assert).
Template Embed Back THE fix (confirmed by per-load logging): hx-push-url was the culprit. In the embed iframe, clicking a category pushed a same-document pushState history entry (/products?category=X); on Back the browser restored that state FROM MEMORY without reloading or running any scripts, so the reconstruct never fired (no pill, no scroll, stale). The All-Products path always worked because nothing pushed, so the URL stayed /embed and Back was a clean reload. Fix: drop hx-push-url entirely in embed mode (standalone keeps it). Now category/filter changes are pure HTMX swaps with no history manipulation, the iframe URL stays /embed, every Back is a clean reload, and the localStorage filter+scroll reconstruct runs -- same mechanism as the working All-Products case. Combined with the embed-document no-store from the prior entry.
Web App Embed Back ROOT CAUSE of why fixes 'only worked on hard refresh': Chrome was serving a STALE cached copy of the /c/<slug>/embed document on browser Back (the response had no Cache-Control, only Vary), so the iframe ran OLD inline JavaScript on Back and none of the client-side Back-restore code executed -- only Ctrl+Shift+R bypassed the cache and ran current code. Tell-tale: the freshly server-rendered debug pill was absent after Back but present after a hard refresh. Fix: send Cache-Control: no-store on the embed DOCUMENT response (tenant_embed) so Back always fetches current code; the HTMX product-table fragment is left cacheable. This lets the filter+scroll reconstruct actually run on Back.
Template Embed Back diagnostics round 3: the debug pill is now server-rendered so it is present on EVERY load (including the Back), removing the need to hard-refresh to see it -- which was contaminating tests (the refresh was a fresh load that showed the post-reconstruct or All-Products state, not the Back's actual state). Logger now wires the existing element instead of creating one, and tags every entry with a per-load id so loads can be told apart. Lets us capture the Back load's own log directly. Diagnostic only.
Template Embed Back diagnostics round 2: confirmed reconstruct reads the right saved scroll (970) and applies it (holds at +76ms), but the position resets to top later with no JS event -- likely late layout settling (thumbnail image load reflow). Now applies scroll up to 150ms then logs observe-only samples at 300/600/1200/2500/4500ms to pinpoint when it resets. Also made the demo logger pill persistent (re-creates on htmx:afterSettle and pageshow) so it no longer needs a manual refresh to reappear. Diagnostic only.
Template Embed Back diagnostics: category reconstruct on Back now works (nav + product list restore correctly), but scroll still lands at top. Added temporary diagnostic logging (recon-start with the localStorage scroll value being read, and recon-apply with the wanted vs actual scrollTop after setting) to the embed reconstruct, written into the demo event log only, to pinpoint why the saved scroll isn't applying. No behaviour change.
Template Embed Back fix v4 (timing): the reconstruct correctly fired on Back (flag worked, nav briefly showed the right category) but its htmx.trigger ran at DOMContentLoaded -- before htmx finishes initialising (htmx loads deferred), so the trigger was lost: the product list never reloaded to the saved category and the nav reverted. Now the reconstruct defers its htmx trigger until htmx has processed the body (htmx:load event, with a 350ms fallback), and re-asserts the active/expanded category after the table swaps in so nothing reverts it. Scroll is restored on that same post-swap step. Embed-only.
Template Embed Back fix v3: trigger the filter+scroll reconstruct off a sessionStorage flag set when a product is opened from a filtered view, instead of Navigation Timing type. The event logger revealed the forced /embed reload-on-Back reports navType 'reload' (same as a manual refresh), so the previous 'back_forward' check never fired. The flag ('came back from a product') survives the product navigation and Back reload and is unambiguous, so reconstruct now reliably re-applies the saved category/filter/scroll. Embed-only; standalone untouched.
Template Embed Back fix v2 (storage reconstruct). Root cause proven via the event logger: the cross-origin embed iframe reloads its hardcoded src (/c/<slug>/embed) on browser Back and discards ALL History API changes -- so both hx-push-url and hx-replace-url fail and the filtered view + scroll are lost. New approach that doesn't depend on iframe history at all: the catalog now persists the current filter (category/subcategory/search/sort/layout/attribute filters) plus scroll position to localStorage on every table update, scroll and pagehide. When Back forces the bare /embed reload (detected via Navigation Timing type 'back_forward'), it re-applies the saved filter through HTMX, re-marks the nav active/expanded, and restores the scroll once the table swaps in. Embed-only; standalone untouched. Brief flash of the full list before it rebuilds is expected. Also added navType to the demo event logger to confirm the reload's navigation type.
Web App Demo-only Back-bug event logger: records pageshow/pagehide/popstate/DOMContentLoaded/htmx:historyRestore/htmx:afterSwap and category+product clicks, with scrollTop, scrollHeight, nav active text, product row count, and the current sessionStorage scroll-key value at each. Persists to localStorage (rolling, max 300 entries) so it survives the subframe reload. A tiny floating 'L:N' button bottom-right opens a pane with the JSON pre-selected for copy. Gated to slug == 'demo' so no other tenant is affected. Purpose: catch the random-timing Back failure (correct products + nav reset to All Products + scroll lost) by recording the exact event ordering on a broken run; remove after diagnosis.
Web App Fixed the flaky catalog scroll-restore / nav-desync on Back. Root cause was htmx's whole-<body> history snapshots: (1) the snapshot was poisoned by the client-side accordion (clicking a category mutates the nav synchronously before htmx saves the outgoing snapshot, so Back replayed the WRONG category expanded/active over the right products); (2) restoring a snapshot replaced <body>, tearing off the scroll listeners that were bound to .main-content. Fixes: (a) hx-history="false" on the catalog .layout so htmx no longer caches/replays body snapshots -- on Back it re-fetches from the server, which renders the correct pre-expanded nav (hx-push-url still carries the filter state in the URL, so this works). (b) Rewrote the scroll script to bind all listeners to document/window (never to .main-content, which can be replaced), using a document-level capture scroll listener; restore now also runs on htmx:historyRestore. (c) Changing category/filter now resets scroll to the top (htmx:afterSwap on #product-table-wrap), so you no longer land mid-list in a newly selected category. (d) Dropped the pageshow(persisted) cache-clear that could discard a saved position when a subframe bfcache restored the DOM but not the scroll. Verified on the standalone catalog; embed-iframe is the primary target.
Web App Catalog now restores your scroll position when you view a product and click Back. The product list scrolls inside .main-content (not the window), so the browser only restores it automatically via bfcache -- which works in a normal tab but NOT inside the cross-origin embed iframe, where Back reloads the catalog fresh and the position was lost. Added a small script in catalog.html that saves .main-content.scrollTop to sessionStorage (throttled on scroll + on pagehide) keyed by the active filter state, and reapplies it on a fresh load. The key is built from the filter query with the cache-buster `v` and the `embed` flag stripped, so it still matches after Back goes through the /c/<slug>/products partial-URL -> /c/<slug>/ redirect. On bfcache restores (normal tab) the browser handles scroll itself and the script just clears the stale saved entry, so that path is untouched. Verified on the standalone catalog; the embed-iframe case is the primary target.
Web App Product table now shrinks columns to fit the available width and only shows a horizontal scrollbar when it genuinely cannot fit. Two fixes, found by inspecting the live computed widths with a browser: (1) The attribute-cell wrap rule was a dead no-op -- `.col-attr {white-space:normal}` (specificity 0,1,0) was always overridden by `.product-table td {white-space:nowrap}` (0,1,1), so attribute values never actually wrapped and the table was stuck at max-content (1152px in the Bolts test) and overflowed. Re-targeted as `.product-table td.col-attr {white-space:normal}` so it wins the cascade; values like 'Hot-Dip Galvanised' now wrap to two lines and the table collapses toward its true minimum. (2) The horizontal scrollbar lived on .table-scroll, which is as tall as the whole product list (~54,000px for 800 rows), so the scrollbar sat far below the fold and was unreachable. Moved horizontal overflow to .main-content (the fixed-height scroll region): .main-content overflow-x hidden -> auto, and .table-scroll overflow-x auto -> visible. Result: wide screens fill with no scrollbar; when columns can't shrink enough the table overflows and a reachable scrollbar appears at the bottom of the visible area. No fixed table-layout, no per-column max-width -- columns stay fully dynamic. Verified at 1800/1280/1000px on the standalone demo catalog. CSS v=114 -> v=115.
Web App Table column headers are now always one line. The word-wrap rule (white-space:normal) was shared by both the attribute data cells and their header cells via .col-attr, so narrow headers (Size, etc.) wrapped their sort arrow onto a second line. Split the rule: .col-attr (data cells) keeps white-space:normal so values still wrap; added a separate th.col-attr rule with white-space:nowrap so headers stay on one line. Data-cell wrapping is unchanged. CSS v=113 -> v=114.
Web App Reverted the table-layout:fixed approach to the product table -- the table is meant to stay dynamic (columns auto-size to content). Removed table-layout:fixed, the col-name 260px / col-price 96px widths, and the mobile table-layout:auto override; col-name is back to min-width:200px. Kept .col-attr white-space:normal so attribute values can wrap. CSS v=112 -> v=113.
Web App Proper fix for the product table overflowing its right edge. The previous white-space:normal change alone did nothing because .table-scroll has overflow-x:auto, giving the auto-layout table unbounded horizontal room -- so it never felt width pressure and kept wide columns (e.g. Finish = 'Hot-Dip Galvanised') on one line and overflowed; the only visible effect was the narrow Size header's sort arrow wrapping. Switched .product-table to table-layout:fixed so columns share the 100% width and actually wrap. Gave col-name a 260px width and col-price 96px; col-img keeps its 52px; the remaining attribute columns split the leftover space equally and wrap as needed. No per-column max-width (per request). Mobile (<=700px) keeps table-layout:auto so the existing clamped-name + horizontal-scroll behaviour is unchanged there. CSS v=111 -> v=112.
Web App Fixed the product table overflowing its right edge on categories with long attribute values (and on All Products, which shows every attribute column at once). Attribute cells were white-space:nowrap, so values like 'Hot-Dip Galvanised' could never wrap; with the width:100% auto-layout table, the summed natural width of all the nowrap columns exceeded the container and the last columns spilled off-screen (worse inside the embed iframe). Flipped .col-attr to white-space:normal so attribute values wrap onto multiple lines and the table compresses to fit -- columns may shift width but no longer overflow. Headers are short attribute names so they're unaffected; the product-name column already wrapped. No max-width added (per request, columns are free to move within the table). CSS v=110 -> v=111.
Web App Category nav now pre-expands the active category on load. When the catalog is opened on a filtered URL (e.g. ?category=Bolts or a subcategory link), category_nav.html server-renders that group already expanded and marks the correct link active, instead of always hardcoding 'All Products' as active with every group collapsed. So a shared/linked category looks complete on arrival. Done entirely in the template (cat_active = cat.category == category drives the expanded class, the subcat-list display, and active highlighting on the category/subcategory link); both the standalone and embed routes already pass category/subcategory, so no Python change. Works with the accordion click behaviour unchanged. Bumped the bolts embed iframe ?v= to pull the fresh template.
Web App Category nav is now an accordion: only the active top-level category stays expanded, the rest collapse. Clicking a category expands it and closes all others (clicking it again collapses it); clicking 'All Products' collapses everything; clicking a subcategory keeps only its parent group open. Previously each category toggled its own subcategory list independently, so several could be open at once. Pure JS in catalog.html (collapseAllCatGroups / expandCatGroup helpers + reworked the click handler's toggle block); the CSS .cat-group.expanded toggle-arrow rotation was already in place. No CSS version bump.
Web App Theme code cleanup after the colour/layout fixes. 1) Renamed the second theme 'mosaic' -> 'bulletin' everywhere (CSS selectors + thumb class, app.py THEME_PRESETS key + allowlist, admin picker tile/label). The downloadable xlsx template already called it 'bulletin', so picking it from a spreadsheet previously produced data-theme=bulletin with no matching CSS and silently fell back to default styling -- the theme was unreachable that way. Now consistent end to end. Migrated the two live tenants storing theme='mosaic' (metro-industrial, steel) to 'bulletin'. 2) Preset colours are now a single source of truth: app.py THEME_PRESETS is passed to admin.html via tojson instead of being duplicated as a hardcoded JS object, so the values can no longer drift. 3) Removed the now-dead catalog_layout key from the presets (the view is the admin's own choice and is skipped on apply). CSS v=109 -> v=110.
Web App Fixed Default View (grid/table) being ignored in embed mode. The /c/<slug>/embed route rendered catalog.html without passing the layout/default_layout variables, so product_table.html fell back to its hardcoded 'table' default and the tenant's grid setting was silently ignored on any embedded catalog (e.g. the bolts demo iframe). Embed route now computes layout from config.catalog_layout (honouring an optional ?layout= override) exactly like the standalone catalog route, and passes layout + default_layout into the template. The standalone route was always correct; only the embed path was missing it.
Web App Theme/colour precedence fixes (bugs from testing). 1) Removed the Light/Normal/Dark display mode entirely -- it was the root cause of two bugs: the body[data-mode] CSS (specificity 0,1,1) overrode the per-tenant custom colours emitted on :root (0,1,0), and the default 'light' mode recomputed --bg/--surface/--primary purely from --primary-base + a hardcoded near-white, so the Page Background and Table Background colour pickers were silently ignored. Catalog now renders straight from the tenant's chosen colours (or theme preset). Dropped the data-mode body attribute, the 23 body[data-mode] CSS rules, the Display Mode admin radios, the theme_mode config field/validation/default, and the theme_mode column from the downloadable xlsx template + docs. The sheetdropper theme's dark look is unaffected (hardcoded in body[data-theme=sheetdropper]). 2) Default View (table/grid) is no longer overwritten by theme presets. Picking a theme + 'Apply colours' previously reset catalog_layout to the theme's default, so the admin's view choice silently reverted. applyPresetToForm (client) and apply_theme_preset (server) now apply colours only and leave the view alone. 3) Active theme in the admin picker is now clearly highlighted -- the amber highlight override only matched :has(input:checked) (no radios in this picker), so the active .theme-option-active fell back to a faint colour; added .theme-option-active to the admin amber rule with a heavier ring and bold label. CSS v=108 -> v=109.
Web App DB audit follow-ups (Phase 2). 1) New cleanup_pending_orders.py iterates all tenants and deletes pending_order_data rows older than 24h -- customer PII from abandoned Stripe checkouts no longer accumulates forever. Cutoff built with strftime('%Y-%m-%dT%H:%M:%f', 'now', '-24 hours') to match the stored isoformat (T separator, microseconds); the naive datetime('now',...) form uses a space separator and would silently match zero rows. Wired into root crontab at 16:30 UTC daily, 30min after the existing demo-orders job to avoid WAL contention. 2) _finalise_order order-number generation switched from COUNT(*)+1 to MAX(CAST(SUBSTR(order_number, 4) AS INTEGER))+1, so deleting an order can no longer cause a collision on the orders.order_number UNIQUE constraint when the next order is created.
Web App Embed dead-cart fix follow-up: after a successful Stripe Checkout payment, the iframe now redirects the customer back to /c/<slug>/embed (the catalogue) instead of leaving them parked on the dead checkout form with a success notice. The customer already saw the order confirmation page in the now-closed Stripe tab, so dropping them back on the half-completed form was the wrong destination. The unpaid-close behaviour (re-enable Pay button, hide the in-progress notice) is unchanged.
Web App Embed mode dead-cart fix: when the customer closes the Stripe Checkout tab, the iframe now reconciles automatically. handlePayEmbed saves the opened window reference and the cs_xxx session id, then polls stripeTab.closed every 1.5s. On close, fetches new endpoint /c/<slug>/cart/check-payment?session_id=... which retrieves the Stripe session, idempotently runs _finalise_order if needed, and returns {paid, public_token}. If paid: iframe clears cart + cartShippingClear, hides the Pay button, shows a green success notice with a View-your-order link. If not paid: iframe hides the in-progress notice and re-enables the Pay button so the customer can retry. /cart/checkout-session response now includes session_id alongside url. No cross-tab postMessage, no cross-partition localStorage -- works regardless of browser storage partitioning.
Web App Pentest fix 6: Tenant admin logout and superadmin logout now require POST + CSRF token. Previously GET-based -- a malicious third-party page could log a logged-in admin out via <img src='https://sheetdropper.com/c/<slug>/admin/logout'>. Admin nav templates updated to use a tiny inline form. Tests adjusted; new test_logout_rejects_get covers the 405 on GET.
Web App Pentest fix 5+7: Stripe Connect onboarding callbacks now use single-use nonces instead of putting the long-lived API key in the URL. /api/cart/connect mints two nonces, one for refresh_url and one for return_url (the ?state= param Stripe forwards). /api/cart/connect/refresh consumes the refresh nonce and mints a new one. /c/<slug>/cart/connect/done now requires a valid state nonce bound to the slug -- without it the page renders nothing and does not call Stripe or write to the DB. New connect_nonces table in tenants.db (auto-migrated). Closes the API-key-in-URL leak (logs, Stripe stored AccountLinks, browser history) and the anonymous-Stripe-trigger gap on /done.
Web App Pentest fix 3: Rate limits on /forgot-password (10 per IP per 5 min, AND 3 per target email per hour) and /contact (5 per IP per hour). Past the threshold the response looks identical to a successful submit so an attacker cannot probe the limit. Extracted a generic _check_and_record helper from the existing failed-login limiter. Defends against ZeptoMail quota burn and email-bombing of tenant inboxes.
Web App Pentest fix 1 (CRITICAL): Public order confirmation page is now keyed by an unguessable per-order token instead of the sequential SD-XXXX order number. Old route /c/<slug>/order/<order_number> returns 410 (hard cutover). New route /c/<slug>/order/t/<public_token>. Order number stays human-readable on receipts; only the URL changes. orders table gets a public_token column (auto-migrated, backfilled with secrets.token_urlsafe(16) per row, UNIQUE INDEX). _finalise_order mints a fresh token at order creation. tenant_cart_confirm_order JSON now returns public_token; checkout.html JS redirects accordingly. Closes the unauthenticated PII enumeration: anonymous internet user could iterate SD-0001..SD-9999 on any tenant and harvest customer names, emails, phones, addresses, and pickup codes.
Web App Pentest fix 8: image_url / image_2_url / image_3_url columns are now scheme-allowlisted at parse time. Only http://, https://, and relative paths (/ or /static/) are kept; javascript:, data:, file: and other schemes are dropped with a per-row warning. Defense-in-depth: not exploitable today (image_url only lands in <img src> where browsers don't execute javascript:) but stops future template changes from silently turning this into XSS.
Web App Pentest fix 4: Apostrophe-prefix formula-injection payloads on xlsx export. Any cell value starting with =, +, -, @, or tab gets a leading single quote when written to the downloaded xlsx. Excel/LibreOffice render the cell as plain text instead of evaluating it as a formula. Closes the upload->download round-trip CSV injection: an attacker who can write into a tenant's catalog (any text field) could otherwise plant =cmd|... or =HYPERLINK(...) payloads that detonate when the tenant opens their next data export. Applied in converter.products_to_xlsx (data rows + Config tab) and template_generator.fill_config_tab.
Web App Pentest fix 2: Hard 10 MB cap on all request bodies via Flask MAX_CONTENT_LENGTH. Previously /api/upload had no size cap (the web admin path enforced 10 MB but the API path did not), so a 3.7 MB xlsx tied up a gunicorn worker for 40+ seconds -- two such uploads could DoS the whole site (-w 2). Added a 413 errorhandler that returns JSON for /api/* and plain text otherwise.
Web App CSS v=107 -> v=108 to bust caches for the new .header-logout-btn rule used by the POST-based logout form.
Web App Set on_behalf_of on PaymentIntents and Checkout Sessions for live (non-test) Stripe Connect tenants. This makes Stripe Checkout (and the card form) display the tenant's business name and branding instead of SheetDropper's. Note: in stripe_test_mode the merchant name still shows as whoever owns TENANT_TEST_SECRET_KEY (i.e. SheetDropper) -- that's a known artifact of using a shared test key for demo tenants. Real production tenants with their own Connect account will see their own brand.
Web App Fix 500 on Stripe Checkout Session create. Stripe doesn't populate session.payment_intent at create time -- only after the customer pays. Reworked the pending order data flow: checkout-session endpoint now saves pending data keyed by Checkout Session id (cs_xxx). The finalize endpoint retrieves the completed session, gets the real PI id, migrates the pending row from session_id to PI id, then calls _finalise_order. Also verifies payment_status == 'paid' before finalizing -- guards against someone hitting the finalize URL without actually paying.
Web App SESSION_COOKIE_SAMESITE changed from 'Lax' to 'None' so the Flask session cookie travels inside cross-origin iframes (the tenant embed). Without this, POSTs from inside the iframe (cart checkout-session, payment-intent etc) returned 403 because the CSRF middleware couldn't read the session-stored token. CSRF protection still works -- we check the explicit X-CSRFToken header. HTTPOnly+Secure on the cookie remain, so no JS-theft or HTTP-exposure risk.
Web App Embed mode now uses Stripe Checkout (hosted page) in a new tab instead of Stripe Elements. Stripe Elements doesn't survive a nested cross-origin iframe -- the card form rejects confirmCardPayment because storage/cookies are partitioned. Workaround: when in embed mode, the Place Order button POSTs to a new /c/<slug>/cart/checkout-session endpoint, gets a Stripe-hosted checkout URL, and opens it in a new tab. After payment Stripe redirects to /c/<slug>/order/finalize/<session_id> which finalises the order via the existing _finalise_order helper and shows the embed-styled order confirmation. New endpoints: tenant_cart_checkout_session, tenant_order_finalize. checkout.html branches on window.CART_EMBED -- non-embed flow still uses Elements unchanged. Customer briefly sees checkout.stripe.com URL during payment (industry standard for embedded shops) but never sees sheetdropper.com in the original tab. Known papercut: cart in the embed iframe doesn't auto-clear after order placement in the popup -- can add postMessage handoff later.
Web App Embed mode now covers the full purchase flow, not just the catalog. Cart, checkout, product detail, and order confirmation pages now honour ?embed=1: they emit ALLOWALL framing headers (so they render inside a cross-origin iframe instead of being blocked by X-Frame-Options) AND hide the SheetDropper header/footer. All internal links in those templates propagate ?embed=1 via a new embed_qs() Jinja global so the whole journey stays in the same iframe and the same localStorage partition. This fixes two bugs in the bolts/live-products embed: (1) clicking Cart inside the iframe rendered a blank page because /c/<slug>/cart returned SAMEORIGIN. (2) Placing an order failed with 'No valid products found' because navigating to /cart or /checkout broke the visitor out into the top-level sheetdropper.com partition where the localStorage cart was empty or stale.
Web App Bolts live-products iframe now points at /c/demo/embed instead of /c/demo. The /embed route already exists and renders catalog.html with embed=True, which suppresses the SheetDropper header+footer and sets frame-ancestors *. Reverted the demo-specific X-Frame-Options carveout I added earlier -- not needed because the existing _set_embed_headers handles framing for the embed route. Net result: white-label embed for any tenant via sheetdropper.com/c/<slug>/embed.
Web App Fixed 'Upload to Live Site' silently failing after using 'Run AI Review' from the saved-mapping screen. The Run AI Review fetch injects converter_review.html into the page and re-executes its <script> by appending to body; the second execution tried to re-declare top-level const _fieldLabels and threw SyntaxError, killing all the event-listener wiring including the upload button click. Wrapped the script in an IIFE so its top-level declarations are scoped per execution.
Web App Fixed 'Forbidden' error when clicking the 'rerun AI' button on the saved-mapping converter screen. The fetch() to /convert/ai-review sent no CSRF token, so the global before_request CSRF check returned 403. Added X-CSRFToken header to the fetch.
Web App AI converter v2: wired the second-level cleaner into the saved-mapping fast path too. Re-uploading a file when a saved column mapping exists previously rendered converter_saved.html, which bypassed the cleaner entirely -- so blank-sentinel detection and swap/parse suggestions never ran for repeat uploads (which is most uploads). The saved-mapping branch in tenant_admin_convert now builds a mapping_result shape from the saved data and calls run_second_level_cleaning before rendering. converter_saved.html now contains the same Data Cleaning section markup as converter_review.html. Form fields match so tenant_admin_convert_confirm parses cleaner_* fields identically from either template.
Web App AI converter v2 test pass + fixes. Verified the second-level value-cleaning pass against the synthetic stress test (column swap, blank sentinels, combined fastener) and the 1640-row Nerang catalog. Three fixes shipped: (1) Fixed _FASTENER_RE so that bare 'M10x40' parses as diameter+length(40mm), not diameter+thread_tpi=40. The TPI branch now requires the literal '-' separator; an 'x' separator always denotes length (or pitch x length for the full M8x1.25x30mm form). parse_fastener now also returns pitch_mm. (2) Added _deep_sample_rows: cleaner now strides across the full sheet (up to 500 evenly-spaced rows) instead of relying on the 15-row preview from extract_sheet_data. Without this, em-dash blanks in Nerang's Thread/Drive Type/Finish columns were invisible because they start at row 886 of 1642. With the stride sampler, blank-sentinel detection fires on all four expected columns. (3) Replaced an &mdash; entity in converter_review.html with a colon (no em dashes in user copy). Test outcomes: Haiku was NOT called (no ANTHROPIC_API_KEY in the test env) so the regex pre-filter path was exercised end-to-end. Swap detection fired on the synthetic. Fastener split applied correctly: synthetic produced Diameter + Length mm attribute columns with correct per-row values. After swap, Size column ended up holding Coarse/Fine and Thread held M-sizes.
Web App AI converter v2: second-level value-cleaning pass. After the column-mapping AI runs, a new module (second_level_cleaner.py) profiles the cell values in each mapped column and surfaces two things in the converter review screen. (1) Auto-applied blank-sentinel detection: cells containing dash, em/en dash, N/A, n/a, nil, none, null etc are treated as empty so they don't become filter options. Skipped for free-text fields and SKU-ish columns. Operator confirms with a single hidden checkbox baked into the review form. (2) Suggestions: column-swap candidates (e.g. the Nerang case where a Thread column holds M-sizes and a Size column holds coarse/fine words) and combined-fastener parse candidates (M8x1.25x30mm -> split into Diameter, Thread TPI, Length (mm) attribute columns). Each suggestion has radio buttons; defaults follow the pre-filter + Haiku confirmation. Haiku is only called when the regex pre-filter found ambiguity; failures collapse silently. apply_human_mapping now takes cleaning_decisions and applies blank-normalisation, header swaps, and synthetic split columns. Reuses existing converter-section-details visual language; no CSS bump.
Multiple GST rate format changed from decimal (0.10 = 10%) to percent (10 = 10%). The number you type is the percent now -- no more divide-by-100 trap. Affects: spreadsheet uploads, AI converter (prompt updated, value clamped to 0-100 with optional % suffix stripped), cart math, product page price display, catalog table price display, template help text, docs, guide. Site default still 10%. Existing fake-tenant catalog DBs not migrated -- re-upload xlsx to refresh.
Template Nerang Bolts & Nuts demo: refreshed 51 new product/subcategory images. Generator now supports per-material variants for hex bolts (zinc / stainless / galvanised), per-shape shackles (bow / D), per-product swage terminal types, hole-saw arbors as individual products, and 8 nail types. Output: nerang-bolts-catalog-04.xlsx, downloadable at https://sheetdropper.com/static/downloads/nerang-bolts-catalog-04.xlsx. Server image store now 71 files at /static/images/demo/.
Web App Real fix for the back-button broken-catalog bug: added Vary: HX-Request header to all responses. Without it, browsers cache the HTMX partial response under the URL and serve it as the page document on back-nav, leaving the visitor on a bare product table. The Vary header tells the cache to key on HX-Request, so the AJAX partial and the full-page response are stored separately. Reproduced reliably after an upload with images: first time only, because once images are cached the catalog page is fast enough for bfcache to kick in and bypass the HTTP cache.
Web App Fixed catalog back-button bug: when a visitor filtered the catalog, opened a product, then hit back, they sometimes landed on a bare product table with no header, sidebar, or filters. Cause: HTMX's history-restore fetch (used when its localStorage snapshot has been evicted) hit /c/<slug>/products and got the partial. The endpoint now detects HX-History-Restore-Request and serves the full catalog page instead. tenant_catalog also now honours ?layout=table|grid from the query string so the visitor's grid/table choice survives history restore.
Template Nerang Bolts & Nuts demo catalog (1640 SKUs) wired with images: 42 subcategory thumbnails + 17 individual product images uploaded to /static/images/demo/ on sheetdropper.com. Generator (bolts/generate_nerang_catalog.py) now applies a subcategory thumbnail to every row, with per-product overrides for branded silicones/adhesives/chemical resins. xlsx available at https://sheetdropper.com/static/downloads/nerang-bolts-catalog.xlsx for upload to the demo tenant.
Template Catalog price column slimmed: removed the price unit line (each/pack/etc) and the 'ex GST' line from both table and grid views. Cart was making the column 4 lines tall when enabled. Unit and GST info still shown in cart and checkout. Underlying data unchanged -- unit still on product page and in cart line items.
Template Added a tiny analytics beacon (track.js, async/defer) to public-facing templates (landing, demo, contact, guide, docs, legal, changelog, signup, login, forgot/reset password, signup_success). Beacon POSTs the visited path + cross-origin referrer to https://w3swaps.com/api/track so visitor stats roll up into the w3swaps admin Analytics tab. No PII -- visitor uniqueness uses a daily-rotating salted hash of IP+UA. Admin pages and superadmin pages intentionally not tracked.
Template Template overhaul: removed all 'required field' indicators (stars, REQUIRED labels, red header fills, yellow row highlighting) per house style. Added stock_out_behaviour and auspost_from_postcode to the Cart Settings section with dropdowns and notes. Added a Cart & Checkout subsection to the User Guide explaining how the teal product columns and cart settings tie together, and noting that shipping options are configured in admin, not the spreadsheet. Fixed font_color default mismatch in the guide (was #1a1a1a, actual default is #333333). Updated business_info description to mention ABN as a use. Body Settings guide now lists catalog_layout, theme, and theme_mode. Removed every em dash from headings, descriptions, comments, and legends.
Web App Market theme: reduced table-meta row padding from 6px to 3px top/bottom.
Web App Market theme: fixed cart button and grid/table toggle not showing. The table-meta was absolutely positioned into the filter strip but was being covered by the sidebar (z-index stacking context issue). Changed to static positioning so it sits naturally above the product list.
Web App Checkout: big red warning banner when Stripe is not connected -- tells visitors orders won't be charged and directs the store owner to the admin panel to connect Stripe. Banner only shows when neither stripe_connect_charges_enabled nor stripe_test_mode is set.
Web App Cart visibility fix: cart_free tenants now show the cart UI even without Stripe Connect or test mode configured. Previously the cart was silently hidden for all non-demo cart_free tenants because the Stripe condition was never met.
Web App Grid view: fixed 5 columns on desktop, 2 columns on mobile (<=600px). Replaced auto-fill minmax with explicit column counts.
Web App Cart: Clear Cart is now a single click with no confirmation -- clears localStorage and reloads immediately.
Web App Cart: Clear Cart no longer uses a native confirm() dialog (Chrome silently blocks location.reload() after confirm() consumes the user activation). Replaced with inline two-click confirmation -- first click changes button text to 'Are you sure?' in red, second click clears and reloads the page. Auto-resets after 3 seconds if not confirmed.
Web App Cart: Clear Cart now reloads the page after confirming, so the empty state is immediately visible.
Web App Cart button: moved from sidebar to the table-meta bar (next to grid/table toggle) as a proper styled button with item count badge. Removed sidebar cart link. Product page: cart nav button added to breadcrumb row; Add to cart changed from underline link to a solid primary-coloured button. Cart button also appears on product pages via breadcrumb row.
Web App Grid view: fixed broken card layout caused by nested anchor tags. Product card changed from <a> to <div> with a stretched-link pattern -- the product name is now the anchor with a CSS ::after overlay covering the full card, so the whole card is still clickable. Cart link and attribute tags get z-index:1 to sit above the overlay. Fixes the image-beside-text two-card rendering bug on tenants with cart enabled.
Web App Superadmin nav: consistent header links on Dashboard and Finance pages — Dashboard / Finance / Docs / Changelog / Logout on both. Current page highlighted as active. Docs and Changelog hidden on mobile.
Web App Finance compliance + automation pass: (1) Expense categories -- new fixed dropdown on add-expense and recurring-expense forms (Hosting, Software, AI/API, Domains, Marketing, Professional, Banking, Other), shown as a column in both tables, included in expenses CSV; DO auto-pulled invoices tagged 'hosting' automatically. (2) GST threshold tracker -- new bar above stats showing rolling 12-month Stripe gross income vs A$75,000 threshold; green/amber/red traffic light at $0-60k, $60-75k, and $75k+ (must register within 21 days). (3) EOFY Pack one-click download -- new amber 'Download EOFY Pack' button next to FY selector; produces sheetdropper_eofy_FYxxxx.zip containing income.csv, expenses.csv (with categories), summary.html (printable P&L with ABN, FY dates, category totals, taxable profit), and README.txt with tax-time instructions. Hand the zip to your accountant or attach to myTax.
Web App Finance automation: DigitalOcean invoices now auto-pull on page load once per calendar month (tracks last pull in finance_settings.json, no manual button needed). Recurring expenses system: define a template once (description, amount, frequency, start date) and entries auto-generate on every page load for any months not yet covered. Frequencies: monthly, quarterly, biannual, annual. Delete recurring template with optional bulk-delete of all its generated entries. DO manual pull button removed.
Web App Anthropic API cost tracking: every AI import call now logs input tokens, output tokens, and calculated USD cost to data/api_costs.db. Pricing: claude-haiku-4-5-20251001 at $0.80/M input + $4.00/M output. Finance page shows Anthropic API section for selected FY with per-call log (timestamp, model, tokens, cost) and FY total in USD. Logging is silent -- never breaks the import flow.
Web App Finance page: DigitalOcean and Cloudflare API integrations. Settings panel (collapsible) stores DO + CF tokens in data/finance_settings.json. Pull DigitalOcean button imports invoices for selected FY as expenses (de-duplicated by invoice UUID, tagged with DO badge). Cloudflare domains panel shows all registered domains with registered date, expiry date, days until renewal (colour-coded green/orange/red), and auto-renew status. CF domains sorted by days remaining.
Web App Superadmin mobile pass: stats bar collapses to 3-col (2-col at 480px), info row stacks vertically, create form wraps to 2-col (1-col at 480px), action buttons wrap, header padding reduced, Docs/Changelog links hidden on mobile (Finance + Logout kept), pill hidden. Same mobile treatment applied to Finance page.
Web App New Finance page at /superadmin/finance. Pulls live income data from Stripe balance_transactions API (gross, Stripe fees, net per charge). Manual expense log stored in data/expenses.json (add/delete, persists across deploys). Financial year selector (July-June, Australian). Summary stats: gross income, Stripe fees, net income, expenses, taxable profit. CSV export for both income and expenses. Finance link added to superadmin dashboard header.
Desktop New installer built and deployed (catalog-manager-setup.exe). Includes updated Help screen: Cart Settings and Orders sections, cart/shipping columns in Spreadsheet Format, cart revenue in Analytics, Import & Backups renamed, AusPost and cart payment troubleshooting, cart tips, Cart & Checkout and Orders support topics.
Web App Legal page: Privacy Policy updated (added end-customer order data bullet, Anthropic AI Import data sharing, order data deletion on account close; all dates to 1 May 2026). Terms of Service updated (billing section now shows all pricing tiers inc annual A$319/yr and cart add-on A$10/mo + A$110/yr; service description updated to mention cart; Cart & Checkout terms moved into new standalone section with merchant of record, Stripe Connect, refunds/disputes, customer data, taxes, prohibited use). New Cart & Checkout section added to sidebar nav. Signup form annual pricing corrected: A$290 -> A$319, A$400 -> A$429 (confirmed against Stripe live price IDs).
Web App Docs page (/docs) full rewrite: Overview updated with Stripe/ZeptoMail stack, demo tenant notes; Admin Guide updated to tab-based layout, full product column table inc cart columns, all 8 desktop app screens, Cart & Checkout section (payment flow, status flow, AusPost, pickup codes); API Reference updated with analytics endpoint, full Orders API, Cart Config API, download-xlsx, support, updated config fields list; Developer tab updated with full file structure, new DB tables (orders/order_items/refunds/pending_order_data), complete route summary inc cart/orders/signup/webhook routes, Stripe webhook security note.
Web App Signup page: replaced passive 'by continuing you agree' fine-print with an active checkbox requiring explicit agreement to Terms of Service and Privacy Policy (both open in new tab). Client-side validation highlights the checkbox if unchecked on submit. Server-side validation added to /create-checkout-session -- returns 400 if agree_terms != '1'.
Template Template generator updates: cart column headers now teal (distinct from standard blue columns); Products tab legend updated to show teal = cart & checkout; User Guide PRODUCTS column intro mentions teal; WHAT THIS APP DOES section mentions cart & checkout add-on and teal columns; upload instruction updated 'Go to Upload' -> 'Go to Import'; custom column description updated 'after the blue columns' -> 'in the green section'.
Desktop Desktop Help screen rewrite: added Cart Settings and Orders sections, updated TOC (15 entries), added cart/shipping columns to Spreadsheet Format table (stock_qty, gst_rate, weight_kg, dimensions), added cart revenue section to Analytics, renamed Restore & Backups to Import & Backups with updated copy, added AusPost and cart payment troubleshooting entries, added cart tips, added Cart & Checkout and Orders to support topic dropdown.
Web App Guide page full rewrite: fixed 'catalogueue' typo throughout, updated column reference (added weight_kg, length_cm, width_cm, height_cm, stock_qty, gst_rate, meta_description with cart badge), rewrote admin panel section to reflect tab layout and all tabs (Import, Orders, Analytics, Settings, Cart Settings, Account), added Orders section (status flow, pickup codes, refunds, actions), updated desktop app section with all 8 current screens, expanded shopping cart section (GST, stock, AusPost with weights, pickup code flow), added Orders management sidebar link, added two new FAQ entries (AusPost setup, refunds).
Web App Landing page: removed Coming Soon from Shopping Cart feature card, updated cart description to reflect live Stripe payments, AusPost rate quoting, and order management. Removed all emoji icons from feature cards, how-it-works steps, and industry proof strip.
Desktop New installer built and deployed to sheetdropper.com/static/downloads/catalog-manager-setup.exe. Includes: Import screen (drop zones moved from Dashboard), Cart Settings as plain cards, shipping option column alignment, weight/dimension fields in CATALOG_FIELDS for AI Review, refund confirmation emails, mobile admin polish.
Desktop Cart Settings screen: removed collapsible sections, replaced with plain cards. Cart Configuration and Shipping Options each get their own card. Save button sits as a plain row below the shipping card.
Desktop Rearranged desktop app screens: moved Smart Import and Upload Template drop zones from Dashboard into the Restore screen; renamed Restore nav item and heading to Import. Dashboard now shows status, controls and quick links only. Import screen has drop zones at the top followed by server backups and local backup folder.
Multiple Refund confirmation emails: added _send_refund_confirmation_email() helper that fires after Stripe confirms a refund. Sends customer an email with refunded items, quantities, amounts, and a note that payment will appear within 5-10 business days. Wired into both the web admin refund route and the desktop API refund route. Uses ZeptoMail via ZEPTO_API_KEY same as all other transactional emails.
Web App Mobile CSS cleanup: removed redundant .ord-row.expanded > td:first-child and :last-child overrides (already covered by !important on the base rule); removed redundant grid-column:1/grid-row:1 on td:nth-child(1) (auto-placed there); fixed dead padding-left:4px on chevron cell by adding !important. Also fixed tab spacing (padding 10px 9px) to fit Orders badge, fixed .ord-table > tbody and .ord-table > thead selectors to not affect nested ord-items-table, fixed items table subtotal price misalignment by scoping max-width and nth-child rules to tbody only.
Web App Admin orders: removed (priceUnit) from product name in order items table. Fixed mobile orders expanded detail -- root cause of values running together was .ord-table tbody selector matching nested ord-items-table tbody, changed to direct child selector .ord-table > tbody. Fixed .ord-table > thead to not hide ord-items-table headers. Added font-size:12px, max-width:140px on product column, centered qty values on mobile.
Web App Admin orders expanded detail mobile fixes: all action buttons on one line (smaller font/padding/letter-spacing); customer+delivery address side-by-side (forced 1fr 1fr grid + email truncation); product names fixed (removed overflow:hidden from detail td that was collapsing width chain, changed to white-space:nowrap+ellipsis on fixed-layout table).
Web App Admin orders mobile: col 3 changed from 1fr to auto so status/total shrinks to fit and col 2 (name) gets all remaining space; column-gap bumped to 16px for breathing room between order number and name columns.
Web App Admin orders: fixed .ord-row-time showing on desktop. Moved display:none rule outside the 640px media block so it's globally hidden; media block only shows it on mobile.
Web App Admin orders mobile: fixed unexpanded rows being tall/padded. Root cause: mobile media block at line ~1006 was defined before the desktop .ord-table td { padding:12px } rule at line ~1019, so the desktop rule was winning at equal specificity. Added !important to mobile td padding:0 override to force it.
Web App Admin orders mobile: all order cards now always show with surface2 background and bordered card style (not just when expanded). Expanded state indicated by amber border colour only.
Web App Admin orders mobile: date/time JS split into ord-ts-date and ord-ts-time spans; time hidden from date cell on mobile, shown instead in items cell via ord-row-time span (e.g. '17:30 · 3 items'). Grid back to 2-row auto/1fr/1fr/auto — col1: order#/date, col2: name/time+items, col3: status/total.
Web App Admin orders mobile: reworked collapsed row to 3-row grid (auto 1fr 1fr auto). Order# spans all 3 rows centred in a narrow auto column. Center column: Name row1 / Date row2 / Items row3. Right column: Status row1 / Total row3. Chevron spans all rows.
Web App Admin orders mobile: fixed amber borders on every cell (desktop expanded td border rules now overridden with !important); fixed items table overflow by adding width:100%/box-sizing on detail row td and max-width on ord-detail; product name column uses word-break instead of ellipsis; reduced row padding and gap; detail row amber accent changed to left border only.
Web App Admin orders tab mobile: collapsed rows redesigned as 3-col x 2-row card (order+date / name+items / status+total with chevron); expanded detail gets overflow:hidden, tighter progress bar, email truncation, 2-col detail grid, fixed-width items table columns with product name truncated.
Web App Admin cart shipping options: fixed grid back to 1fr 1fr 65px so all rows align consistently regardless of whether a value field is present; value input narrowed from 80px to 65px (fits 5 digits + decimal).
Web App Admin cart tab shipping options: added Type/Display name/Value column headers above the option rows; cost/postcode input fixed to 80px wide; remove button tightened to 26px; grid changed from 1fr 1fr 1fr to 1fr 1fr auto so pickup/included/none rows (no value field) automatically expand type+name to fill full width.
Web App Admin cart tab: select/dropdown elements now match the height of text inputs (both locked to 34px with box-sizing:border-box). Most noticeable in shipping options rows where the type dropdown was visibly shorter than the name input.
Web App Admin account tab mobile fixes: password inputs constrained to their grid cell width (width:100% box-sizing:border-box) so they no longer overflow the card; embed section Copy Code button drops below snippet on mobile and snippet expands to full width.
Web App Merged Backups tab into Import tab to reduce admin nav from 7 to 6 tabs. Restore Previous Data section now appears at the bottom of the Import tab. Backups tab button removed.
Multiple Mobile layout fixes: cart item table wrapped in overflow-x:auto div so long product names scroll rather than breaking layout; checkout order summary panel moved above payment form on mobile via CSS order:-1 so users see total before paying; admin tab bar now horizontally scrollable on mobile instead of squishing 7 tabs; admin settings rows stack label above input at 700px; orders table wrapped in overflow-x:auto; order progress step labels shrink to 8px at 640px; converter mapping table gets overflow-x:auto. CSS v102.
Web App Code quality fixes from review: moved _num() helper above the row loop in apply_human_mapping (was being redefined on every iteration); fixed products_to_xlsx writing falsy numeric values (stock_qty=0, gst_rate=0) as blank by replacing `val or ''` with explicit None check; fixed desktop API saved-mapping auto-promotion using stale saved sheet name instead of the resolved selected_sheet variable.
Web App Fixed weight_kg/length_cm/width_cm/height_cm/stock_qty/gst_rate still appearing in Section 2 on saved-mapping uploads. Root cause: converter_saved.html had its own catalog_fields list with only the original 9 fields, so Section 1 never rendered dropdowns for the new fields and syncRemainingTable() never hid them in Section 2. Updated catalog_fields and _fieldLabels in converter_saved.html to match converter_review.html.
Web App Fixed saved mapping flow not auto-mapping weight_kg/length_cm/width_cm/height_cm/stock_qty/gst_rate when column names exactly match field names. Both the web and desktop API saved-mapping paths now auto-promote any catalog field that's missing from the saved mapping but present as an exact-name column in the uploaded file, before building col_to_field.
Web App Fixed AI converter not auto-mapping weight_kg, length_cm, width_cm, height_cm, stock_qty, gst_rate -- these fields were missing from the JSON response template in the AI prompt so Claude never returned mappings for them. Added all 6 to the column_mapping block in the prompt.
Web App Fixed Smart Import (AI converter) not saving weight_kg, length_cm, width_cm, height_cm, stock_qty, gst_rate. These fields were missing from _CATALOG_FIELDS in convert/confirm route, not extracted in apply_human_mapping, and not shown in the mapping review UI. All four fixed: converter_review.html now shows all 6 shipping/inventory fields in Section 1; apply_human_mapping extracts them as numeric values; products_to_xlsx writes them as a teal-coloured column group; _CATALOG_FIELDS includes them so the confirm route reads them from the form.
Web App Admin import tab: fixed converter review elements (mapping table, section summaries, AI note, dropdowns, sample values) rendering with light backgrounds and dark text on the dark admin theme. Added body.admin-page overrides in admin.html for converter-ai-note, converter-section-details summary, mapping-table headers/cells/selects, field-select dropdowns, converter-attrs, converter-action-note, sample-vals, and emergency-section background.
Web App Guide page: rewrote Shopping Cart section to reflect live product (add to cart, checkout, shipping options, AusPost rates, pickup/click & collect, order emails, GST). Removed all asterisk placeholders and the 'not yet available' footnote. Updated FAQ cart entry to reflect live feature.
Multiple Removed Reset to New button from order detail on both website and desktop. Desktop orders now update in-place after status changes -- progress bar, status dot in collapsed row, filter counts, Cancel/Refund button visibility all update without closing the expanded order or re-rendering the list.
Web App Orders tab: progress line steps now update in-place without page reload. Clicking a step updates the dot, fill width, status badge in the collapsed row, filter data attributes, and utility button visibility. Order stays expanded. Uses JS ordUpdateInPlace() with IDs on row, status cell, progress bar, cancelled message, and utility button wrappers.
Web App Added cart revenue and order stats to website admin analytics tab: Today/7d/30d/all-time revenue + order counts + avg order value (30d). Only shows for tenants with cart active. Also ensures init_cart_tables is called before analytics loads on the admin page.
Multiple Bug fixes: progress line steps now individually clickable (click any step to jump to that status) on both desktop and website admin. Desktop order detail loading now shows actual error message instead of staying on Loading. Cart metrics section shows whenever orders table exists, not just when revenue is non-zero.
Multiple Phase 4: cart revenue/order metrics on desktop Analytics screen (Today/7d/30d/all-time for both revenue and order count, plus avg order value last 30 days). Order status progress line on both desktop and web admin -- visual timeline replaces individual action buttons, click right 2/3 to advance, left 1/3 to step back, pickup-ready confirms before sending email, cancelled state shows all steps dimmed. Copy Address button added to order detail on both desktop and web admin. Added cart stats query to get_analytics() in database.py.
Multiple Phase 3 cart port: added Orders screen to desktop app. Filter tabs (All/New/Processing/Shipped/Ready for Pickup/Done/Cancelled with counts), clickable order rows that expand inline, full detail panel with customer info, items table with refund indicators, totals, internal notes, and action buttons. Context-sensitive forward-progress buttons (Mark Processing/Shipped/Delivered/Picked Up/Ready for Pickup), Print (writes temp HTML file opened in browser, marks new as processing first), Refund panel with per-item qty inputs + Refund All, Reset to New, Delete. Added GET /api/orders/<num>/print-html server endpoint.
Multiple Cart Settings fixes: api_cart_config now uses cart_is_active() (respects cart_free flag for demo/comped tenants). Live-checks Stripe Connect charges_enabled when DB flag is stale and updates it. Not-enabled card in desktop app now shows Enable Cart & Checkout button opening web admin #tab-cart instead of a mailto.
Desktop Phase 2 cart port: added Cart Settings screen to desktop app. Stripe Connect status + connect/disconnect flow (opens browser), GST source/display selects, stock-out behaviour, AusPost postcode, and a dynamic shipping options builder (flat rate, free over, pickup, delivery included, no shipping, AusPost). Wired via 4 new IPC handlers and preload entries.
Web App Added cart/orders API layer for desktop app: GET /api/orders (list with optional status filter), GET /api/orders/<number> (detail with refunds), POST status/refund/notes/pickup-ready/delete per order, POST /api/orders/mark-new-printed, GET+POST /api/cart-config (GST, shipping options, AusPost postcode), POST /api/cart/connect (generate Stripe Connect onboarding URL), POST /api/cart/disconnect. Also added /c/<slug>/cart/connect/done landing page and /api/cart/connect/refresh for AccountLink expiry.
Web App Removed admin link from catalogue header.
Web App Removed flash message after clicking Ready for Pickup -- pickup code is visible on the order itself.
Web App Order confirmation page: click & collect orders show an extra line telling the customer they'll receive another email with their pickup code when the order is ready.
Web App Customer order confirmation email: click & collect orders now show a green notice that the order is being packed and a pickup code will be sent when ready.
Web App Business order email: always show shipping method. Flat rate shows name + price, AusPost shows name with note that it was quoted at checkout, pickup shows name only. Click & collect orders include the pickup code inline with customer details.
Web App Pickup orders: generate pickup code at order placement so it appears on the print slip immediately. Ready for Pickup button reuses the existing code instead of regenerating. Customer is still only notified when tenant clicks Ready for Pickup.
Web App Demo tenant cart fix: _cart_context() now allows stripe_test_mode tenants to show cart UI without a connected Stripe account.
Web App Per-tenant Stripe test mode: added stripe_test_mode column to tenants table. When set, the tenant's cart uses test Stripe keys per-call (bypasses Connect, no transfer_data/application_fee). Checkout page shows test card notice (4242...). Refunds also use test key. Global server mode and all other tenants unaffected.
Web App Orders detail polish: Total border shortened (skips Product column, aligns with subtotal text width); sset-input CSS rule added so notes textarea and refund qty inputs match admin theme instead of white browser default; notes textarea gets extra top padding.
Web App Refunds: track shipping refunds via shipping_refunded column on refunds table. save_refund() accepts shipping_refunded flag. Refund panel replaces shipping checkbox with 'Shipping already refunded' message once shipping has been refunded on an order.
Web App Partial refunds: qty column shows remaining qty (original struck through), line totals recalculated to remaining qty x price. Totals section adds a red Refunded deduction row and grand total shows net amount. Applied to both admin expand and print page.
Web App Refunded items: fully refunded rows now show a red 'Refunded' label alongside the strikethrough in both admin and print views.
Web App Refund panel: qty inputs default to 0; max adjusted for already-refunded qty; disabled when fully refunded. Added Refund all button (sets all inputs to max, checks shipping). Admin items table and print page show refunded badge on partial refunds and strikethrough + faded row on full refunds. Refund data aggregated per order on admin page load.
Web App Orders tab: Refund button now scrolls the refund panel into view when opened, so it's visible without manual scrolling.
Web App Orders tab: added Reset to New button in the right button group (between Refund and Delete). Visible for any order not already New or Pending. Reuses existing status endpoint.
Web App Print pages: mark as processing fires on Print button click (not afterprint). API call happens in background immediately, then window.print() opens the dialog. No prompts, no manual buttons.
Web App Print pages: replace afterprint auto-mark with manual 'Mark as Processing' button. afterprint fires on cancel too so auto-mark was unreliable. Button only appears for new orders, shows feedback states (Marking.../Done).
Web App Print new orders page: carousel mode -- shows one order at a time with Prev/Next buttons and a counter. View all button stacks all orders for bulk printing. Print button prints whatever is currently visible.
Web App Orders tab: replaced 'Clear all orders' with 'Print new orders' button (visible only when new orders exist). Opens order_print_new.html with all new orders one per page. afterprint event fires POST /admin/orders/mark-new-printed which marks all new orders as processing. Removed the clear-all route.
Web App Orders list: Status heading padded 25px left to align with status text in cells (accounts for 8px dot + 5px margin-right).
Web App Orders list: total column left-aligned; date cell shows DD Mon YYYY HH:MM in browser local time (JS converts stored UTC timestamp using data-ts attribute).
Web App Orders tab expanded detail: hover titles on all buttons; Print moved next to workflow buttons, Cancel/Refund/Delete grouped right; edge-to-edge blue divider under buttons and between totals and internal notes; notes textarea gets left padding; print page fires afterprint event to auto-mark 'new' orders as 'processing' via new POST /admin/orders/<num>/mark-printed route.
Web App Orders tab: moved action buttons to top of expanded order detail. Added Delete button (with confirmation) to delete an individual order via new POST route /admin/orders/<order_number>/delete.
Web App Orders tab: detail panel gets full border in adm-border colour on all 4 sides (was bottom only).
Web App Orders tab: amber border now only on the title row when expanded (all 4 sides). Removed amber from detail panel below.
Web App Orders tab: fix amber borders bleeding into nested items table. Changed descendant selectors (space) to direct child selectors (>) so only the outer expanded tr's own td cells get the amber border, not the inner ord-items-table cells.
Web App Orders table: switch to border-collapse:separate with border-spacing:0 so expanded row borders render correctly. Left padding moved directly into th/td base rules. 4-sided amber border on expanded rows now works without collapse interference.
Web App Orders tab: switch expanded row border to inset box-shadow (immune to border-collapse:collapse). Full 4-sided amber box: header row gets inset top + left/right on first/last cell; detail row gets inset bottom + sides. Padding on first column uses !important to override specificity conflict.
Web App Orders tab: complete 4-sided amber border on expanded order (header row top/left/right, detail row left/right/bottom via adjacent sibling selector). Added left padding to first column header and cells.
Web App Orders tab: expanded order row gets amber top/left/right border and amber chevron to clearly distinguish the open header from collapsed rows. Border and amber chevron removed on collapse.
Web App Fix confirm-order and _finalise_order: (1) Remove broken stripe.PaymentIntent.retrieve call from confirm-order -- SDK v15 StripeObject does not support .get(), causing 'Could not verify payment' error on every order. (2) Rewrite _finalise_order with BEGIN EXCLUSIVE transaction: single connection handles check-existing + insert + stock decrement + delete pending atomically, eliminating race condition between webhook and confirm-order. Emails sent after commit so the lock is not held during network calls.
Web App confirm-order: remove stripe.PaymentIntent.retrieve call (SDK v15 incompatibility). Security is maintained by requiring the pi_id to exist in pending_order_data (only created by our server). Webhook remains the real payment confirmation.
Web App Fix confirm-order race condition: check for existing order (created by webhook) before calling Stripe API. Also fix Stripe SDK v5 StripeObject access using .to_dict().
Web App Critical fix: orders and emails now only created AFTER payment is confirmed, not at PaymentIntent creation. New flow: /cart/payment-intent stores order data in pending_order_data table and returns client_secret + payment_intent_id. After stripe.confirmCardPayment succeeds, client calls /cart/confirm-order which verifies PI status with Stripe, creates the order, sends emails. Stripe webhook (payment_intent.succeeded) serves as backup via shared _finalise_order helper. Eliminates premature emails and phantom orders.
Web App Cart/checkout split revised: cart page is now just items + subtotal + Proceed link (no shipping). Checkout page has full address form, shipping options (AusPost rate fetched from postcode field), and payment. No more AusPost inline postcode input.
Web App Split cart and checkout into separate pages. /cart now shows items, shipping selection and totals with a Proceed to checkout button. /checkout is a new page with customer details, delivery address (shown when shipping requires it), and Stripe payment. Selected shipping persists via localStorage between pages. cart.js gains cartShippingSave/Load/Clear helpers.
Web App Orders tab: added 'Clear all orders' button next to the Orders heading. Shows only when orders exist, requires confirmation, deletes all orders via new POST route /admin/orders/clear.
Web App Cart checkout: Pay button now requires street, suburb, and postcode when shipping type is not pickup/none/included. Address fields wire into updatePayBtn on input so the button enables as soon as all required fields are filled.
Web App Order print page: added a bordered 'Ship to' block at the bottom showing customer name, street, suburb/state/postcode, and shipping method/cost. Appears only when an address is present.
Web App Orders chevron cell: removed top/bottom padding and set line-height:1 so row height matches other rows.
Web App Orders chevron size doubled from 16px to 32px.
Web App Orders table: added chevron column on right side of each row. Chevron rotates 90deg (CSS transition) when row is expanded. Row has title='Click to expand' tooltip on hover. Detail row colspan updated from 6 to 7.
Web App Monthly summaries table: all column headings and values center-aligned so headings sit directly above their values.
Web App Monthly summaries table: date now shows as 'April 2026' instead of '2026-04' (Jinja2 dict lookup on month/year parts). Added View button (inline expand row with 4-stat grid: orders, revenue, GST, fees), Email button (resends summary email via new tenant_resend_summary POST route), Delete button. New route: POST /c/slug/admin/orders/resend-summary/<id>.
Web App Fix monthly summary month selector: loop started at i=1 (last month) so the current month was never selectable. Changed to i=0 so the current month appears first. All test orders are from April 2026 so this was the reason nothing could be generated.
Web App Orders tab split into two cards: Orders (order list) and Monthly Summaries (reporting). Monthly summary month selector replaced: native input[type=month] (rendered as dashes when empty) replaced with a sset-select styled dropdown populated by JS with the last 13 completed months by name (e.g. March 2026). Button wording changed to 'Generate & email to me'. Added explanation paragraph. GST column renamed to 'GST collected'.
Web App Shipping options builder redesign. Columns reordered: Type first, Name second, extra field third. Dropdowns use sset-select / sset-text matching Site Settings style. Name field auto-fills with a sensible default when type changes (Standard Shipping, Free Shipping, Click & Collect, Delivery Included, No Shipping Required, AusPost) -- only auto-fills if name is empty or still a default, never overwrites custom names. Expanded explanation with bullet list describing each type. Add option and Save buttons on one line.
Web App Admin Cart Settings: dropdowns now use sset-row / sset-label / sset-select / sset-hint pattern matching Site Settings style. Each setting has an explanatory hint below the label. Dropdown option wording improved: 'Exc GST -- my prices don't include tax', 'Show price with GST included', 'Show as Out of stock -- hides Add to cart button', 'Keep taking orders -- ignore stock level', etc.
Web App Admin Cart tab: Disable card was gated behind 'not cart_free' so Ryan never saw it while testing with the free cart flag. Now shown whenever cart is active regardless of cart_free. cart_free users see a different note ('enabled free of charge'). Enable card still hidden for cart_free users since they don't need billing.
Web App Admin Cart tab: split from one large card into four separate admin-section cards matching the Account tab style. Card 1: Cart & Checkout status (active/inactive/stripe status). Card 2: Connect Stripe (only shown when active but not connected). Card 3: Cart Settings -- GST, stock, AusPost postcode, shipping options (only shown when active). Card 4: Enable/Disable toggle.
Web App Admin Cart tab: merged two separate green status blocks (cart active + Stripe connected) into one combined banner. When both are true it shows 'Active -- accepting payments' with the Disconnect button inline. Payments section only renders when Stripe is not yet connected, so there's no duplication. When cancelling (cart_period_end) the amber banner still shows and the Payments section still shows as normal.
Web App Shipping option builder cleanup: removed dead hidden fields. Fields are now conditionally rendered in the DOM based on type -- flat_rate gets cost input, free_threshold gets threshold input, auspost gets postcode input, others get nothing. typeChange() replaces the field wrap innerHTML rather than show/hide. Form submit reads only the fields that exist. No hidden dead inputs.
Web App Free threshold shipping reworked: option is either unlocked (free, selectable) or locked (greyed out, not selectable). No shipping cost field for orders under threshold -- if the threshold isn't met the option simply cannot be selected. Shows 'Spend $X more for free shipping' on the locked option. Cost field removed from admin for free_threshold type. CSS v101.
Web App Free threshold shipping fixes. Cart now shows 'Spend $X more for free shipping' when the order is under the threshold, instead of always showing Free (which happened when cost field was left at $0). Admin cost field now shows placeholder 'Cost when under threshold' for free_threshold type so its purpose is clear. typeChange() also updates the placeholder dynamically when switching types.
Web App AusPost UX overhaul. Single AusPost shipping option now automatically shows both Regular Post and Express Post rates in the cart (no need to create two separate options). Shipping endpoint returns two synthetic options per AusPost entry with IDs like auspost_21_AUS_PARCEL_REGULAR; payment intent recalculates the rate server-side using the customer's postcode for security. Admin shipping option builder: removed Regular/Express service dropdown, replaced with a dispatch postcode field that auto-syncs with the top Cart Settings postcode field (type in one, both update). Saving either field persists to the global auspost_from_postcode config.
Web App Fix AusPost showing Free: effectiveCost() did not handle auspost type so it fell through to return 0. Added auspost case alongside flat_rate.
Web App Fix AusPost rate calculation: fetchShipping was defined inside the DOMContentLoaded callback but called from renderShipping which is in the outer IIFE scope -- ReferenceError was silently caught, so the fetch never fired. Moved fetchShipping to the outer IIFE scope so all functions can access it.
Web App AusPost UX fix: postcode input is now embedded directly inside the AusPost shipping option row rather than relying on the address section postcode field. Previously, if Click and Collect was auto-selected first the address fields were hidden making it impossible to enter a postcode for AusPost. Now the postcode field appears right in the AusPost option, typing a valid 4-digit postcode triggers rate calculation (600ms debounce), and the resolved rate replaces the pending row. CSS v100.
Web App Phase 6 complete -- inventory display, refunds, AusPost. (1) Inventory: stock_qty=0 now shows 'Out of stock' in table/grid/product page when tenant setting is 'show_oos'; setting added to Cart Settings with 'continue taking orders' alternative. (2) Refunds: Refund button added to order detail for non-pending/non-cancelled orders; inline form lets tenant select item quantities + optional shipping refund; Stripe partial refund API called with reverse_transfer=True and refund_application_fee=True; refunds saved to new refunds DB table; order marked cancelled if fully refunded. (3) AusPost: new shipping option type 'auspost' with Regular/Express service selector; tenant sets dispatch postcode in Cart Settings; cart fetches rate from AusPost PAC API when customer enters their postcode (4-digit listener with 600ms debounce); AusPost options show 'Enter postcode to calculate' placeholder until postcode entered. AUSPOST_API_KEY added to server env. CSS v99.
Web App Fix webhook and remaining order.items bugs. Webhook handler now parses raw JSON payload (json.loads) for all data access instead of using Stripe SDK StripeObject -- StripeObject does not expose .get() or iterate like a dict, causing AttributeError and KeyError on metadata access. Entire webhook rewritten to use plain dicts. Also fixed order.items in order_print.html (same dict.items() method collision as order_confirmation.html and admin.html).
Web App Fix 3 bugs introduced with cart orders. (1) Admin orders tab 500 error: order.items in admin.html resolved to Python dict.items() method instead of the items key -- changed to order['items'] in both places (item count and items loop). (2) Webhook 500 error: Stripe SDK StripeObject does not expose .get() as an attribute, so metadata.get('source') failed -- now converts metadata StripeObject to a plain dict via dict comprehension before access. (3) Registered test mode Stripe webhook via API and added STRIPE_TEST_WEBHOOK_SECRET to server .env so test mode orders complete correctly.
Web App Fix: order confirmation page crashed with 500 error. order_confirmation.html used order.items which Jinja2 resolved to Python's built-in dict.items() method instead of the items key. Changed to order['items'] to force dict key lookup.
Web App Cart payment card redesigned using Stripe's official co-branding approach. Header changed to 'Payment details' with a lock icon -- no longer impersonating Stripe. Removed fake Stripe wordmark. Added official 'Powered by Stripe' link (links to stripe.com per Stripe brand guidelines) at bottom of payment card. Card itself is now plain white with subtle shadow and neutral border rather than blue-tinted. CSS v98.
Web App Cart payment card: replaced broken Stripe logo SVG (was rendering as garbled text) with a styled text wordmark. CSS v97.
Web App Cart payment section redesigned as a distinct Stripe-branded card. Card fields (number, expiry, CVC) are now wrapped in a visually separate panel with a light blue-grey background, Stripe blue border, and a header showing a lock icon ('Secure payment') and the Stripe wordmark -- so it reads as Stripe's domain rather than part of the site form. Stripe Elements inputs use a white inset style with subtle box shadow. CSS v96.
Web App Cart sidebar fixes. Section headers (Order summary, Your details, Payment) were inheriting the theme's --text CSS variable which is light-coloured on dark themes -- added explicit #1a1a1a override scoped to .cart-summary-panel so they are always readable. Stripe card input split from a single combined element into three separate fields (Card number, Expiry, CVC) with labels and a two-column grid for expiry/cvc, so all inputs are clearly visible. Place order button now requires all three card fields to be complete before enabling. CSS v95.
Web App Cart fixes: CSS was SCP'd to wrong path (/var/www/sheetdropper/ instead of /var/www/sheetdropper/static/) causing layout to render single-column -- redeployed to correct path. Added hidePostalCode:true to Stripe card element so Stripe's built-in ZIP field does not block checkout (delivery address postcode is already collected in the form). Cart summary panel, form inputs, and card element container now use explicit white backgrounds and dark text so they are readable on all themes including dark mode. CSS v94.
Web App Cart Phase 5 -- Orders admin tab. New Orders tab in admin nav (only shown when cart active or orders exist; shows badge count of New orders). Orders list: table with order number, date, customer, item count, total, colour-coded status dot. Status filter buttons: All/New/Processing/Shipped/Ready for Pickup/Done/Cancelled. Click a row to expand inline detail: customer info, delivery address, items table with GST breakdown, internal notes field (auto-saves on blur), action buttons for status workflow. Status workflows: physical (New > Processing > Shipped > Delivered > cancel at any point) and click & collect (New > Processing > Ready for Pickup [generates 6-digit code + sends pickup email] > Picked Up). Resend Code button on ready_pickup orders. Reopen button on cancelled orders. Print button opens clean printable A4 page (order_print.html). Monthly summary section: month picker + Generate & email summary button saves to DB and emails business owner; previous summaries listed with delete button. Auto-generate: if today is 2nd+ of month and previous month has no summary but had orders, summary is auto-generated and emailed on first admin page load. New DB tables: order_summaries; new columns on orders: notes_internal, shipping_type, pickup_code (via ALTER TABLE upgrade). New app.py routes: order status update, internal notes, pickup-ready, print, generate-summary, delete-summary. Email helpers: _send_pickup_ready_email, _send_monthly_summary_email. New templates: order_print.html.
Web App Cart Phase 3 -- full cart + checkout + orders. CSS fix: cart-link color changed from var(--primary) to var(--text) with underline so it is always readable regardless of theme. Cart page fully rebuilt: item table with qty +/- controls and remove buttons, shipping option selection (radio cards loaded from /c/slug/cart/shipping), GST breakdown in order summary, full checkout form (name, email, phone, address, notes), Stripe Elements card input, Place Order button. Backend: /c/slug/cart/shipping returns active shipping options as JSON; /c/slug/cart/payment-intent validates cart server-side, calculates totals with GST, creates Stripe PaymentIntent with 0.5% application fee to tenant's Connect account, saves pending order to DB, returns client_secret + order_number; tenant_cart_settings POST saves gst_source, gst_display config values and shipping options; Stripe webhook now handles payment_intent.succeeded to mark order complete, decrement stock, send emails; /c/slug/order/<order_num> shows order confirmation page. New DB tables: shipping_options and orders in tenant catalog.db (auto-created via init_cart_tables). New email helpers: _send_order_confirmation_email (customer), _send_order_business_email (merchant). Admin Cart tab: GST settings section (two dropdowns for price source and display), shipping options manager (add/edit/remove options with name, type, cost, free threshold) with Save cart settings button. CSS v93.
Web App Cart Phase 2 -- catalogue UI. _cart_context() helper added: returns cart_enabled (requires both cart_is_active and stripe_connect_charges_enabled), gst_source, gst_display. All 4 catalogue routes (standalone, embed, product page, HTMX partial) now pass cart context. product_table.html: GST display macros added (converts price based on gst_source/gst_display settings), 'Add to cart' text link added under price in table and grid views, 'ex GST' label shown when displaying exc prices. product_page.html: same GST display + Add to cart button. catalog.html: cart count badge added to sidebar, Cart button added to mobile bottom bar, cart.js loaded conditionally when cart_enabled. new static/cart.js: localStorage cart management (cartAdd, cartLoad, cartSave, cartClear, updateBadges). cart.html stub: shows cart contents from localStorage with remove buttons. CSS: cart-link, cart-count-badge, sidebar-cart styles added (v92). GET /c/slug/cart route added.
Web App Stripe Connect Express onboarding built. New routes: GET /c/slug/admin/cart/connect (creates Express account + generates Account Link, redirects to Stripe hosted onboarding), GET /c/slug/admin/cart/connect/return (checks charges_enabled after onboarding), GET /c/slug/admin/cart/connect/refresh (generates fresh link if expired), POST /c/slug/admin/cart/disconnect (removes account). Cart tab now shows: Connect button when no account, Finish setup warning when account exists but incomplete, Connected status with Disconnect button when charges_enabled. _stripe_connect_ready() helper checks Stripe API for account status. stripe_connect_id and stripe_connect_ready passed to admin template.
Web App Stripe test mode support added. STRIPE_TEST_MODE env var (0/1) swaps all Stripe keys, price IDs, and webhook secret to test equivalents. Test prices created in Stripe test mode: base monthly/annual and cart monthly/annual. Admin panel shows a purple 'Test mode' banner when active so it's obvious no real payments will process. stripe_test_mode and stripe_publishable_key passed to admin template for use with Stripe.js in upcoming Connect and checkout work.
Web App Admin: Cart tab moved after Account, green dot removed from tab title. Plan & Billing card updated to show cart add-on status (billed line, active until date, or 'no add-on' with link to Cart tab) covering active/cancelling/wind-down states. Superadmin preview mode: visiting /c/slug/admin?preview=paying while logged into superadmin injects mock subscription data so the billing card can be reviewed as if the tenant is on a paid plan.
Web App Superadmin: Cart free toggle added to tenant action buttons. Clicking 'Cart' grants complimentary cart access (no billing), clicking 'Cart ✓' revokes it. cart_free column added to tenants DB. cart_is_active() checks cart_free first -- if set, cart is always active regardless of subscription. cart_free passed to admin template; Cart tab hides the enable/disable billing toggle when cart_free is active so tenant just sees the cart as on. Platform fee note updated to remove 'for high-traffic stores'.
Web App Cart tab added to admin nav. Cart & Checkout section moved from Account tab to new dedicated Cart tab with a full info/features card when inactive and status card when active. Account tab billing card updated to show cart add-on status with Manage link. Billing alignment fixed: when enabling cart mid-cycle, Stripe subscription item is created with trial_end set to the first billing date >= 1 month away, preventing short months. A flat upfront charge ($10 or $110) is immediately invoiced via Stripe InvoiceItem to cover the gap period. Cart disable also redirects to Cart tab now.
Web App Cart add-on Phase 1B -- billing infrastructure. Created Stripe product (prod_UPDZ8tWqONzoRs) with monthly (price_1TQP8nB5LhyOKZr8DhKlzUh7, $10/mo) and annual (price_1TQP8nB5LhyOKZr8yv0ga5Lm, $110/yr) prices. Added CART_PRICE_ID and CART_ANNUAL_PRICE_ID env vars and app constants. Added cart_period_end column to tenants DB. Added set_cart_enabled() and cart_is_active() helpers to tenant_manager. Added /c/slug/admin/cart/enable and /c/slug/admin/cart/disable POST routes -- enable adds Stripe subscription item, disable deletes item (with proration) and stores period_end so cart stays active until billing period ends. Added cart_active and cart_period_end to admin template context with lazy expiry check. Added Cart & Checkout section to admin Account tab (enable/disable UI, pricing, status). Updated create_checkout_session to accept include_cart form field and add cart price as second line item. Updated webhook to set cart_enabled on provisioning if cart was included at signup. Added cart add-on checkbox to signup form with live price calculation JS and CSS.
Multiple Cart add-on Phase 1A -- schema and template groundwork. csv_parser.py: added gst_rate, stock_qty, weight_kg, length_cm, width_cm, height_cm to FIXED_COLUMNS with full validation (gst_rate 0-1 float, stock_qty non-negative int, dimensions positive float). database.py: added all 6 columns to products table fixed_col_defs and insert_cols. converter.py: added all 6 columns to _FIXED_COLUMNS_INFO so AI import is aware of them. tenant_manager.py: added cart_enabled, stripe_connect_account_id, cart_subscription_item_id columns to tenants table via ALTER TABLE. template_generator.py: added 6 new product columns (gst_rate through height_cm) as blue fixed columns between sort_order and attribute examples; added Cart Settings section to Config tab (gst_source, gst_display with dropdowns); added column explanations to User Guide. All changes are purely additive -- no existing catalogue functionality affected.
Web App guide.html: standardised all descriptive uses of 'catalog' to 'catalogue' throughout. sort_order column description changed from 'Integer for manually...' to 'A number for manually...' for non-technical readers. Config key catalog_layout and app name Catalog Manager preserved as-is.
Multiple Desktop app installer added to server at /static/downloads/catalog-manager-setup.exe. Welcome email updated: desktop app section now includes a download button, SmartScreen bypass instructions, and the API key labelled clearly. Admin account tab API key section updated: download button added above the key, SmartScreen warning note added below it.
Multiple Support form added to desktop app Help screen. Topic dropdown (Upload issue, Settings, Analytics, Billing, Bug report, Other) and message textarea. Sends via new POST /api/support Flask endpoint which emails info@sheetdropper.com via ZeptoMail with tenant name, slug and reply-to set to tenant email. Help screen also updated: TOC links now have amber border only (not amber text/background), back-to-top link centered, removed from Quick Start card.
Desktop Help screen complete rewrite. New table of contents with anchor links to 13 sections. Quick Start step-by-step. Full spreadsheet format documentation including core columns, product page columns, attribute filter columns, and the Config tab. Detailed Smart Import explanation. Upload Settings modal explained. AI Review screen guide. Configure & Preview broken down by every settings group (Header, Body, Theme, Footer). Analytics screen documentation. Restore + local backups. Dashboard controls. Images & Logos section with hosting tips. Tips & Tricks. Comprehensive troubleshooting (upload failed, images not showing, settings not applying, can't connect, preview vs live, AI mapping issues). Support section. CSS additions: help-toc grid, help-steps numbered list, help-section-sub headings, help-table for data tables, code/strong styling for inline elements.
Multiple Analytics: sparkline, top search terms, and most viewed products now shown for all three source tabs (Combined, Standalone, Embed) in both the web admin and the desktop app. Previously sparkline and top products were combined-only. database.py updated to return per-source spark and top_products dicts.
Multiple Analytics screen added to desktop app: page views and unique visitor stats (today/7d/30d/all time), 30-day sparkline, top search terms, most viewed products, with combined/standalone/embed source tabs. New GET /api/analytics Flask endpoint added to serve analytics JSON to the desktop app. Desktop app package.json branding fixed (appId and description updated from w3swaps to SheetDropper). icon.ico generated from SheetDropper logo PNG, icon path corrected in build config. Files array in build config corrected (removed non-existent root assets/ reference).
Web App Fix: Stripe webhook handler used .get() on StripeObject which is not supported in Stripe SDK v15 -- changed to 'key' in obj checks with bracket access throughout. Also moved 'import requests as _requests' to module top-level (was a late import at line 2651 and a local import in the contact route), fixing test mock isolation.
Web App Fix: AI Converter smart upload returning 403 Forbidden on web -- all JavaScript fetch() POST calls in admin.html now include csrf_token (checkConfig, sendFile/converter upload, deleteBackup, logo upload). These calls build FormData manually and didn't include the CSRF token that Flask's before_request check requires.
Web App Security: fixed three vulnerabilities -- (1) X-Forwarded-For rate-limiter bypass: _client_ip() now uses the last XFF entry (nginx-appended, unspoof-able) instead of the first (attacker-controlled), closing brute-force bypass on all login endpoints; (2) hardcoded SECRET_KEY fallback removed -- app now raises RuntimeError at startup if SECRET_KEY env var is absent, preventing session-cookie forgery; (3) hardcoded SUPERADMIN_PASSWORD fallback 'superadmin' removed -- app now raises RuntimeError at startup if SUPERADMIN_PASSWORD env var is absent.
Web App Self-host htmx 1.9.12 -- moved from unpkg CDN to local static/htmx.min.js, eliminating CDN dependency and supply chain risk.
Web App Security hardening: CSRF protection added to all POST forms (custom HMAC-based before_request check); session cookies hardened (Secure, HttpOnly, SameSite=Lax); rate limiter fixed to use X-Forwarded-For behind Nginx proxy; security headers added (X-Frame-Options, X-Content-Type-Options, Referrer-Policy); SVG uploads blocked for logos; secure_filename applied to logo uploads; XLSX magic-byte validation added; CSS/color injection hardening in settings (color fields validated against hex regex, font fields stripped of dangerous chars); api/rollback NameError fixed (backup_id -> newest.id); Stripe webhook updated to use verified event object, empty secret now rejected; debug=True disabled in production; email HTML-escape applied to all user-supplied content in outbound emails; admin page Cache-Control: no-store added; converter temp files moved to app-local restricted directory with path-traversal check; .gitignore added to pricelist-app; stripe and requests added to requirements.txt
Desktop Full visual overhaul of the Electron desktop app to match the web admin aesthetic. Phase 1: CSS design system -- sharpened border-radius (8-12px down to 4-5px), matched web admin card/button/modal shapes. Phase 2: Sidebar -- amber gradient top-rule, SheetDropper icon + two-tone wordmark (Sheet white, Dropper amber), mono uppercase nav buttons with amber left-border active state, cleaned-up activity log and disconnect button. Phase 3: panel-label hierarchy (amber mono tag above Barlow uppercase heading) added to all view headers and cards across Dashboard, Restore, AI Review, Configure, and Help screens. Status bar items centered. Live badge on restore cards sized to match Restore buttons. Phase 4: Setup screen rebuilt with brand header (icon + wordmark), amber top-rule, and panel-label layout. Modals cleaned up (no emojis, panel-label + uppercase Barlow title, mono option buttons, amber top-rule). Right-click on API key input now auto-pastes from clipboard.
Web App Logo settings added to web admin Settings tab: Logo URL field, position dropdown (left/right/both), width and height number inputs. All four fields saved via tenant_save_settings route and applied to the catalog header.
Multiple Logo size setting: new logo_width and logo_height config fields (pixels, leave blank for defaults). Applied as inline styles on the catalog header logo. Added to xlsx template Config sheet with descriptions. Desktop app configure panel gains Width/Height number inputs in the logo section, wired into getCurrentConfigFromForm, loadConfigIntoForm, and generatePreviewHTML.
Web App Superadmin Phase 6: (1) Trial accounts flagged in tenant table with amber Trial badge and days remaining. (2) Site-wide analytics section: aggregate page views (today/week/month/all time), top catalogs by total views, top search terms across all tenants. (3) Server stats section: RAM usage with progress bar, disk usage with progress bar, load average (1-min and 5-min). Stats bar extended to 5 tiles with On Trial count. All read from /proc on the server.
Web App Trial code signup flow: enter a promo code at signup to bypass Stripe entirely. Code fullofsheet1 = 30-day trial, fullofsheet3 = 90-day trial. Account provisioned immediately, trial_end date stored in DB. Admin shows amber trial banner with days remaining and Subscribe button. Trial expiry checked on admin and catalogue load -- auto-deactivates when expired. Signup form re-enabled and promo code field added.
Web App Inactive tenant handling: catalogue shows a 'temporarily unavailable' page with an owner note instead of 404. Admin login and panel remain accessible. Admin panel shows a prominent expired banner (Resubscribe / Download data / Delete account) when subscription has lapsed. Resubscribe creates a new Stripe checkout session pre-filled with their email. New catalog_inactive.html template.
Web App Account tab: subscription management card (plan, next renewal date, cancel button -- or cancellation scheduled banner if already cancelled). Cancellation flow: dedicated cancel page with data export email choice, cancellation reason checkboxes, comments field. On confirm: Stripe set to cancel_at_period_end, xlsx + analytics CSV emailed to chosen address, confirmation to account email, feedback to info@sheetdropper.com. Delete account section: type DELETE to confirm, cancels Stripe immediately and wipes all data.
Web App Switched Stripe to live mode: updated STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, STRIPE_PRICE_ID, and STRIPE_WEBHOOK_SECRET on server to live values. New live webhook endpoint created at https://sheetdropper.com/stripe/webhook listening for checkout.session.completed, customer.subscription.deleted, customer.subscription.paused. SheetDropper is now taking real payments.
Web App Signup page (/signup) full revamp: two-column layout (pitch left, form right) with shared nav and footer matching all other public pages. Left column has headline, what-you-get checklist, pricing block (A$29/mo), and trust signals. Form card updated to match design system (IBM Plex Mono labels, amber accent). Nav 'Get started' button replaced with 'Log in' since user is already on signup.
Web App Superadmin delete route: metro-industrial tenant is now protected from deletion -- returns an error flash and redirects without deleting. It is the demo tenant powering /demo.
Web App New /demo page: SheetDropper nav and footer wrapping an embedded iframe of the metro-industrial catalogue. All 'Live demo' links across landing, guide, and login now point to /demo instead of the standalone catalogue URL.
Web App Consistent header/footer across landing, guide, and login pages: guide footer replaced with full landing footer (logo, links, copyright); login page gets same nav and footer, body restructured around <main> so login card stays centred.
Web App Guide page: full nav replaced to exactly match landing page -- same logo, same links (How it works/Features/Guide/Live demo/Log in), same Get started button style, same mobile behaviour.
Web App Guide page: nav logo updated to match landing page -- SVG square icon at 36px, plain 'SheetDropper' text, flex layout with gap.
Web App Landing page: nav logo enlarged to 36px (fills 60px header without increasing height); footer dot replaced with SVG logo at 16px.
Web App Landing page nav: replaced CSS dot with Sheetdroppersquare.svg logo (22px, served from static).
Web App Admin mobile fixes: tabs font reduced + padding tightened at ≤640px so all 5 fit without horizontal scroll. Stat strip switches to 2×2 grid on mobile. CSS v78.
Web App Admin tab sections reorganised: Quick Actions moved to top of Overview tab; Downloads moved to bottom of Settings tab; API Key moved to top of Account tab. CSS v77.
Web App Admin header: replaced 'Catalog Admin' h1 + meta with amber mono slug. Added hero section below header (─── Admin label, big business name H1, last upload sub text). Overview tab (first): stat strip (Status/Products/Views/Last Upload) + analytics combined. Tabs: Overview|Import|Settings|Backups|Account. CSS v75.
Web App Admin panel: horizontal tab navigation added (Import / Settings / Backups / Analytics / Account). Tab state persists in localStorage. Existing sections grouped into tabs, styles kept. CSS v74.
Web App SheetDropper theme: filters now rendered inside sidebar below categories (template change in catalog.html). Right col hidden for sheetdropper. Products fill full width. Dark filter select styling added for sidebar context. CSS v89.
Web App Theme picker: modal no longer does an immediate fetch/save. Selecting a theme + Apply/Keep now only updates the form fields in-page. Changes go live only when Upload Settings is clicked. Reset to theme defaults also only updates form fields, not DB directly.
Web App Theme system overhaul: (1) THEME_PRESETS dict + /apply-theme-preset route. (2) Bulletin renamed Mosaic — gradient card accents, gradient table header. (3) SheetDropper: filter panel repositioned to left via CSS. (4) Market: filter strip nowrap+scrollable. (5) Theme picker: radio inputs replaced with click-based divs + modal. (6) Reset to theme defaults button. (7) Mosaic thumbnail updated. CSS v88.
Web App Search input background: switched from var(--bg) to var(--surface) in color-mix so it follows table background colour, not page background. CSS v87.
Web App Search input: background now uses color-mix(in srgb, var(--bg) 30%, white) — slightly brighter than surrounding bg, adapts to all themes. CSS v86.
Web App Search input placeholder: set color to var(--text) at 75% opacity so placeholder text is clearly readable. CSS v85.
Web App Analytics: unique visitor tracking added. visitor_hash computed server-side as SHA-256(IP|UA|date) truncated to 16 chars — no PII stored, no cookies. log_event now accepts ip param. get_analytics returns uv dict alongside views. Stat tiles redesigned: each tile is two-column (page views left, unique visitors right) divided by a thin border. 'Catalogue page views' heading removed. Fine print updated to clarify no personal information is stored and no visitor can be identified.
Web App Analytics: server-side event logging added (pageviews, product views, searches). analytics_events table in each tenant SQLite DB. Bot UA filtering. Admin panel gets Traffic section with Combined/Standalone/Embedded tabs (default combined): today/7d/30d/all-time view counts, 30-day sparkline, top 10 searches, top 10 product views. Search terms logged at 3+ chars from the HTMX products partial.
Web App Welcome email: replaced heading underlines with amber horizontal rules separating each section. Headings replaced with bold labels. Tighter spacing throughout.
Web App Welcome email: URL and Password now on separate lines in admin panel section. Getting started steps rewritten — leads with AI import, ends with share/embed, avoids implying manual data entry upfront.
Web App Superadmin create tenant: welcome email now sent automatically if an email address is entered. Flash message confirms email was sent. No email entered = existing behaviour (API key shown in flash).
Web App Login improvements: (1) Change password section added to admin panel — requires current password, sends notification email to account email. (2) Business name shown on per-tenant login page instead of 'Catalog Admin'. (3) Stay logged in checkbox on both login pages — sets 30-day persistent session cookie. (4) Password changed notification email via ZeptoMail.
Web App Superadmin: email field added to new tenant create form. Set Email action button added to each tenant row (prompt-based, can update or clear). set_tenant_email() added to tenant_manager. superadmin_set_email route added to app.py.
Web App Password reset flow + general login: added /login page (email or slug + password), /forgot-password (email-based token reset, 1-hour expiry, single-use), /reset-password?token=... (new password form). Reset email sent via ZeptoMail. Forgot password link on per-tenant admin_login. Login link added to landing page nav. Superadmin dashboard gets Reset PW button per tenant (prompt-based, sets password directly). password_reset_tokens table added to tenants.db. tenant_manager: get_tenant_by_email, set_admin_password, create_reset_token, validate_reset_token, consume_reset_token.
Web App Guide AI Import data/privacy section: removed misleading 'by default' phrasing. Clarified that API data is not used for training as a firm policy (not a default setting), and noted this is the key distinction from Claude.ai consumer usage.
Web App Guide: added Data and privacy subsection to AI Import. Explains spreadsheet sample is sent to Anthropic API via SheetDropper's key, links to Anthropic privacy policy, notes API data is not used for training by default, and offers template method as an alternative for sensitive data.
Web App Guide page: reworked copy across every section to shift from sales pitch to reference documentation. Removed promotional framing ("fastest way", "most businesses find…", "more powerful management experience", "for trade suppliers"). Replaced marketing card pairs with factual What/What-it-isn't framing. FAQ answers tightened to neutral reference style. Structure and sidebar unchanged.
Web App Pricing: added annual tier at A$319/year (save 1 month) in Stripe test mode. Signup page now has monthly/annual billing toggle. Landing page has new pricing section showing both plans before the CTA.
Web App Fix: theme save was silently resetting to 'classic' when bulletin or market was selected — validation allowlist still had old names 'clean' and 'bold' from before rename. Updated to 'bulletin' and 'market'.
Web App Themes: replaced 'bold' theme with 'market' — fundamentally different layout. Sidebar becomes a sticky horizontal category tab bar, filters become an inline horizontal strip, grid view becomes full-width horizontal row cards (image left, content right). Page uses natural scroll instead of fixed viewport. Renamed in admin picker, template_generator, and changelog.
Web App Themes: replaced 'clean' theme with 'bulletin' — classified-ad/newspaper aesthetic. Ruled lines instead of card boxes, double-rule borders, monospace prices in primary colour, typographic category nav. Updated admin picker, template_generator, and CSS theme thumb.
Web App Admin: number input spinner arrows enlarged (12x18 SVG, wider triangles) and separated with 2px gap between up/down arrows.
Web App Admin: number input spinner — replaced color-scheme and background-color approaches (unreliable in Chrome) with -webkit-appearance:none + SVG data URI drawing white up/down triangles on #060f1c background.
Web App Admin: number input spinner — added background-color:var(--adm-bg) on webkit spin button pseudo-elements to match input box background. color-scheme:dark retained for white arrow rendering.
Web App Admin: number input spinner styled with color-scheme:dark so browser renders spin button natively with dark bg and white arrows. Replaces failed filter:invert approach that caused always-visible spinner and wrong colours.
Web App Admin: number input spinner arrows styled with filter:invert(1) — inverts system default (light bg, dark arrows) to dark bg and white arrows to match dark admin theme.
Web App Admin: font size number input given explicit height:28px and box-sizing:border-box to match select and colour picker trigger heights in font rows.
Web App Admin: colour picker trigger buttons resized from 40x32px to 28x28px square to match the natural height of font input boxes and keep label text vertically aligned.
Web App Admin: Catalog Theme and Display Mode (light/normal/dark) moved out of Body section into their own Theme section between Body and Footer.
Web App Light mode reworked to match dark mode approach: bg/surface/border now tinted from primary colour instead of mixing with white from bg-base/surface-base. Uses color-mix with near-white base (#f5f7f5/#ffffff) so light mode stays light but inherits brand palette. Primary still lightened 20% toward white.
Web App Dark mode reworked: bg/surface/border/filter-bg/img-bg now tinted from user's primary colour instead of hardcoded slate-blue. Uses color-mix with #0d1117/#161b22 base so dark mode stays dark but matches the brand palette. Primary colour gently darkened by 10% in dark mode. Text/hover/tag vars unchanged.
Desktop Ported custom colour picker to desktop app: all 9 native input[type=color] replaced with cp-wrap structure. Swatches (default) + spectrum (canvas + hue slider + HEX/RGB/HSL inputs) + eyedropper button. CSS added to style.css with desktop variable names (no --adm- prefix). Footer same-as-header logic updated to use cpSetValue. syncAllPickers called after applyConfigToForm so trigger buttons show correct colour on load.
Web App Colour picker rebuilt as cohesive custom popup: two modes (swatches default, spectrum) toggled via bar button. Spectrum mode: canvas saturation/brightness box + hue slider, draggable cursors. Input modes cycle HEX/RGB/HSL. Eyedropper button (EyeDropper API, hidden if unsupported) in bar. No native Chrome picker. All dark admin styled.
Web App Colour picker: restored native Chromium picker (eyedropper + spectrum) as ⊙ button in swatches popup bar. ⊙ opens native picker without closing the custom swatches popup, so swatches are still accessible after native picker closes. Reverted accidental removal of eyedropper/spectrum access.
Web App Reworked colour picker: native input[type=color] retained (Chromium picker with saturation box, hue slider, RGB/HSL/Hex unchanged). Trigger button now opens a custom swatches popup first — 10×10 spectrum grid (100 Tailwind-palette colours, light→dark, warm→cool). Bottom bar shows current hex value and a ⊙ button that closes swatches and opens the native Chromium picker. Picking a swatch sets the colour and closes the popup.
Web App Replaced all 9 native input[type=color] pickers in admin Settings with a custom colour picker component. Default tab is a 64-swatch palette (8×8 grid: greys, blues, teals, greens, yellows/ambers, reds/oranges, purples, warm neutrals). Additional tabs: HEX, RGB, HSL. Popup opens on click, closes on click-outside or Escape, flips up/left if near viewport edge. Selected swatch highlighted with amber ring. Footer 'same as header' checkbox and theme preset colour injection updated to use new cpSetValue API.
Web App Light mode now shifts palette toward white using color-mix: --primary at 55% (paler header), --bg at 25% (near-white page), --surface forced to #ffffff, --border at 50%. Previously light and normal were identical; now light is a true lighter shift.
Web App Added 'Normal' as a third Display Mode option (between Light and Dark). Normal passes configured colours through unchanged with no CSS overrides — sits in the middle of the Light/Normal/Dark toggle. Updated admin.html radio buttons, app.py validation, template_generator.py dropdown and notes, and style.css comment.
Multiple Configure & Preview: 'Download as .xlsx' now shows a modal with three options — Config + live products, Config only, or (if a file is staged in the Upload Template drop zone) Config + staged file's products. All options merge unsaved form config over DB config so the downloaded file reflects current form state. New Flask POST /api/download-xlsx endpoint handles all three sources. Desktop: new api:download-xlsx IPC + preload wiring. Hover tooltips added to all dashboard, AI Review, Restore, and Configure buttons.
Desktop Configure & Preview polish: preview header and footer now use a 3-column grid (sidebar-width / 1fr / filters-width) so heading and footer text always align with the product list column. Mobile view collapses left/right columns to 0. Added actual-size/scaled toggle button next to viewport size buttons. Fixed Configure & Preview heading alignment (AI Review CSS was overriding view-header-left globally — scoped to #view-ai-review). Section header/body spacing tightened. Chevron styled at 38px with overflow:hidden. flex-shrink:0 on headers to prevent compression.
Desktop Configure screen: sections (Header Settings, Body Settings, Theme, Footer Settings, Upload/Save) are now collapsible with chevron toggle. Header Settings and Upload/Save default open; others default collapsed. Removed Revert to Last Saved button and Load from File option.
Desktop Added contact_email field to desktop Configure screen (after Footer Subheading). Wired into load, save, live-change listener, and preview footer as a mailto: link with 1.2em left margin.
Web App Added margin-left:1.2em to contact_email link in catalog footer to separate it from contact_info text.
Web App Added contact_email config field. Renders as a tappable mailto: link in the catalog and product page footers. Admin footer section has a new Contact Email input with hint. Added to database defaults, app.py field lists, csv_parser CONFIG_KEYS, and template_generator guide + config rows.
Web App Footer contact info: force color:inherit on both span and iOS auto-detected anchor tags to fix invisible phone number on dark footer backgrounds. Mobile footer stacks to single column — each element on its own line.
Web App Footer redesign: 3-column grid matching the catalog layout (220px / 1fr / 190px). Date small (9px) pinned left under sidebar. Price note and contact info in center column — price note left, contact info pushed right with margin-left:auto. Right column (under filters) empty. Removed last-updated date from catalog header.
Web App Removed last-updated date from catalog header. Footer updated date moved to leftmost position, font-size 10px, margin-right auto to pin it left while other footer content sits right.
Multiple New catalog theme: Ledger — spreadsheet-style with hard cell borders on all sides, border-collapse on table, primary-color header row with white text, zero border-radius throughout. Grid view uses hard-bordered matrix layout. Added to admin theme picker with thumbnail, template_generator.py description and dropdown validation.
Web App Removed Page Background and Table Background color pickers from admin. These were overriding the light/dark mode system via :root CSS variables. Light/dark mode now fully controls --bg and --surface. Primary color remains as accent. Removed page_bg_color and table_bg_color from server-side fields lists and base.html injection.
Web App Fix: SheetDropper theme was rejected by save-layout validation allowlist and silently reset to classic. Added sheetdropper to allowed themes in app.py.
Web App New catalog theme: SheetDropper — dark navy (#060f1c) background with amber (#f5a623) accents and green (#2eb87e) prices, matching the landing page aesthetic. Added to admin theme picker, style.css theme tokens, and template_generator.py spreadsheet validation list.
Web App Contact page built: /contact — name, email, subject dropdown, message. POST sends to info@sheetdropper.com via ZeptoMail with reply-to set to sender. Dark industrial style matching guide page. Linked from admin footer.
Web App Admin page footer added: SheetDropper (amber), Help, Legal links. Mono uppercase style matching admin dark theme.
Web App Mobile admin header: company name and product/date info split onto separate lines. Company name 13px (up from 9px), product count + updated date stays 9px. Dot separator removed.
Web App Mobile admin header redesign: 3-column CSS grid (SheetDropper left | Catalog Admin centered | View Catalog/Logout right) with business name/product info spanning full width below. Moved h1 out of .adm-brand in HTML. Required width:100%;max-width:none on h1 to override Chromium's phantom content-width max-width on grid items.
Desktop Configure & Preview 'Upload to Live Site' button now checks if a spreadsheet is staged in the upload zone. If yes, shows the config choice modal (same as the green upload button) asking whether to use spreadsheet settings, form settings, or keep live settings. Products and settings upload together in one action. If no file staged, button works as before (settings only).
Multiple Fixed: AI convert modal now correctly shows 'Use spreadsheet settings' button when file has Config tab. Backend was returning wrong field name (show_config instead of has_config), causing desktop to always hide the button. Fixed variable names: has_config=True when 'Config' in sheets_data. Desktop message 'Your file has no Config tab' now shows when !has_config. Removed em dash from button text.
Desktop Desktop app visual redesign: full dark industrial theme matching web admin. Dark blue palette (#060f1c/#0d1f35), amber accents, IBM Plex Mono + Barlow Condensed fonts, grid overlay on body, amber left-border on active nav. Drop zones: AI zone gets amber dashed border + glow, upload zone gets teal accent. Status pills, restore cards, buttons, mapping tables all dark-themed. Config choice modal added: when uploading (template or AI import), user picks 'Use spreadsheet settings', 'Use Configure screen settings', or 'Products only — keep live settings'. Theme preset + light/dark mode selects added to Configure panel. theme/theme_mode wired into applyConfigToForm and getCurrentConfigFromForm.
Web App Fix flash messages not showing after fetch-based uploads: switched to redirect:'manual' so fetch doesn't follow the server redirect and consume the flash message before the browser navigates. All unified upload and converter confirm fetches now navigate to the admin URL directly. Renamed 'Keep current site settings' to 'Keep existing live settings' to avoid confusion with settings currently shown on the page.
Web App AI converter confirm: 'Upload to Live Site' button now shows the same settings choice modal as the regular upload. showConfigModal() refactored to accept hasConfig + callback so both flows share one modal. Converter never has a Config tab so spreadsheet option is always hidden. 'Use settings from this page' merges admin form fields into the converter confirm POST. Backend tenant_admin_convert_confirm now reads config_source and applies admin form settings if requested.
Web App Upload settings modal reworked: now always shows when a file is staged (not just when file has Config tab). Three options: 'Use spreadsheet settings' (only shown if Config tab present), 'Use settings from this page' (applies admin form values), 'Keep current site settings' (preserves existing DB settings unchanged). Backend handles new config_source='current' by calling load_data with empty config dict, leaving DB settings intact.
Web App Unified upload: both 'Upload & Go Live' (drop zone) and 'Upload Settings' (site settings) buttons now call the same doUnifiedUpload() function. It builds FormData from the settings form, appends the staged file if one is dropped, and POSTs to save-settings. Both domains (products + settings) always go live together. Removed submitWithFile() and the old form submit interceptor — upload zone button is now type=button with onclick.
Web App Upload reworked to use fetch+FormData instead of DataTransfer file copying (which browsers block). File object stored in window._stagedUploadFile on drop. submitWithFile() builds FormData from settings form + file and POSTs to save-settings. Upload template button and settings upload button both use this path. Config choice modal also uses submitWithFile.
Web App Upload Template form submit is now intercepted by JS and routed through the settings form — products and settings always go live together in one action. File is copied to sset-pending-file, config_source set to admin, settings form submitted. If file has a Config tab, choice modal appears instead.
Web App Settings upload reworked: button label now updates immediately (sync) when file is dropped, not waiting for async check-config. Settings save now re-applies products.csv from disk with new settings when no new file is dropped and products exist — products and settings always stay in sync. New file with config tab still shows choice modal.
Web App Upload Settings button can now include products: when a file is staged in the Upload Template drop zone, button becomes 'Upload Settings + Products' and submits both together via load_data. If the file has a Config tab, a modal asks whether to use spreadsheet settings or admin settings. Both drop zones now call check-config on file drop to detect config tabs. config_source field added to settings form.
Web App Settings save now always creates a new backup (same as xlsx upload): apply config changes, call _backup_current from DB, mark as live. Previously it only updated the existing live snapshot in-place, which meant settings changes never produced a backup.
Web App Upload config merge: load_data() now reads existing DB config before wiping and uses it as a fallback layer (defaults < existing DB < non-empty uploaded config). Uploads with no config tab (AI converter) now preserve current site settings instead of reverting to hardcoded defaults.
Web App Backup system: deleting the Live backup card now clears the live catalog (drops products table, clears current CSV files, clears last_restored_backup_id). Added database.clear_products() which wipes only the products table leaving config intact. Settings save no longer creates a pre-backup — only updates the live snapshot in place.
Web App Backup system: saving site settings now re-exports the live snapshot backup from the DB so the Live card always reflects the current config (business name, colours, fonts etc), not just the state at last upload.
Web App Backup system: every upload now creates a live snapshot backup after loading and marks it as last_restored_backup_id, so the current live dataset always appears as a card in the Restore section. Rollback drops the live badge and restore button appears on old cards. API rollback now also sets last_restored_backup_id. _backup_current returns the backup ID and handles same-second timestamp collisions.
Web App Admin Smart Import: upgraded to rotating conic-gradient border beam (amber light sweeps the border every 3s), section breathes with pulsing outer glow, drop zone pulses independently, scan-line overlay retained.
Web App Admin Smart Import section: premium 'charged cell' styling — amber gradient background tint, centred amber top-bar accent, fine horizontal scan lines, AI-Powered label as amber badge, Smart Import heading with white-to-amber gradient text, drop zone pulses slowly with amber glow (4s cycle), stronger glow on hover/drag.
Web App Admin restore cards: Restore and Delete buttons now equal fixed width (95px), centred text, space-between layout so Restore sits left and Delete sits right with a gap between them.
Web App Admin restore: added Delete button to each backup card. Confirms before deleting, calls new POST /c/<slug>/admin/delete-backup route which removes the backup folder server-side. Card fades out and removes from DOM on success.
Web App Admin site settings: Header, Body, Footer subsections are now collapsible. Clicking the orange group header expands/collapses the rows. Hint text (right-justified) shows 'Click to expand' or 'Click to collapse'. All three start collapsed by default.
Web App Admin embed section: added Copy Code button (matches Copy Key button style — btn-secondary). Copies iframe snippet to clipboard, shows Copied! for 2 seconds then resets.
Web App Admin mobile header: SheetDropper and Catalog Admin now stack vertically on mobile. Catalog Admin resized to ~11px Barlow Condensed to match SheetDropper width. Right separator removed on mobile. Links (View Catalog / Logout) switch to column layout with padding for breathing room from header edges. Meta info hidden on mobile.
Web App Admin site settings: Default Layout toggle now uses mode-option/mode-toggle classes to match Light/Dark button style. Row is sset-color-row so label and buttons stay on one line. Hint text removed.
Web App Admin site settings: Default Layout changed from select dropdown to two radio button toggle (Table/Grid) with selected one highlighted amber. Currency Symbol row stays inline on mobile (sset-inline-row). Standalone colour pickers (Header/Page BG/Table BG) now align with Font Colour pickers — font colour picker changed to margin-left:auto to be flush right.
Web App Admin mobile site settings: fixed colour pickers going off-screen (sset-color-row label was width:100% in row flex mode — fixed with flex:1). Fixed currency symbol input rendering 100px tall (style.css sset-text-sm had flex:0 0 100px which set height in column flex — fixed with flex:none width:80px align-self:flex-start).
Web App Admin mobile font rows: SIZE column fixed to 50px, FONT COLOUR column fixed to 68px, FONT column takes remaining width (flex:1). Sub-labels set to white-space:nowrap so FONT COLOUR stays on one line.
Web App Admin mobile font rows: all three columns (FONT/SIZE/FONT COLOUR) now equal width (flex:1), same height (32px), no-wrap, and FONT column left-aligns with the row label above. Colour picker centred under its label. Font-col color picker now explicitly styled (was using browser default).
Web App Admin mobile: reduced outer viewport gap (admin-content sides 24px→10px) and section inner padding (24px 26px→16px 14px) so sections use more screen width on mobile.
Web App Admin mobile site settings: sset-row and sset-group-header restored to 12px left/right padding (down from 16px) so text has breathing room from box edges without being flush.
Web App Admin site settings: sset-group-box padding zeroed out (style.css had padding:6px 16px 12px leaking in) — HEADER/BODY/FOOTER amber bars now span the full width of their group box instead of being inset 16px on each side.
Web App Admin mobile site settings: sset-row and sset-group-header left/right padding removed so labels, inputs, and HEADER bars all align to the same left edge. sset-label now full-width (width:100%) on mobile. Business Info label text expanded to proper two-line description. Font size held at 11px.
Web App Mobile header: logo now vertically centered with business name (was pinned to top). Logo height increased to 60px on mobile (was 48px). CSS v=74.
Web App Mobile catalog: sticky shrinking header on scroll. After 50px of scroll the header collapses to a compact bar showing only the logo and company name (single line, ellipsis). Subheading, Admin link and date are hidden. Smooth CSS transitions on padding and font-size. Expands back to full size when scrolling to top. CSS v=73.
Web App Mobile catalog header: header-inner now wraps on mobile so Admin link and upload date drop to their own full-width row below the heading and above the search bar. Prevents large heading fonts from pushing them off screen. CSS v=72.
Web App Admin: quick actions and downloads sections now match — both use flex wrap with fit-content cards. Download button arrows spaced and slightly larger/bolder via adm-arr span.
Web App Admin quick actions: removed symbols from button text, centered buttons in cards. Downloads: cards now shrink-wrap to content, buttons centred, text updated to 'Download Blank Template' and 'Download Current Data'.
Web App Admin panel full visual redesign: dark navy (#060f1c) background with grid texture, amber (#f5a623) accents, Barlow Condensed display headings, IBM Plex Mono for labels/data/code. Sticky header with amber top border. All sections styled as dark cards with amber section identifiers. Emergency/download/restore cards have distinct accent colours. Dark form inputs with amber focus rings. Matches landing page aesthetic. Login page (admin_login.html) redesigned to match.
Web App Catalog: layout toggle (table/grid) buttons moved to right side of product count row. Removed border line between search and Categories in sidebar. Dark mode filter dropdown: explicit background (#1e293b) and text (#e2e8f0) set on select and option elements so dropdown is readable. CSS v=71.
Web App Catalog layout: moved search bar and grid/table toggle buttons to top of sidebar above Categories. On mobile (≤700px) sidebar is hidden so a duplicate mobile-only search bar appears in main-content and syncs to the primary HTMX input. CSS v=70.
Web App Theme background consistency: Clean theme main-content now uses #f8f9ff to match sidebar/right-col (no more gray gap behind search bar). Bold theme main-content uses var(--surface) to unify search area with table rows. Clean+Dark main-content uses var(--bg) to stay consistent with its dark sidebar/right-col. CSS v=69.
Web App Catalog: hidden scrollbars on all catalog pages (.catalog-page) — scroll still works, bars just not visible. Clean+Dark mode fix: sidebar and right-col now use var(--surface)/var(--border) instead of hardcoded light-blue colours. CSS v=68.
Web App Theme CSS overhaul: all 4 themes now visually distinct. Clean — light table header with uppercase labels (overrode thead background, not just th). Bold — dark primary-color sidebar with white text, dense layout, uppercase heavy type. Minimal — site header uses var(--bg) with !important to override inline style, thead background cleared to var(--surface) so table header renders light with dark uppercase labels, transparent sidebar. Dark mode confirmed working. CSS bumped to v=67.
Web App Theme picker: fixed radio button visibility (added appearance:none + !important on width/height). Added ?v=62 to style.css URL in base.html to force browser cache bust. Theme preset colors now auto-fill the Header Colour picker on theme selection. Thumbnails updated with distinct colors: Classic=navy, Clean=sky-blue, Bold=deep-red, Minimal=stark black.
Web App Catalog theme presets: 4 themes (Classic/Clean/Bold/Minimal) + Light/Dark mode toggle. theme and theme_mode config fields added to database defaults, save-settings route, template_generator config rows (with xlsx dropdown validation), and base.html body attributes. CSS updated with new themeable tokens (--card-radius, --grid-gap, --row-hover, --tag-bg, --tag-color, --table-row-border, --cat-hover-bg, --cat-active-bg, --filter-bg, --img-bg) plus full theme and dark mode CSS blocks. Admin site settings form gets visual theme picker cards and a Light/Dark toggle.
Web App Preview drop zone: column chip source name colour changed from --text-faint to --text-dim for readability.
Web App Preview badge text changed from 'Live preview' to 'Drop your .xlsx and see it live'.
Web App Preview drop zone: show 6 products instead of 5 (better for 3x2 grid). Drop hint text now amber always, not just on hover.
Web App Landing page live preview: hero-demo becomes a drop zone (dashed border, drop hint). Drop an .xlsx to replace the mock with your own products — AI maps columns silently, shows detection strip, table/grid toggle, first 5 products, and Get started CTA. Rate limited to 3 requests per 15 min per IP. New route: POST /preview/analyse. Added /legal to footer, removed /docs.
Web App Legal page: data retention changed to immediate deletion on cancellation (was 30 days). Added Australian Consumer Law carve-out to limitation of liability clause.
Web App Added /legal page: Privacy Policy, Terms of Service, Acceptable Use Policy, and Fair Use. Styled to match landing/guide aesthetic. Route added to app.py.
Web App Guide page: removed all em dashes, replaced with colons, commas, or semicolons as appropriate.
Web App Guide page full visual redesign: matches landing page aesthetic — dark navy, amber accents, Barlow Condensed headings, IBM Plex Mono labels, noise texture, grid background, sticky frosted nav and sidebar with scroll-based active highlighting. Content updated: All Categories → All Products, contact email → info@sheetdropper.com, all Get started links → /signup.
Web App Landing page: removed all 'free' references. Nav, hero, and CTA buttons now say 'Get started' and link to /signup. Removed 'No credit card required' from CTA note.
Web App Superadmin dashboard full visual redesign: matches landing page aesthetic — dark navy (#060f1c), amber accents, Barlow Condensed headings, IBM Plex Mono labels/code, noise texture overlay, grid background. Stats bar with coloured top borders. Dense data table with monospaced data cells, amber slug badges, precise action buttons. All functionality preserved.
Web App Superadmin dashboard redesign: stats bar (total tenants, active, MRR, suspended), collapsible create form, full-width table layout for 1920px. New columns: email, billing status (Paying/Manual/Past Due/Canceled/Paused pulled live from Stripe API), next billing date, separate active/suspended status. Billing and active status are independent — billing is informational, active is the manual override. Route now fetches Stripe subscription data per tenant on load.
Web App Superadmin create tenant now calls _seed_new_tenant instead of init_db — new tenants created via superadmin panel now get a sample product and correct business name, matching the Stripe signup flow.
Web App last_updated timestamp format changed from '2026-03-29T09:19:48Z' to '2026-03-29 09:19' — removed T separator, Z suffix, and seconds.
Web App Security fixes: (1) Path traversal in tenant_admin_rollback — backup_id now validated against \d{8}T\d{6} pattern before path construction. (2) XSS in product page — long_description now HTML-escaped before newlines are converted to <br>. (3) api_rollback (desktop app) now calls _backup_current before overwriting live data, matching the web admin rollback behaviour. (4) superadmin_delete_tenant now cleans up uploads/ and data/ directories on disk after removing the DB row. (5) src_count in converter confirm route wrapped in try/except to prevent 500 on invalid input. Added top-level import re; removed duplicate import re as _re from Stripe section.
Web App Backup system hardening for new tenants: _backup_current now always exports from DB when db_path is provided (captures admin panel config changes, not just on-disk CSV state). Save-settings and all upload routes now pass db_path. export_backup_csvs handles missing products table gracefully. Backup/restore validation ignores 'No valid product rows' error so config-only backups are restorable. New tenant provisioning seeds a sample product so catalog, backups, and restore all work from day one. Code cleanup: imports moved to module level, docstrings updated.
Web App Stripe + onboarding: signup page (/signup), Stripe checkout session, webhook handler for auto-provisioning tenants on payment (checkout.session.completed), tenant suspension on subscription cancellation. ZeptoMail welcome email sent on provisioning with catalog URL, admin URL, password, API key, guide link. Tenant DB initialised on creation. Bug fix: upload without Config tab now preserves existing DB settings instead of resetting to defaults. Bug fix: empty-catalog page had broken url_for('admin') — fixed to url_for('tenant_admin').
Web App Redesigned landing page (sheetdropper.com homepage): dark navy/amber industrial aesthetic, Barlow Condensed display type, IBM Plex Mono labels, scrolling proof bar, CSS spreadsheet-to-catalogue demo with animated AI progress, 3-step how-it-works, 6-feature grid, stats row, 8-industry tiles, CTA section.
Web App Rewrote /docs page with current accurate information — SheetDropper branding, correct server paths, current route list, API reference, backup system, security notes, deploy workflow. Removed all stale w3swaps references. Page remains behind superadmin auth.
Web App Security hardening: replaced placeholder SECRET_KEY and default superadmin password with strong generated values in server .env. Added 10 MB file size limit to /admin/upload (converter already had it). Added in-memory rate limiting to tenant admin and superadmin login routes (10 failed attempts per IP per 5 minutes → 429). Added automatic cleanup of temp converter files older than 2 hours.
Web App Code cleanup: removed ?v= cache-busting from all CSS links (base.html, admin.html, superadmin_dashboard.html) — Cloudflare dev mode used instead. Removed dead confirm-saved converter route (never called — converter_saved.html already posts to the main confirm route). Removed dead cfg_overrides block from converter confirm route (quick-config fields no longer exist in partials). Fixed base.html and superadmin_dashboard.html which were still on stale CSS version reference.
Multiple Drop zone mutual exclusion: dropping a file into either drop zone (AI Smart Import or Upload Template) now clears the other. Web admin: AI zone clears any pending upload file; upload zone clears the AI converter result. Desktop app: AI zone resets upload zone and clears aiReviewData/nav button; upload zone resets AI zone state.
Web App Admin Site Settings: 'Header Background Colour' renamed 'Header Colour'. Save button replaced with two-column action area: 'Upload Settings' (saves to DB only) and 'Download xlsx' (downloads filled template with form settings + live catalog data). Download action merges form config over DB config so unsaved tweaks appear in the file. New 'download_with_file' action: if a file is selected in the Upload zone but not yet submitted, clicking Download xlsx shows a modal asking whether to use the pending file's products or the live site data. Flask route updated to handle action=upload/download/download_with_file. CSS v=75.
Web App Admin: Site Settings and Restore Previous Data headings centered in their sections. Site Settings moved to its own box with Header/Body/Footer sub-boxes. Row dividing lines removed from Site Settings. 'Footer Background Colour' renamed 'Footer Colour'. CSS v=74.
Web App Admin: Display Settings section replaced with a full Site Settings form covering all config fields (Header, Body, Footer sections) — business name, tagline, all colours, fonts, sizes, currency, layout, and footer text. The upload form's temporary quick-config section removed (it's redundant now). The per-converter Site Settings quick-config sections removed from both converter partials. New /c/<slug>/admin/save-settings route saves all fields via database.set_config_values(). Footer Background Colour has a 'Same as header' checkbox. CSS v=74.
Multiple Desktop: added 'Download Current Data' button on dashboard — downloads live products + config as a filled SheetDropper xlsx via new /api/download-filled route. Desktop: uploading via the Upload Template drop zone now also pushes Configure screen settings live at the same time. AI Review download already included config via cfg_overrides. Flask: added GET /api/download-filled.
Multiple Desktop: Configure screen 'Download as .xlsx' button now downloads the live config as a filled SheetDropper template xlsx (via new /api/download-config route) instead of a CSV. Desktop: 'Save to Live Site' renamed 'Upload to Live Site' to match AI screen. Desktop: AI Review screen Site Settings section replaced with a simple link to the Configure & Preview screen — AI upload now always includes current Configure screen settings. Flask: added GET /api/download-config and fill_config_tab() in template_generator.py.
Web App Converter: '(click to expand)' hint moved to sit inline just after the section title, not pushed to the far right. CSS v=73.
Web App Converter section headers: '(click to expand)' hint appears on the right side of collapsed section bars, disappears when open. CSS v=72.
Web App Converter: rewrote Remaining Columns section note to clearly explain what Attribute Filter and Ignore do on the live site.
Web App Converter: renamed 'Sample Values' column header to 'Example Values' in both converter partials.
Web App Sample values column: text now wraps instead of overflowing on mobile (was incorrectly hidden — reverted). CSS v=71.
Web App Converter mapping tables: both Catalog Fields and Remaining Columns now use table-layout:fixed with matching column widths (26%/38%/rest), and all selects share the same styling — no more mismatched dropdown widths. Sample Values column hidden on mobile (≤700px) to prevent horizontal overflow. CSS v=70.
Web App Removed remaining em dashes: column labels in converter now use 'Col S: Notes' format instead of 'Col S — Notes' (app.py, 4 occurrences); fixed section notes in converter_review.html and converter_saved.html.
Web App Converter section headers: darker grey background (#dde3ef), arrow moved back to left. CSS v=69.
Web App Converter collapsible sections: summary bars now have a light background, hover state, and a larger coloured chevron on the right — much more obvious that sections can be collapsed. CSS v=68.
Web App Admin polish: removed all em dashes (replaced with colons, commas, or middots); fixed Smart Import section blurb so it no longer implies the mapping is already saved before it's been confirmed; clicking the drop zone no longer opens the file picker (only the 'click to browse' link does, drag-and-drop unchanged); AI Analysis notes box now shows a labelled badge so it's clearly AI output; rewrote Catalog Fields and Remaining Columns section notes in both converter partials to better explain what each column/dropdown does. CSS v=67.
Web App converter.py: fixed two bugs when re-feeding a SheetDropper template back into the AI converter. (1) The legend row ('★ Red = required...') was included in AI sample data, confusing column mapping — now skipped. (2) Legend text still said 'Red = required' — changed to 'Red = core columns' to match template_generator.py.
Desktop AI Review nav button: a persistent 'AI Review' nav item now appears in the sidebar as soon as a file has been analysed, so you can navigate away and come back without re-importing. Removed the back button from the AI Review screen — use the sidebar instead.
Multiple Removed all mandatory/required field language: desktop index.html note, CSS dead rules (.required-star, .req-star), template_generator.py (Products legend, column guide labels, header comments, User Guide text, Config legend), docs.html. Colours unchanged — red/yellow kept but no longer labelled required. CSS v=66.
Multiple Removed all mandatory/required field indicators from the AI converter mapping UI — web (converter_review.html, converter_saved.html) and desktop app. Removed req-star (✱), recommended-field CSS class, and submit-time warnings. Also fixed: deploy /api/convert/start and /api/convert/confirm routes to sheetdropper.com (were never deployed); fixed apiPostForm silent crash when server returns non-JSON (wraps in try/catch now).
Multiple Desktop app redesign: replaced Upload & Backups view with two dashboard drop zones (Smart Import AI and Upload Template). Added Restore view with server-side backup cards (3-slot, with Live badge and per-backup Restore buttons). Added new server API routes: GET /api/backups (all backups), POST /api/convert/start (AI analysis), POST /api/convert/confirm (apply mapping — upload or download). Updated POST /api/rollback to accept optional backup_id in request body. Added converter IPC handlers to desktop main.js and preload.js. New AI Review view with catalog field mapping table, remaining columns table, and optional Site Settings section.
Web App Fix: restoring the oldest backup (slot 3) caused a 500 error. _backup_current() would prune to 3 slots, deleting the folder being restored before shutil.copy2 could read it. Fix: all backup files are now read into memory before _backup_current runs, then written from memory — folder existence no longer matters.
Web App Restore cards: restored backup now shows a green 'Live' badge and its Restore button is replaced (so you can see at a glance which dataset is active). CSS v=65.
Web App Restore: now backs up the current live data before overwriting (so you can undo a restore). Flash message now shows which backup was restored — business name, timestamp, and product count.
Web App AI converter: downloading the xlsx no longer clears the session. You can now download to review, then click Upload to Live Site without getting a session expired error.
Web App AI converter: Site Settings fields (business name, contact, colours) now carried through to the downloaded xlsx when action is Download. Previously they were only applied on Upload to Live Site.
Web App Upload section Site Settings: now hidden by default, appears expanded automatically when the dropped file has no Config tab (matches AI converter behaviour). Quick server-side check fires on file selection.
Web App Upload section: added collapsible Site Settings (business name, contact details, font/header/table colours) for files with no Config tab. Config tab values always take priority — form fields only fill in what's missing. CSS v=64.
Web App Restore cards: each card now shows the business name stored in that backup's config.csv, not the current live business name. Falls back to 'Catalog Backup' if no name is stored.
Web App AI converter: Site Settings fields now start blank when the uploaded file has no Config tab. Previously the existing DB config (last used name, contact details, colours) pre-filled the form — now those fields are empty so the user fills in fresh details for the new catalog.
Web App Restore cards: business name now shows as the heading (replacing the date). Date and product count moved to a single smaller line below, matching style. CSS v=63.
Web App AI converter: added quick Site Settings section (business name, contact details, font colour, header/footer colour, table background) that appears when the uploaded file has no Config tab. Page background auto-calculated from table background (RGB -10). Settings are applied at import time alongside the product data. CSS v=62.
Web App AI import now uses the same pipeline as green upload: mapped products are converted to a full SheetDropper xlsx (with Config tab pre-filled from existing DB config), then parsed through parse_xlsx to produce both CSVs. Both products.csv and config.csv are written to current/ before load_data is called.
Web App AI import now writes products.csv + config.csv back to current/ after loading into DB, keeping current/ in sync. Previously current/ was only updated by green uploads, so backups after AI imports captured stale data.
Web App Fixed AI import not creating backups. When no products.csv exists in current/ (AI import never writes CSVs), _backup_current now exports products + config directly from the DB into the backup folder instead of silently skipping.
Web App Backup system overhauled: now stores up to 3 timestamped backups per tenant. AI import routes now also trigger a backup before going live. Restore section redesigned as blue cards showing up to 3 backups. API rollback updated to use newest backup. CSS v=61.
Web App Admin: upload section heading/wording updated, darker green, left badge + centered heading layout for both AI and upload sections. CSS v=60.
Web App Admin Upload section redesigned: green drop zone (drag-and-drop + click), shows filename + Upload & Go Live button on file select, whole section is the drop target. CSS v=59.
Web App Admin Smart Import: entire purple section is now the drag-and-drop and click target — not just the dashed inner zone. Guards prevent re-triggering when clicking links, buttons, or the result area. CSS v=58.
Web App Admin AI Import section: renamed to 'Smart Import', AI-POWERED label and heading on same line centered, dividing line removed, new description text explaining the review/confirm flow. CSS v=57.
Web App Admin page: AI Import section redesigned with purple theme — '✦ AI-POWERED' label, purple border and background, purple drop zone, ✨ icon, loading text mentions Claude AI. CSS v=56.
Web App Emergency Controls: buttons moved to top of each card, headings removed. 'Take Catalog Offline' on left, 'Hide All Prices' on right.
Web App Admin page: Emergency Controls redesigned as side-by-side red-themed cards (matching template download card style). Moved above the Download Templates section. CSS v=55.
Web App Admin header: last updated format simplified to '25 Mar 2026 23:09' — no weekday, 24-hour time.
Web App Admin header: product count and Updated date now on separate lines. Last updated shows full date with weekday and time (e.g. Wed 25 Mar 2026, 10:05 pm). Existing DB timestamps updated to ISO format so JS can parse and localise them.
Web App Admin mobile header: 3-column layout — Catalog/Admin (two lines, left), company name bold centered with product count and updated date below it, View Catalog/Logout stacked (right).
Web App Admin page: removed arrow symbols from View Catalog and online guide links. Online guide link now uses standard link colour and underline. Desktop header meta shows business name, product count, and last updated all on one line at 14px. Mobile: View Catalog and Logout stack vertically, business name shown bold on its own line below, stats on next line.
Web App Admin header: now shows business name, product count, and last updated date/time. Timestamp stored as UTC ISO string and converted to browser local time via JS. Old format entries show as-is; new uploads show correct local time.
Web App Admin page: removed Current Catalog section, moved product count and last updated into the header bar. Last updated timestamp now includes time (e.g. '25 Mar 2026 10:41 AM').
Web App Download Templates section: card borders restored with tighter padding, both buttons same style, section heading removed.
Web App Download Templates section: buttons moved above text, centered, no card borders or headings. Reduced padding throughout.
Web App Admin page: Download Templates section redesigned as two side-by-side cards (stacked on mobile). Blank Template card explains the included User Guide and links to the online guide. Current Data card explains the use case (add products, update prices, re-upload). Each card has its own title, description, and button.
Web App Admin page: moved template download buttons into a single section — 'Download Blank Template' and 'Download Current Data as Template' sit side by side with combined description text. Removed the 'Reset Column Mapping' button (redundant — Run AI Review handles that case already).
Web App Admin page: added 'Download Current Data as Template' button in the Upload section — downloads the live catalog as a filled-in .xlsx template (same format as converter download). Converter mapping UI: Catalog Fields and Remaining Columns sections are now collapsible — click the heading to toggle. Catalog Fields expanded by default, Remaining Columns collapsed by default to reduce clutter.
Web App Converter download: output xlsx now uses the proper SheetDropper template format instead of a bare grid. Downloaded file includes User Guide and Config tabs (with defaults), and a styled Products tab with colour-coded column headers (red=required, blue=fixed, green=attribute filters, purple=product page), legend row, alternating row colours, and frozen header. Ready to upload or hand to someone else as a proper template.
Web App Fix converter saved mapping: ignored columns were not tracked, causing them to appear as 'new' on every subsequent upload and triggering AI every time. Now saves all column names from the file (including ignored ones) so future uploads correctly recognise them as known. Also fixed session bug where the wrong sheet name was saved when the file's sheet didn't match the saved mapping's sheet.
Web App Converter: saved column mapping — after the first AI import, the confirmed mapping is saved per tenant. Future uploads skip the AI entirely and apply the saved mapping instantly. If new columns appear that weren't in the saved mapping, they're flagged clearly and added as attribute filters. A 'Reset Column Mapping' button lets tenants clear the saved mapping to re-run the AI review. No AI cost after the first upload.
Web App New public guide page at /guide — full walkthrough of SheetDropper covering AI Import, getting started, spreadsheet format, column reference, design settings, admin panel, emergency controls, desktop app, catalog browsing, and embedding. No sensitive info. Linked from landing page nav and footer.
Web App Landing page redesign — AI put front and centre. New headline 'Drop your spreadsheet. AI does the rest.' New 3-step How It Works section with the AI step visually highlighted. New AI Import section with benefits list and a visual column-mapping mockup showing what the AI does. Features section expanded with individual product pages and emergency controls cards.
Web App Fix converter download: products_to_xlsx was missing long_description, image_2_url, image_3_url, meta_description from the output headers — mapped values for those fields were silently dropped when downloading instead of uploading. Direct upload was unaffected. Also added ?v=47 cache-bust to superadmin_dashboard.html stylesheet link.
Web App Converter: fixed injected scripts not executing (innerHTML doesn't run scripts — now re-executed manually after fetch response). Section 2 now shows 'Used above as Product Name' text instead of hiding assigned rows, keeping the column list intact and in order.
Web App Converter: column letters added to all column labels (e.g. 'Col A (blank)', 'Col B — Description/Qty') to eliminate confusion about which spreadsheet column is being referenced. Section 2 (Remaining Columns) now hides rows for columns already assigned in Section 1, updating live as dropdowns change. Downloaded filename now timestamped (converted_catalog_YYYYMMDD_HHMM.xlsx).
Web App Converter UI redesign — replaced read-only mapping table + questions with two editable dropdown tables: Catalog Fields (choose which source column feeds each field) and Remaining Columns (set each to Attribute Filter or Ignore). AI pre-fills dropdowns, user can adjust anything. Recommended fields (Product Name, Category, Price) warn on submit if left as 'not mapped'. Explicitly choosing 'leave blank' bypasses the warning.
Web App Spreadsheet converter — AI-powered import tool on the admin page. Drop in any existing .xlsx price list and Claude Haiku maps the columns to the catalog format automatically. Shows a mapping review table and answers any ambiguous questions (e.g. which price column to use) before going live. Can upload directly to the live site or download a converted .xlsx for review first.
Web App Removed muted text — all secondary text (product counts, descriptions, filter labels, category headings) now uses the full body text colour with no dimming.
Multiple Fix grid layout toggle: removed localStorage restore that was overriding the admin's default layout on page load; desktop app preview now renders grid card layout when grid is selected.
Multiple Table/Grid layout toggle — visitors can switch between table view (rows and columns) and grid view (image cards) using toggle buttons in the catalog. Per-tenant default layout (table or grid) configurable via Admin page, desktop app Configure screen, and spreadsheet Config tab. Grid cards show image, name, price, and up to 3 attribute tags.
Web App Copy API Key button added to superadmin dashboard (next to each tenant's key) and to tenant admin page — clients can now copy their key directly from their admin panel without asking
Template metro-industrial demoset CSV updated: added long_description, image_2_url, image_3_url, and meta_description columns. All 235 products now have all three image slots filled, using the 21 existing test images cycled with offsets so each slot shows a different image.
Web App Fix: product page links broken after HTMX category/search filter. The products partial route was not passing slug to the template, so url_for generated invalid URLs. Adding slug to the render_template call fixes product links after any filter or search interaction.
Web App Price unit (e.g. 'each', 'per metre') now displays on its own line below the price value — applies on all screen sizes. Mobile: product name and price columns set to fixed widths (230px and 70px) so all three key columns (name, image, price) are always visible before horizontal scrolling. Product description hidden on mobile to keep the name cell clean.
Web App Mobile: attribute columns (Size, Material, Grit, etc.) now hidden on screens ≤700px. Product name, image, and price always visible regardless of how many attribute columns a category has. Fixes categories with multiple attributes pushing the price column off screen.
Web App Product table: image thumbnail moved from inside the product name cell to its own dedicated column between Product and Price. Removes the layout gap caused by rows with images being taller than rows without. Image column is fixed at 52px wide; thumbnail links to the product page rather than opening the image directly.
Template metro-industrial demoset updated with real test images — 20 products now have image_url values pointing to uploaded images on the server. Confirms product page image display and thumbnail column working correctly end-to-end.
Web App Removed bespoke placeholder image code: grey box placeholders had been added as a tenant-specific workaround for metro-industrial during product page testing. Reverted to clean multi-tenant behaviour — no image shown when image_url is blank, consistent across all tenants.
Multiple Individual product pages: each product now has its own URL at /c/<slug>/product/<product-slug>. Slugs generated automatically from product names on upload. Product names in the catalog table are now clickable links. Product page shows name, price, short description, primary + secondary images, attribute pills, and long description — all using the client's branding from config. New spreadsheet columns added: long_description, image_2_url, image_3_url, meta_description (all optional, purple in template). SEO meta description auto-generated from description fields if not provided. Two extra example custom attribute columns (Type, Colour) added to template, giving 5 total attribute examples.
Web App Added landing page at sheetdropper.com/ — dark-theme placeholder with nav, hero, live demo link (metro-industrial), feature cards, and CTA section. Root route now renders landing.html instead of redirecting to a tenant catalog. Placeholder sign-up/pricing section ready for onboarding flow.
Multiple Rebranded from PriceList to SheetDropper: showcase site title, nav logo, and footer updated; changelog subtitle updated; links page section header updated; desktop app BASE_URL changed from w3swaps.com to sheetdropper.com; template download filename changed from pricelist_template.xlsx to catalog_template.xlsx.
Web App Attribute columns (Size, Type, Rating, etc.) now centre-aligned — values sit neatly in the middle of stretched columns instead of hanging left with large gaps.
Web App Fix: attribute columns now filtered per current result set — only columns with at least one non-blank value in the visible products are shown. Blank columns (e.g. Grit when viewing Hand Tools) are hidden automatically, removing irrelevant headers and unnecessary horizontal scroll.
Web App Showcase site (pricelist.w3swaps.com/showcase/) completely rebuilt from scratch — replaced large fan-stack cards and verbose sections with a compact dark-theme design: horizontal pain-points strip, 3-column step cards, compact 2-column feature rows (including new Desktop App feature), In Action screenshots, Before/After comparison, and CTA. Significantly shorter on mobile.
Template Spreadsheet template: expanded User Guide tab with full documentation — Products column guide (every column with examples and required/optional flags), Config settings guide (all header/body/footer settings explained), image URL hosting tips, colour entry guide, common mistakes, and tips for best results.
Web App Web app: added /docs page with three-tab documentation — User Guide (catalog browsing, search, filter, embed), Admin Guide (upload, emergency controls, rollback), and Developer Reference (server setup, routes, API, config system, DB structure, CSS vars, HTMX patterns, deploy workflow).
Desktop Desktop app: replaced placeholder Help section with full user documentation covering all screens and settings in plain language for small business owners.
Desktop Code review pass: fixed missing .trim() on footer heading and footer subheading text fields in the desktop app — consistent with all other text fields. No other issues found.
Desktop Desktop app preview: fixed footer being cut off on mobile size (390px) — added padding-bottom to clear the fixed mobile bar. Fixed stray scroll on all non-mobile preview sizes by locking overflow:hidden on the body; mobile keeps scroll so the sticky category/filter buttons are visible.
Desktop Improved notification messages throughout the desktop app to be more specific: upload now says 'X products now live on site', config restore says 'Config backup restored to live site', save settings says 'Design settings applied to live site', logo upload says 'Logo uploaded to live site', local backup save says 'Config saved to local backup', template download says 'Template downloaded to your Downloads folder'.
Desktop Desktop app: added Activity log to sidebar — every notification (upload, save, error, etc.) is logged with a timestamp and persists for the session. Shows ~5 entries at a time, scrollable without a visible scrollbar, snaps to latest on each new event.
Desktop Desktop app: moved sidebar divider line so it sits between Help and Disconnect, rather than above both.
Multiple Default currency changed to AU$ throughout — desktop app configure screen and spreadsheet template.
Multiple Fix desktop app logo preview and position bugs: (1) preview panel now shows the newly selected local logo immediately, before it's uploaded; (2) unchecking both Left and Right logo position checkboxes now correctly hides the logo (previously defaulted to left); fix applied to both the desktop preview and the live catalog.
Web App Fix logo upload bug: re-uploading a logo now correctly replaces the previous one. Old logo files are deleted before saving the new one (prevents stale files from lingering if the extension changes), and a timestamp is added to the URL on every upload to bust Cloudflare and browser cache.
Template Spreadsheet template: colour field notes updated to clarify that the typed hex code takes priority over cell fill colour — to use fill colour, clear the cell value first.
Template Spreadsheet template: colour fields in the Config tab now show their actual colour as the cell background, with automatically readable text (white on dark, dark on light). Makes it easy to see what each colour looks like at a glance.
Template Spreadsheet template: Config tab pre-filled with sensible defaults — header/footer colour (#1F2D3D navy), Arial font throughout, sizes (heading 22px, subheading 13px, body 14px, footer 12px), white text on header/footer, light grey page background (#f5f6f8), white table background. Clients can now upload immediately after filling in just their business name and products.
Multiple Spreadsheet template updated: Config tab restructured into Header, Body, Footer, and Other sections. New fields added: business_info, logo_position (left/right/both dropdown), heading_font_color, subheading_font_family, subheading_font_color, page_bg_color, table_bg_color, footer_heading_font_family/size/color, footer_subheading_font_family/size/color. All new colour fields support typed hex or cell fill colour. Font family dropdowns added for all font fields. csv_parser updated to support cell fill colour for all new colour fields.
Multiple Configure screen overhaul: (1) Logo position changed from radio buttons to checkboxes — both Left and Right can now be selected simultaneously to display the logo on both sides of the header, applied to live catalog; (2) Footer font row labels renamed to Footer Heading Font and Footer Subheading Font; all configure screen labels standardised to the same font size; (3) Section headers (Header, Body, Footer, Upload) now display as bold filled dividers for clear visual separation between sections; (4) Vertical spacing tightened throughout the configure panel
Desktop Configure screen font row layout: each font row now shows all labels (font name, Size, Colour) on one line above all their inputs; Size and Colour boxes made equal width (44px); all background colour pickers matched to same size (44×26px) and aligned with the Colour column in font rows; Size label centre-justified
Multiple Configure screen — Body settings added: Page Background colour (page_bg_color) and Table Background colour (table_bg_color) fields control the outer page background and the table/sidebar/filter panel background independently; both applied to live catalog and visible in preview; settings sections reorganised to Header → Body → Footer → Upload/Save; Logo moved inside Header Settings; Footer Subheading changed from multi-line textarea to single-line text input
Multiple Configure screen — Footer redesigned to mirror Header: Footer Heading and Footer Subheading text inputs each with their own font/size/colour row; old Contact Font, Footer Text Colour, and Footer Font Size fields replaced by the new per-element font rows; Price Note field moved into Footer Heading; new server fields: footer_heading_font_family/size/color, footer_subheading_font_family/size/color, applied to live catalog
Desktop Configure screen UI polish: Logo position radios (Left/Right) moved to same line as Logo label; input padding reduced for a more compact layout; font select boxes made shorter; Size and Colour micro-labels added above the size/colour inputs in all font rows
Multiple Configure screen: Logo moved to top with Left/Right position option (applied to live catalog); Business Name (heading) label update; heading/subheading/body fonts now show font+size+colour on one line; body font size field made visible (was hidden); Main Font renamed to Body Font; logo_position field added to server config
Multiple Configure screen redesign: Business Name renamed to Business Name (header); new Business Info (subheading) field for header tagline/slogan (separate from Contact Info which is now footer-only); added Heading Font Colour and Subheading Font Colour pickers; hex text inputs removed from all colour pickers (use native picker); heading font+size and subheading font+size now on single rows each; new subheading_font_family field; all new colour/font fields applied to live catalog
Desktop Desktop app: removed default application menu bar; added Help section to sidebar with placeholder content; currency dropdown updated to US$ and includes a Custom option for typing any symbol (points, credits, tokens, etc.)
Desktop Configure screen: 61-char limit on business name and contact info; contact font size and colour options; Revert button to reset form to last saved state; colour pickers now inline (label + picker + hex on one line); currency symbol changed to dropdown ($, AU$, NZ$, £, €, ¥); settings reorganised into Header, Footer, and Other sections
Web App Created changelog system — new /changelog page with JSON-backed log of all changes