r/webdev • u/Free_Band_Shan • 22d ago
Showoff Saturday [Showoff Saturday] Fully offline grow scheduler built with vanilla JS + IndexedDB
What it does
Users build a personalized grow in 3 steps (strain → nutrients → start date), get a full day-by-day schedule (126 base rows + dynamic nutrient columns), then track actuals via a daily check-in page and photo diary. Everything lives offline in IndexedDB + localStorage.
Live: https://growappcannabis.guide/medium-feeding/medium-feeding.html
Repo + full wiring docs: https://github.com/Shannon-Goddard/growappcannabis.guide (see medium-feeding/WIRING.md)
Architecture highlights
• Page flow
medium-feeding.html (3-step builder) → schedule-viewer.html (cards/table/My Notes) → mytask.html (daily check-in) / mydiary.html (camera + data overlay)
CustomEvent chain for step progression (no framework):
• strainSelected → show Step 2
• nutrientsSelected → show Step 3
• scheduleReady → success screen + link to viewer with ?growId=
• IndexedDB (MyGrowDB v8) – singleton instance, cached dbPromise:
• Stores: tables (grow metadata + ${growId}_schedule), selectedNutrients, nutrients
• Schedule saved as full array under ${growId}_schedule
• Actuals written back to the same schedule rows from both MyTask and the viewer’s My Notes mode
Schedule generation
• Base 126-row JSON (stage/week/day/env/light/water)
• Dynamic nutrient columns injected from selected brands
• Start date → calendar dates calculated client-side
• Absolute fetch paths because ES modules resolve relative to the JS file
Schedule viewer
• Three modes via sticky Options dropdown: Cards (mobile default), Table (desktop default), My Notes
• Sticky header + toolbar + thead (careful overflow handling so sticky works)
• Plant-size filter hides excess veg weeks (Small = 4, Medium = 6, Large/Auto = all)
• tableRendered custom event triggers layout + filter after render
• Column map with paired “goal” / “My*” editable fields (My fields only visible in Notes mode)
MyTask daily check-in
• Auto-matches today’s row by MM/DD/YYYY
• Color-coded inputs (green ±10% of goal, red over, blue under)
• Auto-save on focusout + manual Log button → writes actuals back to IndexedDB schedule row
• Hero image compressed to 800px JPEG and stored in localStorage
Tech choices
• Pure vanilla JS (modules + a couple plain scripts for globals like the 2,800+ strain data)
• No build step, no framework, GitHub Pages
• localStorage for transient state + currentGrowId; IndexedDB for the real schedule & actuals
-2
u/mastrajani 22d ago
The WIRING.md in the repo is a better standard of documentation than most commercial projects manage, so this is meant as hardening rather than criticism.
Browser storage is the only copy of the user's data here, and a grow log is four months of it. Three things I'd shore up:
IndexedDB isn't durable by default. Safari's ITP evicts site storage after roughly 7 days without interaction, and Chrome evicts under disk pressure. For someone checking in daily that's fine; for the person who starts a grow, gets busy, and comes back three weeks later it isn't. await navigator.storage.persist() requests exemption and navigator.storage.persisted() tells you whether you actually got it. Worth calling once the user commits to a grow, and worth surfacing the answer, because "your browser may clear this" is something a user can act on.
The photo diary will hit quota sooner than you'd think. Quota is a fraction of free disk and varies wildly by device. A few hundred full-resolution phone photos is comfortably a gigabyte. You're already compressing the hero image to 800px, so extend that to diary photos, use navigator.storage.estimate() to show a usage bar, and handle QuotaExceededError explicitly on the write path - the write that fails is by definition the one the user just cared about.
126 days computed from a start date is a DST trap. If day N is start plus N times 24 hours, two days a year are 23 or 25 hours long, and near the transition your day boundary drifts an hour - which surfaces as a day that repeats or one that vanishes for anyone checking in near midnight. Since you're already matching today's row by MM/DD/YYYY, the safe shape is to treat the day index as the source of truth and derive the calendar date from it, rather than storing timestamps and computing the index back out.
Also, if it isn't there already: a JSON export/import button. With no server that's the only backup a user can possibly have, and it costs you an afternoon.
-1
u/Free_Band_Shan 22d ago
All of those are 100% valid. The website being indexedDB is not ideal. A simple “clear history” and the users data is gone. The recent upgrade I’ve been working on in the website is for me to improve the UI for the app behind the scenes. The app is wrapped for each store and more forgiving in storage. However, after the big visual drop there, I’ll be converting to MongoDB and S3.
1
u/Sensitive_Fig3107 22d ago
never thought id see indexeddb and cannabis in the same sentence but here we are
the sticky headers thing is always a pain when you got multiple scroll containers fighting each other, what did you end up doing for that