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:

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:

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

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.