Admin-side rebuild — campaigns, wishlists, backups, and a worker dashboard
Five days after the storefront rebuild, most of the back-of-house gaps are closed too. This batch is less visible to customers but it's the difference between "we built a store" and "we run a store." The shortlist:
Email campaigns — one sender, three audiences
The general newsletter we shipped last week had double opt-in working, unsubscribe links, and a confirmed-subscriber list. What it didn't have was any way to actually send a newsletter. Today's /admin/campaigns/ closes that loop.
It's one composer that covers three audiences:
- Newsletter — all confirmed, non-unsubscribed subscribers.
- Launch announcement — the pre-launch interest list for a specific product (masterETH, quadETH, cueSystem). Useful for the "we just shipped, your hardware is ordered" email.
- Win-back — customers who haven't ordered in N days (default 90). Drop in a Stripe promo code from the Discounts page and the template substitutes
{{promo_code}}into the body. - Plus a custom list option — paste in a comma-separated bunch of addresses for one-off sends.
Workflow: write the subject and HTML body, see the recipient count update live, send a preview to yourself, then dispatch. The send runs in the background (via ctx.waitUntil), batched 8 at a time at ~20/sec to stay polite to Resend, with per-recipient status tracking so you can see which addresses bounced.
Every email gets a one-click unsubscribe link in the footer. Hit unsubscribe once and that address is excluded from all future audiences — newsletter, launches, win-back — by flipping unsubscribed_at on the central newsletter_subscribers table. One opt-out, all marketing off.
There's no per-email pricing tier here either — same flat Resend transactional bill as the order confirmations.
Wishlist / save-for-later
If you've ever browsed our store while logged in and thought "I'll get back to this later," there's now a place to put it. Every product page has a ♡ Save for later button below the orange Add to cart. Clicking it stashes the variant (with the right Stripe price ID, the right size/option label, and the product photo) on your account, viewable any time from the new Wishlist tab at https://expanseelectronics.com/account/.
Logged out when you click it? No bounce, no friction. We stash what you wanted in your browser's local storage and send you to the login page with a one-line hint at the top: "Log in or sign up to save the dualETH-PixelControl (PoE) to your wishlist." Sign in, the wishlist tab opens, the item is there. Same flow whether you signed in or signed up from cold.
Behind the scenes it's a tiny wishlist_items table keyed on (user_id, price_id) with name/url/image snapshotted at add time — so if a product gets renamed in products.json later, the wishlist still shows something readable.
Daily D1 backups → R2
D1 doesn't have point-in-time restore on the free plan. Until today, our entire backup strategy was "Cloudflare doesn't lose data." That's mostly true; it's also not a strategy.
A cron job now runs every night at 03:00 UTC, dumps every D1 table to a single JSON blob, and writes it to a fresh R2 bucket (expanseelectronics-backups). 30-day retention, automatic pruning, and a new /admin/backups/ page that lists the recent dumps with download buttons and a Run backup now trigger for ad-hoc snapshots before a risky change.
A few high-churn tables (worker_logs, status_checks, user_sessions) are deliberately excluded — they regenerate from production traffic on their own, so backing them up just inflates every dump for no recovery value.
The whole thing — cron, dumper, retention, download UI — is roughly 150 lines including the admin page. No third-party backup service in the loop.
Worker activity dashboard
The previous storefront-rebuild post mentioned the public status page (Stripe / Resend / D1 reachability checks). What we didn't have was visibility into our own worker — how many requests per day, which endpoints get hit, error rates, slow paths.
There's now a Worker activity card on the admin overview. It samples 10% of successful requests and 100% of errors into a small worker_logs table, then aggregates: last-24h request count, 4xx and 5xx tallies, average + max latency, top paths, and the last 25 errors with their status code and duration. 7-day retention, pruned by the same scheduled handler that runs the backup and status probes.
10% sampling means the absolute numbers under-count by about 10×, but the ratios are accurate and the table stays small. If we ever want exact totals, Cloudflare's own Workers Analytics is one toggle away — for now this gives us the questions we actually ask ("is anything 500-ing?", "what's hammering the API?") without the dashboard tab-switch.
Customer lookup, install-shoutout review, stock-alerts admin
A handful of admin pages got a polish pass too:
/admin/lookup/takes an email and shows every order, abandoned cart, account, newsletter, launch-interest, and install-shoutout row associated with it. One screen for the "who is this person?" support question that used to mean four separate SQL queries./admin/shoutouts/is where customer install photos for Share your install get reviewed — accept (and bundle a thank-you discount code), decline (with notes), or sit. Replaces the previous "read the admin notification email and reply by hand" workflow./admin/stock-notifications/lists everyone on the back-in-stock waitlist grouped by product, so we can eyeball demand before re-ordering parts.
These three existed in skeleton form last week but were genuinely broken — wrong HTML scaffold, wrong API helper invocation. Fixed today, plus matched to the same skeleton as the rest of the admin via a shared subnav generator.
Smaller bits
- Multi-size favicons generated from our Instagram avatar — 16/32/180/192/512 PNGs and a
site.webmanifestso adding the site to your iOS or Android home screen actually shows the brand mark instead of a screenshot. - Single-source admin subnav. Adding a new admin page used to mean editing eight HTML files to add the link. Now you edit one constant in
/admin/admin.jsand every admin page picks up the change. - Worker version is now
27b10010-…with the new routes registered, daily-backup window logic in the scheduled handler, and the request-logger wrapper around the fetch handler.
What's next
We've now built basically every customer-side and back-of-house feature on the to-do list since the September 2024 storefront stub. So the next blog post here will almost certainly be back on the product side — the v7.5.3 dualETH firmware (drop scheduled for next week) or a deep-dive on the cueSystem hardware prototype now that we've got the first revision of boards in hand.
If you find anything that breaks, the contact form at https://expanseelectronics.com/#contact lands directly in our admin app these days; reply will be from a person, not a ticket queue.