Working…

🌿
Forest Studio Labs
End-to-end encrypted · GitHub-backed
Connecting…
🔐 Saved device
Trust this device? Your password will be saved encrypted.
Encrypted · GitHub-backed
📋
Select a note from the list
📜 Edit History
New Entry
Number of propagules / cultures produced in this entry.
|
0 wordsLine 1
📚 DOI / References
Attach DOI references to this note — non-PDF papers, datasets, or any DOI-addressable resource.
New Accession
New Media Recipe
Subculture Reminders
Due dates · Overdue · This week · This month
🏡
Greenhouse Plants
Track your stock plants — source of explants for new culture lines, and destination for lab output.
Bulk import · supports unregistered hybrids · all entries get 🏡 GH flair automatically
☣️
Contamination Dashboard
Events · Trends · Photo scan · Batch trace

📷 Contam Scan

Point camera at a culture vessel. The scanner reads the bottle QR to identify it, then texture analysis flags contamination. One tap marks the bottle contaminated and creates a log entry — both wired to the value chain.

🔍 Batch Trace

Enter a bottle code to find all related bottles (same batch, session, or parent) and see which are contaminated.

📊
Lab Analytics
Growth · Success rate · Contam trends · Species breakdown
Lab Tools
Timers · Stopwatches · Media scaler · Batch calculator

Countdown Timers

Stopwatches

🧫
Surface Sterilisation
Full protocol library with timed step-by-step stepper — now on its own dedicated page.

⚗️ Media Volume Scaler

🧮 Batch Calculator

Select a saved recipe → enter bottle count → get scaled ingredient totals.

🌱
Species
In culture · Taxonomy packs · Flairs · GBIF search
🌿 Taxonomy DB ↗

Derived from your notes and accession registry.

🧫
Surface Sterilisation Lab
Protocol library · Guided timer · Autoclave run log
Protocol Library
Saved Protocols
No saved protocols yet.
Custom Protocol

🔥 Autoclave Run Log

📦 Lab Stock & Inventory

Categories
✦ Ravana Stock intelligence — on demand
📰
TC Research News
PubMed · Full abstracts · Pinnable · Archived to repo
Load

Select a topic and toggle on to load papers from PubMed.

📅 Upcoming 14 days
Loading…
📊 This month

🌿 Forest Studio Labs

v3.52

May 2026 · Encrypted · GitHub-backed · Single-file PWA

✨ What's New — v3.52
🔐 Math CAPTCHA on login — A simple arithmetic gate (X + Y = ?) appears before the Sign In button. Blocks bots and scrapers; resets on every lock. Served from VPS — repo is now private, source not public.
▸ v3.50 release notes
🌿 Animated tab favicon — Canvas-based favicon cycles leaf → green planet → leaf on a 5 s loop with cross-fade transitions. Pure vanilla JS in <head>, zero dependencies.
▸ v3.49 release notes
📊 Real token tracking — Cloudflare Worker now extracts actual token counts from all three providers (Groq: prompt_tokens/completion_tokens; Claude: input_tokens/output_tokens; Gemini: promptTokenCount/candidatesTokenCount) and forwards them as inTok/outTok in every API response. The session chip now shows real usage instead of 0.
📈 Token pill — New pill next to the Thin/Robust toggle shows session tokens vs model TPM limit (AI_MODELS.tpmLimit). Groq models: 6,000 TPM limit (green → yellow at 65% → red at 90%). Claude/Gemini: shows raw session count. Updates after every call alongside the session chip.
🏷️ Per-message token badge — Each AI reply now shows a small ↑inTok ↓outTok badge at the bottom right. Hover for total. CSS: .msg-tok-badge. Ghost animation (✦) replaces braille spinner for Robust mode — seeking wobble, scan beam, typewriter captions, burst-to-green on completion.
▸ v3.48 release notes
⚡ Thin / 🔬 Robust mode toggle — New button in Ravana header toggles context depth. Thin: fast, cached, lightweight context (current default). Robust: 6-layer animated deep scan on the first turn — lab vitals + alerts, per-species breakdown, contamination history, supply inventory, schedule & reminders, and active entry. Follow-up turns in both modes use rolling conversation history only.
🎯 Token efficiency overhaul — Rulebook compacted 6,673→3,339 chars. Context cache (90s TTL). PubMed gated to research-keyword queries. Groq: useTools:false removes ~4,700 tokens/request. _callAIExtract() lightweight path. 6-message rolling history, cleared on panel close.
🔄 AI fallback chain fixed — Cross-wiring resolved. Hidden 8B fallback removed. Unified chain: primary → Gemini → Groq 8B. Gemini updated to gemini-2.5-flash.
▸ v3.47 release notes
📋 Plan → Execution Bridge — Weekly schedule now shows a "📋 Plan Actions" section. For each active production plan, it compares required vs actual bottle counts per stage and shows action items when a stage is current or starting within 14 days.
✂️ Take Explant button — Mother plant cards now have a "✂️ Take Explant" button. One tap: records the use, increments propagation_count, sets last_inspection to today, and optionally navigates to new bottle creation with the species pre-filled.
🏡 GH stock auto-deduction — When a sale is marked delivered, greenhouse plant quantities are automatically decremented by the species and quantities in the sale's item list.
▸ v3.46 release notes
⚙️ Lab Orchestration Engine_labOrchestrate(event) is called after every subculture and every status change. It fans out consequences automatically: sets review dates, deducts supplies, and updates reminder badges without any manual steps.
📅 Auto review scheduling — After a subculture, all new bottles get next_review set to date_prepared + 14 days (inoculated stage default). After a status change (e.g. growing, rooting), review is reset to today + expected days for the new stage.
🧪 Auto supply deduction — When a subculture is confirmed with a recipe that has ingredients, supply usage is automatically deducted proportionally (qty_per_batch ÷ batch_size × number of bottles produced). No manual logging needed.
📦 Batch Lifecycle Panel — "📦 Batches" button in the Bottles sidebar. Shows every active batch as a card with stage, bottle count, days-in-stage progress bar (colour: green→blue→yellow when overdue), projected output value, and next review date. Click "View →" to filter the bottle list to that batch.
▸ v3.45 release notes
📋 Reorder Sheet — Supply section reorder panel grouped by supplier with editable quantities, live cost estimates, clipboard copy, and print.
▸ v3.44 release notes
📸 Accession Photo Gallery — Third tab in accession detail showing all gallery photos matched by species. Photo count badge, Add Photo shortcut, lightbox from accession.
▸ v3.43 release notes
🧪 Recipe ingredients — Structured supply-linked ingredient list. Auto-cost mode computes media_cost_per_bottle from supply prices on save.
🔗 Supply backlinks — Supply cards show which recipes use them as ingredients.
▸ v3.42 release notes
📊 Vault view toggle — Vault tab: 📊 Current inventory (qty × today's stage price) vs 🔮 Projected output (ratio chain). Fifth header card showing current total.
🤖 Ravana updated — get_value_inventory returns current_inventory_total_inr and current_value_inr per species.
▸ v3.41 release notes
☣️ Contam Scan — Camera scan reads bottle QR to identify it, runs texture analysis, one-tap marks contaminated + creates Contamination Log note.
🔗 Full workflow wiring — updateBottleStatus('contaminated') auto-creates log note. Bottle detail shows linked contam events. Batch trace has 📷 QR scan button.
▸ v3.40 release notes
✂️ Subculture Wizard — "✂️ Subculture" button in every active bottle's detail header. Creates all offspring with parent_id + batch_id pre-linked. QR scan routes to wizard via _subcultureMode flag.
🔗 parent_id auto-linked — Prior to v3.40 parent_id had to be typed manually. Now all offspring from the wizard have it set, making contam loss in the value chain actually work.
▸ v3.39 release notes
🔗 Live Value Chain — Every bottle now has a calculated production cost (media + labour + mother material) and a projected output value (surviving offspring × next-stage market price). The vault shows these three numbers per bottle grouped by species.
📐 Ratio cascade — bottle override → recipe ratio → production plan stage ratio. Bottles without any ratio show "— set ratio" and are excluded from output value.
💸 Production cost — Recipe: media cost/bottle, lab hours/batch ÷ batch size. Operators: hourly rate. Bottle: assign operator. Mother plant: market value split across propagation count.
☣️ Contam loss wired in — Offspring bottles with parent_id links and status='contaminated' are automatically subtracted from projected surviving output.
▸ v3.38 release notes
💰 Species-aware pricing — Value Inventory now has a Species overrides section. Set custom prices per species per stage (e.g. Nepenthes rajah at ₹2000/bottle, banana at ₹15). Leave blank to fall back to the global default.
📊 Species × Stage vault grid — Species × stage matrix: rows = each species, columns = active TC stages, cells = count and ₹ value. Bottom row = totals per stage. Sorted by species value descending.
🔧 _valGetPrice everywhere — Species-specific prices applied in vault, dashboard, and Ravana tool. CLAUDE.md RULE 7 added.
▸ v3.37 release notes
🔗 Wire Pass — Planning stage deadlines and Mother plant inspections appear on Calendar. Dashboard alerts flag plans behind target and overdue inspections. Delivering an order auto-updates linked bottles to "dispatched".
🌿 Mother Plant Registry — "Mothers" tab in Planning. Register stock plants with species, location, condition, inspection schedule. Wired to Calendar, Alerts, Ravana.
📋 Weekly Work Plan — "Weekly" tab in Planning. 7-day lookahead of all scheduled bottle and note reviews with one-click open links.
🔬 + 🌿 + 📱 + 🔊 — Contamination Forensics heatmap, Plant Passport certificate, WhatsApp dispatch, Ravana Voice read button.
▸ v3.36 release notes
⏱ Lab Timers — New timer panel accessible from the nav bar (⏱ button) and dashboard Quick Actions. 5 presets: Autoclave (20m), Ethanol dip (30s), Bleach soak (20m), Laminar flow (30m), Media pour (5m). Custom duration entry. Multiple concurrent timers, countdown display, badge on nav button, browser notification + toast on completion.
🏷 Bottle QR Labels — "🏷 Labels" button in the Bottles sidebar. Opens a picker modal, select bottles, click Print. Generates a printable window with 70mm × 38mm label grid — QR code, bottle code, species, stage, and date.
🌡 Live Environment Strip — When IoT devices are registered, the Home dashboard shows a live environment strip. Each device shows online dot, name, and latest sensor readings.
🤖 Ravana IoT control — 3 new tools: get_iot_status, control_device (write+confirm), get_active_timers.
▸ v3.35 release notes
📊 Sales Analytics — New Analytics tab in Sales. Revenue bar chart for last 12 months, top 5 customers by revenue, top 5 species by revenue. Summary cards: total revenue, delivered, orders, and avg per month.
📄 Invoice in Pipeline — "Invoice" button now available directly from the Pipeline order detail panel, alongside the stage controls. Prints the full GST invoice in a new tab.
🧪 Bottle Linking — Link specific bottles to a sales order from the Pipeline detail view. Picker shows eligible bottles matching the order's species. Linked bottle IDs are saved to the order record (bottle_ids[]).
🤖 Ravana CRM tools — 4 new tools: get_customers (with order stats), get_orders (filter by stage/customer), create_order (write+confirm), update_order_stage (write+confirm). Ravana can now manage the entire sales pipeline.
▸ v3.34 release notes
👥 Customer CRM — Full customer database in the Sales section. Name, company, phone, email, WhatsApp, GST number, address, city, state, payment terms, source (IndiaMART/direct/referral/exhibition). Order history per customer. New Sale form auto-fills when you pick from CRM.
📋 Pipeline Kanban — Orders now flow through a visual Kanban: Enquiry → Confirmed → Propagating → Ready → Dispatched → Delivered. One-click stage advance from each card. Total pipeline value shown. Click any card for order detail + stage controls.
🚚 Dispatch Tab — Dedicated dispatch queue for Ready + Dispatched orders. Enter courier name and tracking number, hit Dispatch to move to dispatched stage, or Delivered to close the order. All saved to the order record.
🔗 CRM integration — Customers stored separately in sales/customers.enc. Every new sale captures customer_id and stage. Linked orders counted and valued per customer. Loaded at login in parallel with all other data.
▸ v3.33 release notes
🧠 Ravana: 30 new tools — Ravana goes from 15 to 45 wired functions. He can now read every section of the app: notes, greenhouse, supplies, reminders, accessions, journal, finance, production plans, value inventory, and full analytics.
🔍 Intelligence toolsDaily briefing, predict weekly actions, analyze contamination pattern, identify at-risk, compare species, project output, media prep calculator. Ask Ravana for a morning briefing or "what needs to happen this week" and he'll synthesise the entire lab state.
🗺 Navigate & act — Ravana can now take you to any section (navigate_to), open specific bottles (open_bottle), and open species detail (open_species). Say "open bottle NR-042" and he'll go there.
✍️ 9 new write tools — Ravana can add reminders, log journal entries, do batch bottle updates, record subculture transfers (with cultures_out note), add supplies, register accessions, add greenhouse plants, create production plans, and mark reviews done. All require tap-to-confirm.
▸ v3.32 release notes
🗓 Production Planning Engine — New Planning section. Set a species target (e.g. 50 Stage IV N. rajah bottles by Sept) and the system back-calculates how many bottles you need at each stage today, accounting for contamination rate and multiplication factor per stage.
📊 Plans tab — Cards per plan showing Gantt bar (I→II→III→IV timeline), per-stage bottle requirements, current stage badge, days left / overdue indicator. Edit or archive any plan.
📅 Schedule tab — 4-week transfer calendar. Shows when each stage hand-off is due (Stage I initiation, I→II, II→III, III→IV), grouped by week with "Today / Tomorrow / Nd" urgency labels.
📈 Status tab — Live comparison of required vs actual bottle counts per stage (reads from Bottles section). Green / yellow / red traffic-light per stage + progress %. Reveals gaps at a glance.
▸ v3.31 release notes
💹 Financial Engine — New section with full P&L accounting. Tracks labour cost (configurable headcount × salary), monthly supply spend (from Supply section cost_total), and overhead. Computes cost-per-plantlet based on Stage IV output each month.
📊 Monthly P&L table + chart — Last 12 months of Revenue vs Costs with profit/loss per month. SVG bar chart for last 6 months with revenue (green) vs costs (red) bars.
🏷 Cost Breakdown tab — Supply spend by category with inline bar chart showing relative share. Top 10 supply items by total cost. Fixed monthly cost KPIs (labour + overhead).
⚙️ Finance Settings — Configure headcount, salary per head, working days/hours, monthly overhead, and operator names list (used for LIMS in v3.33). Settings saved to finance/settings.enc.
▸ v3.30 release notes
🎨 Page-head migration complete — Reminders, Contam, Analytics, Tools, Species, Sterilise, News converted from inline h2/p to .page-head. Sterilise grid fixed to minmax(260px,320px).
▸ v3.29 release notes
🎨 Section header system — Wrote CSS for the .page-head component (was unstyled in Supply, Sales, Files, AI). All full-page sections now show a consistent titled header bar.
📅 Schedule · 💰 Value · 🔍 Search page-heads — Three sections converted: Schedule + Search also changed to fullpage class. Value inline h1 removed from render function.
📸 Gallery auto-load — Navigating to Gallery now auto-calls loadGallery() — no manual "Load Gallery" click needed.
▸ v3.28 release notes
💾 Firmware Presets — Save any Firmware Studio form state as a named preset stored in localStorage. Preset pills appear above the Templates strip — click to load, ✕ to delete. Never re-enter the same device config twice.
📂 Multi-file Editor — Editor tab now shows file tabs: main.ino + config.h by default. Add your own files (helpers.h, sensors.cpp…). Each file has independent CodeMirror state. Download as ZIP when 2+ files are open.
📡 Serial Monitor — New Serial tab: connects to device via Web Serial API. Live output with timestamp, auto-scroll, colour-coded ERROR/WARN/INFO lines. Send commands to device from input bar. Baud selector (9600→921600). Chrome/Edge only.
⚡ Flash tab — New Flash tab with 3-step flow: Connect port → choose action → run. Write config: sends SOH+CFG JSON+EOT over serial so device saves WiFi/Worker credentials to NVS without reflashing. Flash .bin: fetches compiled binary from URL and writes in 512-byte chunks with progress bar.
📄 Real example sketches — Five complete, compilable .ino files in firmware/examples/.
▸ v3.27 release notes
♻ Device ID persistence (NVS/EEPROM) — Generated firmware now saves its D1 device ID to ESP32 NVS (Preferences.h) or ESP8266 EEPROM on first registration. Reboots, power cuts, and OTA flashes reuse the same record in D1 — no more phantom ghost devices accumulating on every restart. Worker register endpoint now accepts an optional client-provided id for the upsert.
🌡 Multi-channel sensor current_value — Worker heartbeat handler now updates parent pin rows in D1 when sub-channel readings arrive (e.g. env_temp reading also updates the env parent pin). Automation rule labels now resolve sub-channel IDs to human names ("Env Temp °C") using the cached device state.
🗑 Device delete — Each device card has a ✕ button that removes the device, its pins, commands, readings, and rules from D1 in one call. New DELETE /api/iot/devices/:id Worker endpoint performs cascaded cleanup.
🧹 Sensor readings auto-prune — Worker prunes sensor_readings rows older than 7 days with 5% probability per heartbeat, keeping D1 storage bounded without a separate cron job.
▸ v3.26 release notes
🐛 Firmware + IoT fixes — everything now actually works — Seven firmware generator bugs fixed: readings are now embedded in the heartbeat body (automation rules fire correctly), all Worker endpoint paths corrected, JSON key fixed from pin to pin_id, gpio lookup via new PINS[].id field. Multi-channel sensor sub-channels in automation dropdown. Commercial device 🔄 Poll button.
▸ v3.25 release notes
🔌 Full Hardware Catalog — Chips, Sensors & Actuators — 22 chips, 28 sensor types, 50 templates.
▸ v3.24 release notes
📖 IoT Guide — click-by-click how-it-works — "📖 Guide" button in the IoT header opens a full-screen 8-tab interactive guide: System Overview (architecture diagram + 5 key concepts), WiFi Devices (8-step first-flash walkthrough), GSM/4G Devices (modem compatibility table, India APN table for Airtel/Jio/BSNL/Vi, wiring table, 6-step setup), Commercial Devices (Tasmota/Shelly Detect flow), Firmware Studio (Form/Editor/AI tab walkthrough), Automation Rules (7-step rule setup + cross-device guide), Camera Feeds (MJPEG + /cams portal setup), Swarm Deploy (batch setup guide). Compatible with any TinyGSM-supported modem, not just Waveshare A7670E.
▸ v3.23 release notes
🤖 Full Automation Engine — global rules panel, cross-device if/then, enable/disable toggle, hysteresis, cooldown.
🔄 HTTP OTA Push — paste .bin URL → queue in D1 → device downloads + flashes on next heartbeat poll.
🛡 /cams Admin Master Token — bypass all room passwords, All Rooms view, Worker /api/cams/verify-master endpoint.
▸ v3.22 release notes
📡 GSM / Cellular Firmware — TinyGSM support for A7670E/SIM800L/SIM7600/EC600; connectNetwork() unifies WiFi and GSM paths for all D1 endpoints.
⚡ Swarm Deploy Wizard — batch firmware generation and ZIP download; Register All creates all devices in D1 in one click.
🔍 Network Scanner — CIDR range probe, detects Tasmota/Shelly, click chip to pre-fill commercial device modal.
📷 Camera Feeds + /cams portal — in-app camera feed panel with load gate; standalone room portal at /cams with SHA-256 room passwords.
▸ v3.21 release notes
🏭 Commercial Device Onboarding — Add Tasmota and Shelly (Gen 1 + Gen 2) devices directly from the IoT panel. Switch "🏭 Commercial Device" in the add-device modal, enter the local IP, hit Detect. The tool probes the device's REST/RPC API, identifies the model and number of relay channels, then registers it in D1. Relay cards appear in the device grid with type badges and direct ON/OFF controls — no ESP firmware needed.
⚡ Direct local control — Commercial device ON/OFF buttons send HTTP requests directly from your browser to the device on the local network. After each command the state is polled and the card updates live. Requires the browser to be on the same LAN as the device.
🔧 Firmware Studio templates + OTA root layer — 15 firmware templates (pill strip UI). All generated firmware includes a FreeRTOS rootTask on Core 1 with OTA update support, recovery AP mode, and automatic rollback if new firmware crashes before initialisation completes.
▸ v3.20 release notes
🔧 Firmware Studio — full in-browser IDE for ESP32 / NodeMCU firmware. Form tab generates a complete, compilable .ino with WiFi, Worker polling, sensor reads, and command execution. CodeMirror 6 editor tab for hand-writing C++. AI tab sends code to Groq 70B for comment improvement. Wokwi simulate button for hardware-free testing.
👁 IoT Panel toggle — "Panel" button in the IoT header hides or shows the control panel without affecting any physical devices, the Cloudflare Worker, or D1. Devices keep running regardless.
🔒 CORS fixX-App-Key header added to Worker CORS allow-list.
▸ v3.19 release notes
🔌 IoT Master Control Panel — Register ESP32/ESP8266 nodes, view live pin states, toggle relays. Command lifecycle: pending → received → executed → confirmed. Cloudflare Worker + D1.
📊 Sensor history charts — Chart.js line charts per sensor pin, last 24 h of readings.
⏱ Daily timers + 🤖 Automation rules — server-side evaluation on heartbeat, no firmware update needed.
🖥 Browser firmware flasher — Web Serial API flash + config write in Chrome/Edge.
▸ v3.18 release notes
⠿ Drag-and-drop dashboard — every dashboard card has a drag handle. Drag panels to swap. Layout persists in localStorage.
🏷 Preset flairs (46 presets) — Species → Flairs now has ⚡ Load preset flairs with 6 categories.
🎨 Theme cycle fixed — green removed; Dark→Light→Paper→Dark. Icons show current theme.
📅 Calendar review dates fixed — bottles next_review and GH next_inspection now shown as orange dots.
▸ v3.17 release notes
🏠 Home section — Dashboard is now its own top-level section (Home tab), separate from Notes. Quick actions, alert strip, stats band, and recent/upcoming panels live here. Notes section simplified to a clean list + empty state.
🌿 Registry & Recipes blank-page fix — navigating to Registry or Recipes now immediately renders the list and shows the empty state, fixing the blank-page bug on first nav.
📅 Calendar sidebar — Calendar section now has a persistent right-side panel showing the next 14 days agenda and current-month stats (lab notes, bottles, journal, contam count). Hidden on mobile.
🧾 Sales supply categories — "New item" line in a sale now shows a rich category dropdown covering plant material, culture media, 50+ chemicals, labware, packaging, and services. Wide product datalist also includes taxonomy + supplies.
🟢 Green theme (4th skin) — theme cycle now has 4 steps: Dark → Light → Paper → Green (radioactive phosphor / lab radiation aesthetic). Green has dot-scan-line texture and neon glow on version badges.
🖨 Invoice v2 — full redesign: header with GSTIN/CIN, Bill-From party box, itemised table with subtotal row, amount-in-words (Indian numbering: Crore/Lakh/Thousand), T&C block, bank details placeholder, signature box, and page footer.
▸ v3.16 release notes
🔗 Cross-section wiring (6 wires) — actions in one section now trigger smart prompts in related sections: Contamination log → offer journal entry · Bottle save with notes → offer recipe save · Sterilise run → offer supply deduction · Sales save → auto-add species to Registry · Reminder ✅ Done → reschedule next review · Sales dispatched → ✓ Received button marks order paid.
✅ Reminder Done button — each rem-card now has a Done button. Click it → enter days until next review → date is updated and reminder rescheduled (works for bottles, notes, and greenhouse plants).
📦 Sterilise supply deduction — after logging a run, tap "Deduct supplies?" to open a pre-filtered checklist of inventory items. Tick + enter qty used → deducts from Supply Inventory immediately.
✓ Received status for sales — dispatched orders now show a green "✓ Received" button. Click it → confirm payment mode → order status updates to Received.
🌿 Sales → Registry auto-add — any species name typed in a sales line item that doesn't exist in the Registry is silently added as a custom entry on save.
▸ v3.15 release notes
🧾 Sales v2 — richer New Sale form with separate phone/email/PO ref fields, payment mode chips (Cash/UPI/Bank Transfer), GST selector, discount (flat/%), shipping. Line items show live stock counts from bottles + GH. Supply items wired in as sellable.
📦 Backward supply wiring — add a line item as "New item…" in an invoice and it auto-creates the entry in Supply Inventory on save. Works for any lab consumable not yet in inventory.
📊 Sales CSV log — every saved order appends rows to sales/sales-log.csv (plain text, one row per line item). Visible and editable in Files → Sales Log.
💾 PDF save — Orders tab now has a 💾 PDF button per order that auto-triggers the browser's print-to-PDF dialog. Invoice layout updated with phone, email, PO ref, payment mode, discount + GST breakdown.
🗂 File browser v2 — multi-format viewer: CSV renders as editable table (add/delete rows), JSON pretty-prints, Markdown renders with side-by-side edit toggle, code files in monospace editor, audio/video native players. Edit + Save works for both encrypted attachments and plain-text files.
📎 Drag & drop upload — drag any file from your OS onto a folder in the file browser left panel. Auto-encrypts and stores under attachments/<section>/general/. Drop hint highlights the target folder.
🧭 Live breadcrumb — the nav bar now shows the current section name (e.g. / Notes, / Bottles, / Calendar) next to the Forest Studio Labs brand. Updates on every navigation.
▸ v3.14 release notes
📈 Usage Analytics tab · 🎚 Per-feature token caps · 🔀 Chat auto-fallback · 🌐 Web Search · 🗜 Context-saving mode · 🔔 Toast inline in nav · 🌿 Forest Studio Labs rebrand · 🍅 Pomo in nav · 🗂 Nav drawer fixes
▸ v3.13 release notes
📊 Terminal call log — Activity tab rebuilt as monospace terminal. Filter Today / Week / All / Errors. Export JSON.
⏰ Alarm tray chip — countdown to next alarm from any section; turns red when firing.
🔔 Session counter chip — live "N calls · X tok" chip in Ravana panel header.
📈 Enriched log entries — in/out tokens, duration, prompt snippet, error codes per call.
🎛 AI Budget Control — master background switch, per-feature toggles, journal cycle scheduler.
▸ v3.12 release notes
🍅 Lab Pomodoro — floating 🍅 FAB, 3 presets (25/5, 50/10, 90/20), circular progress ring, Web Audio chimes, session log in localStorage.
🔥 Contamination-free Streak — dashboard panel: days since last contam, emoji milestones at 7/14/30+ days, confetti burst on milestone.
🌿 Species Spotlight — daily random species card (date-seeded), IUCN badge, native distribution, refreshes each day.
☑ Daily Lab Checklist — 5 default tasks, auto-resets each morning, add/remove items, progress bar, persisted in localStorage.
🌳 Bottle Lineage Tree — recursive SVG tree of passage history per bottle. Bezier connectors, clickable nodes, shown in bottle detail.
📸 Growth Photo Strip — horizontal filmstrip of gallery thumbnails per bottle (sorted by date), shown in bottle detail.
⚗️ Recipe Inline Scaler — open any saved recipe → enter target bottle count → all ingredient quantities scale in real-time.
⏰ Calendar Alarms — set named alarms with time in IST; full-screen fire modal with Web Audio 3-pulse sound; alarms toggle on/off; persisted in localStorage.
🌱 Login animation — plant sprouts from soil as data loads; grows at the speed of the network.
🪴 Size grades revamped — Sapling / S / M / L / XL / XXL (6 tiers, replaces 4). Old values silently migrated via _normalizeGrade().
⬆ Smart GH import — accepts any file type; URL fetch (Google Sheets auto-converted); tab/semicolon/comma auto-detected; size_grade column added.
▸ v3.11 release notes
🌍 GBIF C/E/F complete — Native distribution, taxonomy hierarchy, bulk CP genus import.
📊 Species Performance Ranking — score/100, contam%, multiplication×, avg cycle days.
💰 Lab Cost Tracking — 6-month supply spend + ₹/bottle estimate.
📥 Export Report — 3-CSV + print-ready HTML, voice-triggered.
📷 QR on GH Plants — scannable code, ?gh=id deep link.
🔗 Passage Tracking — parent bottle reference, lineage chips.
📦 Dashboard 10-card stat band — Recipes, Supplies, Contam 30d, Vault ₹, Journal 7d.
▸ v3.9 / v3.10 release notes
🔐 Single-token login — GitHub token IS the encryption key. Migration tool for existing data.
💰 Value Inventory — 💰 Value page: set 8 stage/size prices, view live vault total.
🌍 GBIF A/B/D — Synonym resolver, common names auto-fill, reference photo in species detail.
📊 S.analytics freshness — LabAnalytics.calculate() called after every save (not just login).
🌿 3-way theme — Dark / Light / Paper themes + skins.css (wavy login dots in light mode).
📰 News chips — 18 topic chips replacing the dropdown. CP micro, Nepenthes, orchids, cryopreservation, media, and more. PMC PDF links when available.
▸ v3.8 release notes
🔗 Logic Flows page — complete dependency map of every system: data arch, nav, CRUD paths, AI, known gaps
🧭 navTo() fixed — all external cross-section links now open the drawer correctly
🫙 viewBatch() — "View all X bottles in batch" filters to actual batch members, not all bottles
⏰ Reminder badge — badge now updates after bottle saves, not just at login
13 wiring fixes — reminders, search results, activity log, contamination list, calendar all navigate correctly
▸ v3.7 release notes
🚨 Dashboard Alerts — live alert strip. Flags contamination spikes, overdue reviews, transfer backlogs, unhealthy GH plants, expiring supplies.
☑ Batch Operations — checkbox bottles, action bar slides up: Subcultured, Growing, Rooting, Contaminated, Discard in one tap.
📅 Lab Schedule — overdue reviews, items due this week, bottles past transfer window, GH plants needing attention.
🔍 Advanced Search — Ctrl+K from anywhere. Live across Notes, Bottles, Accessions, Recipes, Greenhouse.
📊 Multiplication Factor — new Analytics card, cultures-out per species. Green ≥4×, blue ≥2×.
📦 Supply Inventory — track media batches, agar, hormones. Expiry tracking, cost per unit, encrypted in GitHub.
🤖 Ravana AI — system prompt rebuilt from Ravana character map. Scholar-king identity, ∿ glyph after significant outputs.
▸ v3.6 release notes
🌍 GBIF Species Lookup — fetch IUCN status, taxonomy hierarchy, and GBIF link from custom species form
📚 PubMed AI Context — top 3 TC abstracts auto-fed to AI when a note has a species set
✂️ Subculture Calendar — set subculture interval per species; auto-reminders in ⏰ Reminders
🖨 Label size selector — 58mm / 62mm thermal or A4 sheet before printing QR labels
📷 GitHub photo storage — species photos stored in repo (photos/taxonomy/)
▸ v3.5 release notes
🧫 Sterilisation Lab — 10 protocol presets, custom builder, per-step timers
🏠 Dashboard rebuild — donut, 14-week heatmap, 3 sections, pinned notes
📝 Two-mode notes — Quick Note (generic, pinnable) + TC Lab Entry
🤖 Claude AI — Powered by Claude (Anthropic), hardwired TC lab rulebook
📊 Heatmap widgets — Lab Activity, Bottle Prep, Contamination (expand 7/14/26 wk)
▸ v3.3 release notes
✏️ Edit custom species — Edit button, form pre-fills, "Update Species" on save
🟣 Cultivar (CV) — new Conservation Status option, purple badge in autocomplete
🏷 Flairs on taxonomy — required flair picker when adding/editing custom species
🧪 Flairs on recipes — tag media recipes with flairs for stage/type labelling
12 GH sources — own cultivar, sister lab, swap, commercial vendor, botanical garden, donated, rescue…
▸ v3.2 release notes
🌱 Taxonomy source field — Seeds / Greenhouse / Other lab / Custom; HYBRID badge
⬆ CSV in right place — import in GH empty state & New Plant editor
📋 CSV format table — always-visible column reference with template
🏡 GH flair in taxonomy — Greenhouse source auto-creates GH flair
▸ v3.1 release notes
📦 Quantity Ledger — running balance stock history
⊞ Bench View — plants grouped by location
🩺 Condition History — health timeline per plant
⚗️ Output Plan — harvest estimates & pipeline
🔔 Inspection reminders — next inspection in ⏰
🏡 GH flair & sources — GH flair auto-created, expanded accession sources
▸ v3.0 release notes
🏡 Greenhouse — stock plant tracking, explant flow, Send to GH
🔥 Autoclave Session — multi-batch autoclave run planning
✨ Glass tint UI — backdrop-blur on login, toasts, nav bar
5 stat cards — Notes / Bottles / Accessions / GH / Reminders
Species panel — GH plants section, stage bars, accession links

🚀 Getting Started Sign in · navigation

Signing In

Enter your GitHub Personal Access Token (with repo scope). The token is both your authentication credential and your AES-256-GCM encryption key — one field, one secret.

Create a token: github.com → Settings → Developer settings → Personal access tokens → Classic → New token → tick repo → Generate

⚠️ Never lose your token. There is no recovery — all data is AES-256-GCM encrypted client-side. The GitHub repo stores only ciphertext.

Login Screen

ElementWhat it does
Token fieldYour GitHub Personal Access Token — authenticates AND encrypts. Never sent to any third party.
☀️ / 🌙 / 📄 Theme (top-right)Toggle Dark / Light / Paper theme before signing in. Preference is saved.
〜 Wave button (bottom-right)Toggle animated dot wave background on login screen (light mode only)
Trust banner (after login)Tap Yes, save to store the token in localStorage — next visit signs in automatically.
⚡ Loaded from cacheIf session cache is fresh (<10 min), data loads instantly without re-fetching from GitHub.

Top Navigation Bar

ButtonAction
☰ MenuOpen/close the navigation drawer
🏠 HomeReturn to the dashboard from anywhere
🌡️ Weather chipCurrent feel-like temp at lab & greenhouse. Click to expand details.
📷 QR ScanOpen camera scanner — auto-routes to bottle or note on match
⬇ ZIPExport all notes as decrypted Markdown + accessions & recipes as JSON
CSVExport notes and accessions as spreadsheet files
🔒 LockWipe token & passphrase from memory, return to login
⚙️ SettingsLab name, AI key, scan options, cache & device management
☀️/🌙 ThemeToggle dark/light mode

Navigation Drawer

Tap to open the drawer. Sections with a left-panel list (Notes, Registry, Recipes, Bottles, Greenhouse) keep the drawer open as a sidebar. All other sections close the drawer and show full-page content.

📋 Lab Notes Editor · shortcuts · wiki-links · draft · camera

Entry Fields

FieldRequiredPurpose
TitleShown in the drawer list and search results
Experiment TypeCategorises the entry; auto-loads a Markdown template into the body when empty
TC StageStage 0–IV, Self Notes, or N/A. Used in analytics, filters, and the stage funnel chart
Species / AccessionLinks to Registry accessions automatically. Supports taxonomy autocomplete + Browse modal
Recipe UsedPick a saved recipe — shows as a purple badge; powers recipe usage analytics
Next Review DateCreates a reminder in ⏰ Reminders with nav badge count
TagsComma-separated. Autocomplete from existing tags. Powers the tag cloud in Analytics
Cultures OutNumber of propagules produced. Used in media efficiency score (Avg Out column in Analytics)
BodyFull Markdown — supports headings, lists, code blocks, tables, images, wiki-links

Editor Modes

ButtonView
✏️ EditTextarea only — full width for writing
⬛ SplitEditor left + live Markdown preview right (stacks on mobile)
👁 PreviewRendered view only — click any wiki-link to navigate

Markdown Toolbar

Buttons wrap selected text or insert at cursor: H2, H3, Bold, Italic, Strikethrough, inline code, code block, blockquote, horizontal rule, bullet list, numbered list, task list, link, image, table, [[]] wiki-link picker, 📷 Camera/Barcode.

ShortcutAction
Ctrl+SSave note to GitHub
Ctrl+BBold selected text
Ctrl+IItalic selected text
Ctrl+KInsert markdown link
TabInsert 2 spaces
Enter in searchDeep full-text search across all note bodies
EscapeClose Quick Log / Wiki picker

Wiki-Links

Type [[Note Title]] in the body to create a clickable link to another note. Use the [[]] toolbar button for a searchable picker. Below the rendered note, a Linked from section shows all notes that reference this one.

Auto-Save Draft

Keystrokes are debounced and saved to sessionStorage after 1.5 s. On re-opening the editor a yellow "Draft restored from Xm ago" banner appears — click Discard to clear it.

Edit History

Click 📜 History on the note view. A panel shows every GitHub commit for that file, newest first. Click ↩ Restore this version to load it into the editor — you must explicitly Save to overwrite.

Quick Log (FAB)

The blue bottom-right FAB opens a minimal modal (Title + Body only). Saves immediately as Self Notes / Brainstorming. Hidden while the full editor is open.

Camera & Barcode Scanner (in notes)

Click 📷 in the toolbar: 📸 Capture inserts a base64 JPEG (encrypted with the note, keep <1 MB). 🔍 Scan QR/Barcode activates the ZXing reader — supports QR, Code128, EAN, DataMatrix. Use ⇄ Switch for front/rear camera.

List Filters

PillShows
AllEvery entry
☣️Contamination Log entries only
Entries with overdue review dates
I / II / III / IVEntries at that TC Stage

🌿 Accession Registry Track plant origins · timeline · species links

Track every plant accession — its origin, status, and full lab history. Any note whose Species field matches the accession species is automatically linked.

Fields

FieldPurpose
SpeciesScientific name — links to taxonomy autocomplete
Accession No.Your internal identifier (e.g. VAN-001, NEP-RAJ-2024)
OriginGeographic or institutional source
Source🏡 Greenhouse · 🌱 Seeds · Wild · Lab-propagated · Other lab / Exchange · Nursery · Purchased · Donated · Custom… (free text). Choosing Greenhouse auto-selects the 🏡 GH flair.
Date acquiredWhen you received or collected it
StatusSee status table below
FlairsColoured provenance labels (clone, form, etc.)
NotesFree markdown — phenotype, permits, contacts

Status Options

StatusMeaning
Active in cultureCurrently propagating in vitro
In acclimatizationStage IV — hardening off ex vitro
DormantPaused, not actively subcultured
Transferred outMoved to another lab or collaborator
LostCulture failed or fully contaminated

Timeline Tab

On any accession detail, switch to the 📅 Timeline tab to see all linked lab notes as a chronological vertical timeline with stage icons — click any title to open that note.

🧪 Media Recipe Book Formulations · scaler · flairs · autoclave params

Store complete, reusable media formulations. Each recipe records enough detail to reproduce the exact medium at any time.

Recipe Fields

FieldDetails
NameFree text — shown in all recipe dropdowns throughout the app
Base MediumCurated list: MS, LS, Knudson C, VW, Pierik, New Dogashima, B5, N6, MS-Musa, WPM, DKW, Anderson's, or Custom
Sucroseg/L
Gelriteg/L — defaults to 1.9 g/L on new recipes (formerly labelled "Agar/Gelrite")
pH (before autoclave)Numeric
Growth RegulatorsFree text — e.g. "BAP 1 mg/L, NAA 0.1 mg/L"
Autoclave paramsPresets: 121°C/15m, 121°C/20m, 121°C/45m, 115°C/20m, Filter sterile — or type manually
Target speciesOptional — creates a clickable link to the matching accession
FlairsOptional coloured labels — tag recipes by stage, type, or workflow (e.g. "Stage II", "Rooting", "High PGR")
Preparation notesFull Markdown — procedure steps, amendments, observations
💡 Filling in "Target species" and linking to a note via "Recipe Used" powers the media efficiency score in Analytics.

Media Volume Scaler

In the recipe detail view, click ⚗️ Scale Media. Enter a target volume (e.g. 500 mL) and all ingredient quantities are recalculated proportionally. One-click copy for the bench.

🫙 Bottle Inventory Single · Batch · Session · lifecycle · QR · passage tracking

Creation Modes

ModeWhen to useCreates
+ SingleOne specific bottle with full detail1 bottle
⊞ BatchN identical bottles, same media, same species and stageN bottles sharing one batch_id
🔥 SessionOne autoclave run with multiple media batches for different species/stagesAll bottles at once, linked by session_id

Bottle Fields

FieldPurpose
SpeciesTaxonomy autocomplete + Browse modal
AccessionLink to the Registry record for this species
RecipeThe media formulation used — links to Recipe Book
StageStage I – IV, Mother Block
StatusSee lifecycle below
Date prepared / inoculatedTimestamps for preparation and inoculation events
Media volumemL per vessel
Explants per bottleOptional integer
Next review dateCreates a reminder in ⏰ Reminders
Linked noteAssociate this bottle with a lab note entry
Source greenhouse plantLink to the GH stock plant the explant came from
Flairs + provenance noteClone/form labels and free text for traceability
NotesMarkdown observations

Bottle Lifecycle Flow

🧫 Prepared 💉 Inoculated 🌱 Growing ✂️ Subcultured 🏡 Send to Greenhouse
🌱 Growing ☣️ Contaminated or 🗑 Discarded

Active bottles (Inoculated + Growing) appear in the dashboard Active Bottles strip and in ⏰ Reminders. Status changes are stamped with the date and shown in bottle detail.

FSL Code

Each bottle gets a unique auto-generated code: FSL-YYYY-NNNN (e.g. FSL-2026-0042). This code appears on the QR label and is searchable in the bottle list.

QR Labels — Size Options

Every bottle detail shows a QR code. Before printing, choose a label size:

SizeUse case
58mmStandard narrow thermal roll (Brother, Dymo, Zebra ZD220)
62mmWider thermal roll — more space for species name
A4 sheetPrint multiple labels on A4 — 2-up inline grid, cut to size

Tap 🖨 Print label — the browser print dialog opens with the correct @page size set automatically. For batches or sessions tap 🖨 Print batch to print all labels at once.

To scan a bottle in the greenhouse or lab, tap 📷 in the nav bar — the app auto-routes to that bottle record.

🔥 Autoclave Session (New)

A Session models one complete autoclave run: you plan all your media batches for that run in one place, set the capacity (default 120 bottles), and save everything at once.

Session FieldPurpose
Session dateDate of the autoclave run
Autoclave capacityTotal bottles your autoclave holds (default 120)
Temp / TimeAutoclave parameters, saved with every bottle in the session
Media batches (rows)Each row: Species, Stage, Recipe, Quantity. Add as many rows as you need.
Capacity barLive bar turns yellow ≥90 %, red if over capacity
Session notesOptional free text — e.g. "First run after equipment service"

On Save, all bottles are created immediately. Each batch within the session gets its own batch_id; all share the session_id. A purple S badge appears in the list for session bottles. The bottle detail shows session date, batch count, and autoclave parameters.

Passage Tracking (v3.11)

In the bottle editor, fill Source bottle (passage) with the parent bottle's code (e.g. FSL-2026-0012). A live hint confirms the match (green = found, red = not found). The saved bottle shows a Passaged from → link to the parent and a Passaged to ↓ chip section listing all child bottles.

🌳 Bottle Lineage Tree (v3.12)

When a bottle has a parent or children, a 🌳 Lineage Tree section appears in the bottle detail. It renders a recursive SVG family tree: the current bottle in blue, ancestors above it, descendants below. Nodes are connected by bezier curves and are clickable to navigate directly to any bottle in the tree.

📸 Growth Photo Strip (v3.12)

If the Photo Gallery contains photos linked to this bottle (matched by bottle code in the photo metadata), a horizontal photo filmstrip appears in the bottle detail — 80×80 px thumbnails, sorted chronologically, scrollable. Click any thumbnail to open the full photo in the Gallery lightbox.

Send to Greenhouse

Every bottle detail shows a 🏡 Send to GH button. Tap it when a culture is subcultured and plants are ready to go back to the greenhouse. You choose how many plants, and the app merges them into an existing greenhouse entry for that species (or creates a new one). A transfer event is logged for traceability.

List Filters

Filter pillShows
AllEvery bottle
PreparedMedia made, not yet inoculated
InoculatedExplants placed
GrowingActive cultures
ContamContaminated vessels

🏡 Greenhouse Stock plants · bidirectional flow · QR · bench view · CSV import

Bidirectional stock plant management — tracks what's in the greenhouse, where it came from, and where it's going. The bridge between field collection and the TC lab.

The Circular Flow

🏡 GH Stock 🌿 Take Explant 🫙 Bottle / 🔥 Session ✂️ Subcultured 🏡 Send to GH 🏡 Stock grows

Plant Entry Fields

FieldPurpose
SpeciesTaxonomy autocomplete + Browse modal. Hybrids (×) fully supported.
QuantityNumber of plants — tracked via the Quantity Ledger tab.
Health statusHealthy 🟢 / Stressed 🟡 / Dormant ⚪ / Declined 🔴 — coloured dot in list + bench view.
Size grade🪴 Sapling · S · M · L · XL · XXL — used by Value Vault to calculate market value per grade. Old values (Seedling/Small/Medium/Adult) are silently remapped on load.
Source12 options: 🧪 Lab output · 🌿 Own cultivar · 🌍 Wild · 🔬 Sister lab/exchange · 🤝 Swap/barter · 🏪 Commercial vendor · 🌱 Nursery · 🏛 Botanical garden · 🎁 Donated · ♻️ Rescue · 🌾 Seeds/spores · ↔️ Trade. Badge shown in list.
Date addedLogged as the first entry in the Quantity Ledger.
Greenhouse locationBench, section, shade house — defines bench view grouping. Defaults to Q5JV+2X Kelambakkam, Tamil Nadu for new plants (linked to map for climate reference).
Next inspection dateAppears in ⏰ Reminders (🏡 icon). Overdue shows red badge in list; due ≤7 days shows yellow.
Linked accessionOptional link to Accession Registry record.
NotesMarkdown — phenotype, provenance, permits.

Plant Detail — 5 Tabs

TabContents
📋 OverviewCore info table, Take Explant buttons, linked bottles list, notes body
📦 LedgerFull stock movement history as a running-balance table. + Adjust opens an inline form to log any change.
🩺 HealthTimestamped condition timeline with coloured dots. + Log opens an inline form (health dropdown + date + observation text).
⚗️ PlanProduction overview: GH stock count, bottles in culture, subcultured-ready count, harvest estimate (bottles / sessions), lab pipeline note.
📷 QR (v3.11)Scannable QR code for this plant (URL: ?gh=id). Print and Copy buttons. Scanner routes directly here when scanned in the field.

📦 Quantity Ledger

Every change to plant count is logged with a row in the ledger: date, event type, delta (+ or −), and a running balance column so you can see the full stock history at a glance.

Event typeWhen used
ReceivedNew plants arrived — wild, nursery, exchange
LossPlants died, damaged, or lost
Sent outSold, gifted, or transferred to another party
Natural propagationGH plant produced new plants on its own
Lab outputTC plants transferred from lab to greenhouse

Entries are auto-created when: the plant is first saved (Initial entry), when you edit the quantity in the editor (Manual edit), and when "Send to GH" is used from a bottle.

🩺 Condition History Log

A timeline of every health state the plant has been in, newest at the top. The current entry is badged green. Each entry has: status (colour-coded), date, and an optional observation note (e.g. "Aphid infestation — treated with neem oil").

Entries are auto-created when you first save a plant and whenever you change the health in the editor. Add manual entries any time via + Log in the Health tab.

⊞ Bench / Layout View

Toggle between list view and bench view using the ≡ / ⊞ buttons in the list panel header.

ViewHow it works
≡ ListStandard sorted list with species, quantity, health dot, inspection badge
⊞ BenchPlants grouped by location into labelled sections with card grid. Plants without a location go under "Unassigned". Due/overdue inspection shown as badge on the card corner.

Both views respect the filter pills (All / Healthy / Stressed / Lab origin) and the search box.

⚗️ Output Planning

The Plan tab computes production capacity from your actual data:

StatSource
GH stockCurrent plant count
In cultureBottles for this species in inoculated or growing status
Ready for GHSubcultured bottles — lab output waiting to transfer back
Avg harvest yieldAverage explants per event from explant history
Projected bottlesStock × avg yield if all plants harvested
Sessions neededProjected bottles ÷ 120 (autoclave capacity)
💡 The more explant events you log, the more accurate the harvest estimate. Fill Cultures Out on your notes to refine the pipeline view.

🔔 Inspection Reminders

Set a Next inspection date on any plant. It then appears in the ⏰ Reminders section with a 🏡 icon, grouped by urgency alongside notes and bottles. The nav badge count includes GH plants due within 7 days. Tapping a GH reminder navigates directly to that plant.

BadgeMeaning
Red OVERDUEInspection date has passed
Yellow INSPECTDue within 7 days
🔔 chip in headerExact days until / past inspection

⬆ CSV / Spreadsheet Mass Import

Click ⬆ CSV in the list panel header to open the import modal. Three ways to get data in:

MethodHow
📂 Upload any fileClick "Upload any file" — accepts CSV, TSV, TXT, or any text format. No file-type restriction.
⬇ Fetch from URLPaste any public CSV link (e.g. Dropbox, raw GitHub). Google Sheets edit URLs are auto-converted to /export?format=csv. Click Fetch.
Paste dataCopy from Excel / Google Sheets (tab-separated) or type CSV directly. Delimiter is auto-detected: comma, tab, or semicolon.

Column reference — column order is flexible, unrecognised columns are ignored. Common aliases (e.g. qty, bench, status) are accepted automatically.

ColumnRequiredNotes
speciesScientific name or hybrid. Aliases: plant, name, scientific_name
quantityInteger, default 1. Alias: qty, count
healthhealthy · stressed · dormant · declined (default: healthy). Alias: status
size_gradesapling · s · m · l · xl · xxl (default: s). Aliases: size, grade
sourcegreenhouse · wild · lab · nursery · trade · own-cultivar · sister-lab · swap · commercial · botanical-garden · donated · rescue · seeds (default: greenhouse)
locationBench label — used for bench view grouping. Alias: bench
next_inspectionYYYY-MM-DD. Alias: inspection_date
accession_numberStored as a reference note; does not auto-link to Registry. Alias: accession
notesFree text. Alias: remarks

The preview table shows every parsed row with a HYBRID badge (detected by ×), colour-coded health, and ✓ / ✕ validity. Invalid rows (missing species) are skipped on import. All imported plants automatically get the 🏡 GH flair and have their condition_log and quantity_log initialised. Click ⬇ Download template for a ready-to-fill sample CSV.

Auto-taxonomy registration: Any species in the CSV not already in your taxonomy is automatically added to Custom Species with source = Greenhouse and the GH flair. They appear immediately in the autocomplete across all sections.

🏡 GH System Flair

A system flair named GH (emoji 🏡, colour green) is automatically created the first time you:

  • Tap Take Explant → Bottle — flair is pre-selected in the new bottle editor
  • Set accession source to Greenhouse — flair pill auto-toggles in the accession editor
  • Import plants via CSV — all imports get the flair assigned

The GH flair uses a stable ID and is never duplicated. It appears everywhere flairs appear: accession detail badges, autocomplete sub-rows, and bottle detail.

Explant → Lab

ButtonWhat happens
🌿 Take Explant → BottlePrompts for quantity + optional note. Logs event to Explant history. Opens Single Bottle editor with species, accession, source plant, and 🏡 GH flair all pre-filled.
🔥 Take Explant → SessionSame logging. Opens Autoclave Session builder with species in first batch row.

Lab → Greenhouse (closing the loop)

Every bottle detail has a 🏡 Send to GH button. On tap:

  1. Choose plant count to transfer
  2. If a GH entry for this species already exists — option to merge (quantity + N)
  3. Otherwise — a new entry is created with source = Lab output
  4. A transfer event (type: lab_output_in) is logged with the bottle reference

Connections Throughout the App

WhereWhat you see
Bottle editor"Source greenhouse plant" dropdown, filtered by species
Bottle detail"Source plant" row — clickable link to the GH plant
⏰ Reminders🏡 GH inspection items alongside notes and bottles
Species detail panel🏡 Greenhouse Plants section with health dots, quantities + "Add plant" shortcut
Dashboard stat card🏡 Greenhouse: entry count / total plant count
Dashboard stripHealthy plants as chips; yellow "lab output ready" alert when subcultured bottles exist

🏠 Home Dashboard 10 stat cards · alerts · active strips · donut · heatmap

Always accessible via the 🏠 Home button. The dashboard is a real-time lab overview rebuilt around biological workflows.

Quick Actions

ButtonAction
+ New EntryOpen full lab note editor
🫙 New BottleOpen bottle editor (Single mode)
🏡 GH PlantOpen greenhouse plant editor
🧫 SteriliseJump to Sterilisation Lab
📷 Scan QROpen QR scanner in route mode
📊 AnalyticsNavigate to analytics

Stat Band (10 cards)

Two-row wrap grid of clickable stat cards: Notes · Bottles (active count) · Accessions · Greenhouse (total plants) · Overdue · Recipes · Supplies (low-stock, yellow when >0) · Contam 30d (red when >0) · Vault ₹ (live value) · Journal 7d. Each card navigates to the relevant section.

Row 1 — Hero Trio

CardWhat it shows
⚗️ TC Stage DistributionSVG donut chart of all bottles by stage. Each segment is clickable — jumps to that filtered bottle list. Legend alongside.
☣️ Contamination Heatmap14-week GitHub-style grid (day × week). Blue cells = lab entries, red gradient = contamination events. Contamination rate % and last-30-day count shown above the grid. Today's cell is outlined in blue.
📷 QR Scan + Transfer WindowLarge QR scan shortcut card. Below it: cultures ≥4 weeks post-inoculation flagged for subculture (blue=4wk, yellow=6wk, red=8wk+). Click any row to open the bottle.

Row 2 — Ops Panel (3 columns)

ColumnContents
📊 7-day ActivityBar chart of notes created per day for the last 7 days
⏰ Reviews DueNotes + bottles due within 14 days, urgency-sorted. Red = overdue, yellow = ≤3 days.
📋 Recent Entries5 most recent notes — click to open

Row 3 — Species in Active Culture

Up to 8 species ranked by combined note + bottle activity. Each shows 📋 note count and 🫙 bottle count. Click any → Species hub.

Row 4 — Media Recipes + Greenhouse

Side by side: last 5 saved recipes (click to open, + New shortcut) and healthy greenhouse stock plants (click to open, with "Lab → GH ready" alert when subcultured bottles exist).

🔥 Contamination-free Streak Panel

Dashboard card showing days elapsed since your last contamination log. Emoji milestones at 7 days (🌱), 14 days (💪), and 30+ days (🏆) with a confetti burst animation. Click to navigate to Contamination.

🌿 Species Spotlight

Daily randomly-selected species card (date-seeded so it's the same species all day). Shows species name, IUCN badge, native distribution, and a brief bio if available in your taxonomy. Changes each day.

☑ Daily Lab Checklist

5 default tasks (check autoclave pressure gauge, review bottle statuses, note contamination observations, check humidity/temperature, review upcoming subcultures). Auto-resets at midnight. Add custom tasks or remove defaults. Progress bar at the top. Data persisted in localStorage (key: tcplants_checklist).

🍅 Lab Pomodoro (floating FAB)

A 🍅 button floats in the bottom-right corner on all pages. Click to open the Pomodoro panel (slides in from the right). Three presets: 25/5 min, 50/10 min, 90/20 min. Shows a circular SVG progress ring. Web Audio API chimes when a work or break period ends. Session log stored in localStorage (key: tcplants_pomo_sessions).

🌱 Login Plant Sprouting Animation

While data loads after sign-in, a plant animation grows from soil in real time — growth speed reflects actual network load progress. Each data file loaded advances the animation.

🌱 Species in Culture Species table · detail panel · taxonomy packs · GBIF · flairs

Species Table

Click 🌱 Species in the nav drawer. A live table lists every species found in your notes, accessions, and bottles.

ColumnMeaning
SpeciesScientific name (bold if taxonomy pack has data)
NotesTotal lab notes for this species
Active bottlesInoculated + growing bottles
Stage spreadMini stage pills showing active work distribution
Contam rate% of notes that are contamination logs
Avg outAverage "Cultures Out" across notes — proxy for multiplication rate

Species Detail Panel

Click any species row to open the detail panel showing:

SectionContents
SummaryNotes, active bottles, contam rate, avg cultures out
TC Stage ActivityHorizontal bar chart across Stage I–IV
🏡 Greenhouse PlantsAll GH entries for this species with health dots, quantity, and source. "+ Add plant" shortcut.
Accession RecordsRegistry entries — click to navigate
BottlesUp to 8 bottles with status dots — click to open. "View all" link.
NotesMost recent 10 lab notes — click to open

Taxonomy Packs

Switch to the Taxonomy Packs tab to toggle built-in species packs: Carnivorous Plants (Nepenthes, Drosera, Sarracenia…), Orchids, Banana & Relatives. Disabled packs are excluded from autocomplete on next login.

Custom Species (+ Add custom species)

At the bottom of the Taxonomy Packs tab, add any species not in the built-in packs — including unregistered hybrids (type × in the name, e.g. Nepenthes × (rajah × lowii)). A HYBRID badge appears automatically on the entry.

FieldPurpose
Scientific NameRequired. Any format — species, hybrid name, working name. Names with × are auto-detected as hybrids.
Common NameOptional display name
Source / Origin12 options: 🏡 Greenhouse · 🌿 Own cultivar · 🌱 Seeds/spores · 🌍 Wild · 🔬 Sister lab/exchange · 🤝 Swap/barter · 🏪 Commercial vendor · 🌱 Nursery · 🏛 Botanical garden · 🎁 Donated · ♻️ Rescue · Custom. Choosing Greenhouse auto-creates the 🏡 GH flair.
Conservation StatusLC · NT · VU · EN · CR · DD · CV (Cultivar) — shows as a coloured badge in autocomplete. CV appears purple.
FlairsRequired — select at least one flair to label this species (provenance, clone ID, form, etc.). Flairs must be defined first in the Flairs tab.
Subculture IntervalDays between subcultures for this species (e.g. 28 = monthly). Auto-generates ✂️ subculture reminders in ⏰ Reminders when active bottles approach that age.
PhotoOptional — compressed to max 480 px JPEG (~30–50 KB). Stored as a binary file in photos/taxonomy/ in your repo (keeps the taxonomy JSON small). Thumbnail in species list; click for lightbox.
🌍 GBIF LookupAfter entering the scientific name, click Fetch conservation data ↗. Queries GBIF Species Match API (no key needed). Auto-fills Conservation Status from IUCN Red List, shows kingdom/phylum/family and direct GBIF link.

Once saved, the species appears immediately in the autocomplete dropdown for all Species fields across the app (notes, bottles, accessions, greenhouse).

Each row in the Custom Species list has an ✏️ Edit button (opens form pre-filled, button changes to "Update Species") and ✕ Delete. Editing does not affect existing notes or bottles that already use that name.

Flairs

Define coloured labels in the Flairs tab. The 🏡 GH flair is auto-created by the system. Flairs are assignable to accessions, bottles, and media recipes. They appear as pills in lists and detail views.

⚡ Preset flairs — click "Load preset flairs" in the Flairs tab to browse 46 curated presets across 6 categories and bulk-add with checkboxes. Already-existing flairs are greyed out automatically.

CategoryColourExamples
ProvenanceBlue🌍 Wild collected · 🏔 Highland ecotype · 🤝 Exchange material
Conservation statusRed🔴 CR · 🟠 EN · 🟡 VU · ⚠️ CITES · 🏛 Type specimen
Geography / EcotypeGreen🌏 Borneo · Western Ghats · Sri Lanka endemic · Himalayan form
Culture performancePurple⭐ Elite clone · ⚡ Fast multiplier · ✅ Certified clean
Plant traitYellow🌸 Flowering selected · 🍃 Variegated · 💪 Vigorous growth
Media / Recipe tagOrange🧪 MS-based · ✂️ Multiplication medium · 🌱 Rooting medium

Reminders Notes · bottles · GH plants · urgency groups · nav badge

Notes, active bottles, and greenhouse plants with a review / inspection date appear here, grouped by urgency. The nav badge shows combined overdue + this-week count across all three types.

GroupConditionColour
🔴 OverdueDate has passedRed
🟡 This weekDue within 7 daysYellow
📅 This monthDue within 30 days
🗓 LaterMore than 30 days away
IconTypeOn tap
📋Lab noteOpens the note
🫙Bottle (active only)Opens the bottle detail
🏡Greenhouse plant inspectionNavigates to Greenhouse → opens that plant

☣️ Contamination Event log · most affected species · trend chart

All notes of type Contamination Log are aggregated here. Shows a summary (total events, most affected species, most common stage) and a chronological list of every contamination event. Click any entry to open the full note.

💡 Use the Contamination Trend chart in Analytics to spot monthly spikes. If contamination suddenly rises, check laminar flow maintenance, media pH, and explant surface sterilisation protocol.

📊 Lab Analytics 12 charts · species ranking · lab cost · export CSV/print

All metrics are computed in-browser from your already-decrypted data. Nothing is sent anywhere. Click 📥 Export CSV or 🖨 Print Report in the Analytics header to export.

Chart / CardWhat it shows
Notes / MonthLine chart — entries created per calendar month (last 6 months)
Stage DistributionDonut — proportion of notes at each TC stage (I–IV, Contam, Unclassified)
Species Success RateHorizontal bars — % of non-contamination notes per species (≥2 notes required). Green >70%, yellow 40–70%, red <40%
Contamination TrendLine chart — contamination entries per month (last 6 months)
Recipe UsageBars — how many notes reference each recipe (top 6)
Tag Cloud30 most-used tags as sized pills — larger = more frequent
Stage Funnel by SpeciesStacked bars for top 5 species — Stage I → II → III → IV distribution
Avg Days Between EntriesBadge cards per species (≥3 notes) — approximate subculture interval
Media EfficiencyRanked table: Recipe × Species × Stage. Score = (Avg Out / global avg) × (1 − contam rate) × 100
Culture Lifecycle StatusActive bottles grouped by progress vs expected duration: On track / Due soon / Transfer due / Delayed
Species Performance Ranking (v3.11)Score/100, contam%, multiplication×, avg cycle days, active bottles — ranked table for all species ≥2 notes
Lab Cost Tracking (v3.11)6-month supply spend bar chart + estimated ₹ cost per bottle
Feature UsageBars — section visit frequency since tracking began (reset in Settings)
💡 Always fill Cultures Out on Multiplication and Rooting notes to get meaningful data in Media Efficiency and Species Performance Ranking.

📅 Calendar Month grid · day panel · create from date · alarms · export

Monthly view of all lab activity. Navigate with ‹ › or Today.

Calendar Dots

Dot colourMeaning
BlueLab entry created on this day
YellowReview due on this day
RedContamination entry on this day

Day Panel

Click any day → a panel lists all entries with type, stage, and species. Click any entry to open it. On mobile the panel is full-screen.

Creating Notes from the Calendar

Click a day → + New note on this date (past) or + Schedule note for this date (future). Past dates set the experiment date; future dates pre-fill the Next Review Date.

⏰ Alarms (v3.12)

Click the ⏰ Alarms button in the calendar header to open the alarm panel. Alarms fire at the exact minute in IST, play a 3-pulse Web Audio sound, and show a full-screen modal.

ActionHow
Add alarmEnter a time (HH:MM) and optional label → click + Add. Alarm appears in the list immediately.
Toggle on/offClick the toggle switch on any alarm card. Off alarms are greyed out and will not fire.
Delete alarmClick ✕ on the alarm card.
DismissWhen the alarm fires, a full-screen modal shows the time and label. Click Dismiss to close it.

Alarms check every 10 seconds. Each alarm fires at most once per minute (deduplication prevents re-firing within the same minute). All alarm state is persisted in localStorage (key: tcplants_alarms).

💡 Alarms only fire while the app tab is open. They use IST (UTC+5:30). If you switch to another tab, the alarm will fire as soon as you come back within the same minute window.

Exporting

Click ⬇ Export → This week / This month. A print-ready window opens with all entries expanded — note bodies, badges, and contamination/review flags. Save as PDF via the browser print dialog.

Lab Tools Timers · stopwatches · media scaler · biochem calculator · tray

ToolHow to use
Countdown Timers+ Add Timer → enter h/m/s + name → ▶ Start. Up to 5 simultaneous. Beeps on zero. ↺ Reset.
StopwatchesUp to 4 simultaneous. ⏱ Lap records a split without stopping. ↺ resets.
🧫 Sterilisation StepperNow a dedicated page — click Open Sterilisation Lab → from Tools, or tap 🧫 Sterilise in the nav. See section below.
Media Volume ScalerEnter base volume + target volume + ingredients (one per line: "name: amount unit"). Click Scale → proportional quantities shown immediately.
BioChemistry CalculatorMolarity (MW/mass/volume), Dilution (C₁V₁=C₂V₂), % Solution (w/v), pH from [H⁺] or pOH, PGR converter (mg/L ↔ μM using molecular weight)

▂ Minimise to Tray

Each widget (Countdown Timers, Stopwatches, Sterilisation Stepper) has a ▂ minimise button in its header. Clicking it collapses the card and places a chip in a fixed footer bar visible on all sections — so timers and steppers keep running while you navigate to notes, bottles, or anywhere else.

Footer chip actionResult
Click chip labelNavigate to Tools and restore the widget card
Click ✕ on chipClose chip, restore card to Tools (timer keeps its state)
Timer chipShows live countdown or 🔔 when done
Stopwatch chipShows live elapsed time while running
Stepper chipShows current step number / total

The Stepper card in Tools is now a shortcut button only — the full stepper UI lives in 🧫 Sterilise but minimises to the same tray.

🧫 Sterilisation Lab 10 protocol presets · stepper timer · custom builder · minimise to tray

A dedicated page for surface sterilisation work — accessible via 🧫 Sterilise in the nav drawer or the shortcut in Lab Tools. The stepper timer can be minimised to the footer bar and run in parallel with any other section.

Protocol Library (left panel)

10 biologically accurate preset protocols, grouped by explant category:

CategoryProtocols
Your Work✏️ Custom Protocol — always the first entry; clears all steps so you can build from scratch and save your own
Standard Material🌿 Shoot Tips / Meristems · 🍃 Leaf Disk / Leaf Explants · 🪴 Nodal Segments (Axillary Buds)
Seeds & Spores🌱 Seeds (General Angiosperm) · 🌸 Orchid Seeds (Asymbiotic Germination — microcentrifuge method)
Specialised🪲 Nepenthes / Carnivorous Plants (gentle: 5% NaOCl, anti-oxidant rinse, PVP note)
Challenging🌍 Field Collected / Heavily Contaminated (fungicide pre-soak) · 🥔 Bulb / Corm / Rhizome · 🌡️ Thermotherapy (50–55°C water bath, viral elimination)
In Vitro🔬 LAF-only Transfer (UV + flame + cool-down — no bleach needed)

Each preset shows its gentle, standard, robust, or field intensity tag, step count, and estimated total time.

Protocol Tips Panel

When a preset is loaded, a blue tips box appears below the stepper explaining the biological rationale — why this concentration, what to watch for, what to do next.

Saved Protocols

Click 💾 Save to save the current step list under a custom name. Saved protocols appear below the library and can be loaded or deleted. Stored in localStorage on this device.

Stepper Controls

ControlAction
+ StepAdd a blank step to the current protocol
▶ Start ProtocolBegin the timed walkthrough — each step counts down, beeps on completion, auto-advances
⏸ Pause / ▶ ResumePause the active step timer
↺ ResetReturn to step 1, clear active state
▂ MinimiseCollapse to footer tray — stepper keeps running

Step Fields

FieldPurpose
Step nameDisplayed as the active step label during the run
Duration (seconds)Countdown timer for this step
Temperature (°C)Optional bench reference — shown during the step
📝 NoteBench notes shown during the active step — technique reminders, cautions

📷 QR Scan Route mode · bottle/note/GH routing · scan enhancement

Tap 📷 in the top nav bar from any screen — opens the scanner in route mode.

Scanned contentResult
Bottle label QR (?bottle=ID)Automatically navigates to that bottle's detail view
Note QR (?note=ID)Automatically opens that note
Any other QR or barcodeShows decoded text with Copy and Open URL options

The same scanner opens from the 📷 toolbar button inside the note editor for inserting barcodes inline.

Low-Light Tips

Enable Scan Enhancement in ⚙️ Settings. When active, contrast and brightness sliders appear during scanning — drag to boost image clarity. Enable Try Harder Mode for a slower but more thorough decoder pass.

AI & Voice — Ravana 3 models · voice commands · wake word · journal parse · PubMed

Lab Footer Bar

Fixed bottom-right bar visible after login — all AI and voice controls in one place:

ButtonAction
🎤Voice input — tap to start, tap again to stop. Pulses red while listening, yellow while processing.
Model badgeTap to cycle: ⚡ Quick → 🔬 Deep → ◇ Claude
Open / close AI chat panel
Quick log (fast note)

AI Models

ModelUse for
⚡ Quick — Llama 3.1 8BVoice chat, simple questions. Default for all voice input. Free, fast.
🔬 Deep — Llama 3.3 70BComplex protocols, multi-species analysis, detailed troubleshooting.
◇ ClaudeHardest reasoning, undescribed species, nuanced protocol design.

All keys encrypted with your passphrase and stored in GitHub — enter once, auto-loads on any device.

AI Panel Tabs

TabShows
💬 ChatText conversation, quick-action chips, send field
🎤 VoiceAll voice session transcripts with timestamps — accumulates even when panel is closed

Voice Input Modes

ModeHow it works
Native (Chrome)Browser STT → reads transcript back → confirm by saying "send"/"re-listen" or tap buttons
Whisper (Groq)Records audio → silence auto-detects stop → Groq Whisper transcribes → auto-sends. Better accuracy for species names and scientific terms.

Voice Commands (no AI call, instant)

SayDoes
"home" / "dashboard"Navigate to dashboard
"notes" / "bottles" / "greenhouse" / "recipes" / "registry" / "reminders" / "analytics"Navigate to that section
"settings"Open settings panel
"open scanner" / "scan QR"Open QR scanner
"sidebar" / "menu"Open navigation drawer
"read protocol 2" / "read recipe 3"TTS reads that numbered recipe aloud
"what's overdue"TTS reads overdue notes and bottles
"how many bottles"TTS reads bottle count and active count
"latest note" / "read last note"TTS reads most recent note
"switch to deep" / "switch to Claude" / "switch to quick"Change AI model
"whisper mode" / "native mode"Switch STT mode
"stop reading" / "stop"Stop TTS mid-sentence
"close"Close AI panel
"add N bottles of [species]" (v3.11)Opens Batch mode, prefills species + quantity, starts creation
"open bottle FSL-XXXX" (v3.11)Finds bottle by code, opens detail view
"show species [name]" / "open species [name]" (v3.11)Navigates to Species section, opens that species detail panel
"export report" / "download report" (v3.11)Downloads 3-CSV lab report immediately

Wake Word

"Hey Lab" — say it anywhere in the app to open the AI panel and start a voice session (Native mode only). Enable in Settings → Voice & Lab Mode. A small pulsing indicator appears when armed.

Ravana — who he is

The AI is named Ravana after the scholar-king of Lanka: master of the Vedas, sciences, and the veena, and the most devoted of Shiva's devotees. Not the villain of a northern myth — the scholar his own people knew. He responds with (a veena wave) after completing a significant output. In voice, he may use classical Tamil naturally. He operates only within the lab and immediately related workflow — one refusal for anything else, then done.

Context & Rulebook

Every AI message includes your open note, linked recipe, accession, and species summary. Ravana pulls from actual lab data first and clearly distinguishes between "your records show X" and "general TC practice suggests X". Cites mg/L and µM, uses literature ranges only, never fabricates protocols.

PubMed Auto-Context

When a species note is open, the 3 most recent PubMed abstracts for that species + "tissue culture" are injected automatically.

Voice Settings

Settings → Voice & Lab Mode: STT mode selector, readback confirmation, voice commands, wake word toggle, TTS voice/speed/pitch, noise suppression, echo cancellation, auto-gain control, silence threshold for Whisper auto-stop.

📰 TC Research News 18 topic chips · PubMed · PMC PDF links · pin articles · archive

Click 📰 News in the nav drawer. Tap a topic chip, then toggle the switch to load up to 60 publications from PubMed. Changing the chip resets the toggle so you can reload with the new query.

Topic Chips (18 + Custom)

ChipPubMed query covers
🌿 TC & MicropropagationPlant tissue culture, micropropagation, in vitro propagation, plant cell culture
🪲 Carnivorous Plants MicroCP + TC/micropropagation terms
🪴 Carnivorous PlantsNepenthes, Drosera, Sarracenia, Dionaea, Pinguicula, Heliamphora (broad)
🏔 NepenthesNepenthes specifically + TC terms
🌸 OrchidsPaphiopedilum, Dendrobium, Phalaenopsis, Vanilla, orchid + TC terms
🧬 Plant MutationSomaclonal variation, mutagenesis, in vitro mutation
🍌 Banana / MusaMusa, banana, plantain + TC terms
🌼 VanillaVanilla planifolia / Vanilla spp. + in vitro
🔬 Somatic EmbryogenesisSomatic embryogenesis + plant
🌱 OrganogenesisOrganogenesis, shoot organogenesis, plant regeneration
🧫 Callus CultureCallus induction, callus culture, dedifferentiation
🌿 RootingIn vitro rooting, auxin rooting, root induction
💉 PGRsPlant growth regulators, cytokinin, auxin, BAP, NAA, TDZ + TC
🧼 SterilisationSurface sterilisation, sodium hypochlorite, explant sterilisation
☣️ ContaminationFungal/bacterial contamination in tissue culture
⚗️ MediaMurashige Skoog, culture media, macro/micro nutrients
❄️ CryopreservationPlant cryopreservation, vitrification, slow cooling
✏️ CustomType any PubMed query — free-form search

Each article card shows: PMC PDF link (when available), DOI link, abstract toggle, and 📌 pin button.

Card Grid

Results appear in a responsive 2-column card grid. Each card shows: title (DOI link), authors, journal, year, and a collapsible full abstract (Read more ↓ toggle). DOI and PubMed ↗ links open in a new tab.

Pinning Articles

Click 📌 on any card to pin it. Pinned articles float to the top in a dedicated Pinned section with a yellow border, and stay pinned across page reloads (stored in localStorage). Click 📌 again to unpin.

Archive

Each load is automatically saved to your repo at news/YYYY-MM-DD.json — a permanent dated research archive. If a file for today already exists, it is not overwritten.

Activity Log Last 30 GitHub commits · click to jump to note

The ⚡ Activity tab (right edge) shows the last 30 GitHub commits on your lab repo — a real-time record of every save to the cloud.

IconCommit type
🌱New note or Quick Log entry
✏️Note updated
🗑Note deleted
🌿Accession registry saved
🧪Recipe saved
🫙Bottle inventory updated
🏡Greenhouse data saved
📋Note index updated
App code updated

Click any 🌱 or ✏️ commit to jump directly to that note. refreshes. Click , the Activity tab again, or anywhere outside the panel to close it.

📦 Lab Stock & Inventory Reagents · low-stock alerts · expiry · category tabs

Click 📦 Stock in the nav drawer. Track all lab reagents, glassware, and consumables with low-stock alerts and expiry date monitoring. Data is encrypted in your GitHub repo at inventory/data.enc.

Item Fields

FieldPurpose
NameReagent or item name (e.g. "Murashige & Skoog salts 10L kit")
CategoryMedia Salts · Growth Regulators · Gelling Agents · Sugars & Vitamins · Sterilisation · Glassware & Plastics · Equipment · Other
Quantity + UnitCurrent stock with unit (g, mL, pcs, pack, L, etc.)
Low Stock Alert ≤When quantity falls to or below this threshold, the item shows red in the list and appears in the Summary warning. Set 0 to disable.
LocationWhere it's stored: Fridge, -20°C freezer, Shelf B3, etc.
Expiry DateShown in red when within 30 days. Useful for hormones, enzymes, antibiotics.
NotesSupplier name, catalogue number, lot number, preparation notes

Stock Adjustments

Click ✏️ Edit on any item → +/− Adjust stock. Enter a positive number (received new stock) or negative (consumed). The current quantity updates immediately.

Low-Stock Summary

The left panel shows total item count and number of low-stock items. When anything is low, a red warning lists the item names. Sort by category using the left category filter.

💡 Check stock before every media prep session. Link item names to your recipe notes so you know which reagents each formulation needs.

⚙️ Settings Theme · AI keys · voice · scan · cache · device trust

Click ⚙️ in the top bar. All changes persist in localStorage.

SettingEffect
ThemeDark / Light mode toggle
Lab NameShown in the dashboard header
Weather LocationCity name for the weather widget
AI KeysGroq (free default) + Claude (optional). Both encrypted in GitHub — enter once, auto-loads on any device.
Scan EnhancementAdds contrast/brightness sliders in camera scan mode
Try Harder ModeSlower but more thorough QR/barcode decoder
Clear Local CacheForces a full reload from GitHub on next login
Forget Trusted DeviceRemoves the encrypted saved token from this device
Reset Usage StatsClears section-visit counts shown in Analytics

📓 Lab Journal Free-form writing · voice dictation · Ravana parse · overnight batch

Nav drawer → Lab Journal. A freeform encrypted scratchpad — write in plain language, ideas, observations, protocol thoughts. No required fields. Ravana reads it and extracts structured data.

Writing

Tap ✏️ New Entry. Write freely in the big text area — Markdown is supported. Give it a title and tags if you want, or just write. The 🎤 mic button dictates into the entry.

Mic modeHow it works
NativeWords appear live as you speak — final transcript appended to entry
WhisperRecords audio → transcribes via Cloudflare proxy → appended to entry

Ravana Parse

Tap ✨ Parse with Ravana at the bottom of any entry. Ravana reads it and returns action cards:

FoundAction cardWhat it does
A media recipe🧪 Create Recipe →Opens recipe form pre-filled with name, base, PGRs, pH, autoclave, target
A species name🌿 Add to Taxonomy →Opens custom species form pre-filled
An observation📋 Create Lab Note →Opens note editor pre-filled with title, type, species, stage, body

Once parsed, the entry is marked (green) in the list. Editing the entry resets it to unparsed so it can be re-parsed after changes.

Overnight Parser

Unparsed entries (● grey) are automatically parsed in the background:

  • At 1am IST if the app tab is open
  • On first login each day — 8 seconds after unlock, any unparsed entries are batch-parsed silently
  • Runs once per day maximum — a toast confirms how many were parsed
  • After batch parse: lab-summary.json is updated

Journal Log in Ravana panel

Open the Ravana AI panel → tap 📓 Journal Log tab. Shows all parsed entries with their summaries and extracted data. Each extract has a Use → button to pre-fill the relevant form, and a ↺ Re-parse button to flag the entry for re-processing if Ravana made a mistake.

🔬 Culture Lifecycle Tracking Progress bar per bottle · expected duration · status colours

Every active bottle now shows how far it is through its expected stage duration, and the Analytics section shows a lab-wide lifecycle health summary.

On each bottle detail

Scroll down below the spec table to see the Culture Lifecycle strip:

  • Stage path icons (🧪 → 💉 → 🌿 → ✂️ → 🌱 → ☀️ → 🪴) with current stage highlighted
  • Progress bar: days elapsed vs expected stage duration
  • Status badge showing whether the culture is on track, due soon, or overdue
  • Total days in lab since preparation

Expected duration — how it's calculated

SourcePriority
Historical bottles of same species (avg days inoculated → subcultured)Highest — requires ≥2 completed bottles
Taxonomy species subculture_interval_days settingMedium
Stage defaults (inoculated: 14d, growing/subcultured: 28d, rooting: 21d…)Fallback

Status colours

StatusCondition
🟢 On trackLess than 70% of expected duration elapsed
🟡 Due soon70–100% of expected duration
⏰ Transfer due100–150% of expected
🔴 DelayedMore than 150% of expected — needs attention

Analytics — Culture Lifecycle Status

Analytics section → Culture Lifecycle Status card. All active bottles grouped by status with counts and clickable rows that navigate directly to the bottle.

🚨 Dashboard Alerts 5 alert conditions · auto-dismiss · nav to fix

A strip of coloured pills appears above the stat band whenever the lab needs attention. It renders on every dashboard load and disappears automatically when the condition clears — no manual dismiss needed.

AlertConditionGoes to
☣️ Contamination spike (red)Any species with ≥3 notes in last 30 days and >10% contamination rateContam section
⏰ Overdue reviews (yellow)Any active bottle or note with a past-due next_review dateReminders
✂️ Transfer due (yellow)≥5 active bottles past 28 days since inoculationBottles
🏡 GH attention (yellow)Any greenhouse plant with health ≠ healthyGreenhouse
📦 Supply expiry (red if expired)Any supply batch expired or expiring within 14 daysSupplies

Batch Operations Checkbox select · bulk status update · one save

Select multiple bottles and update all of them in a single save — useful after an autoclave run or an end-of-day subculture pass.

How to select

ActionResult
Tap checkbox on a bottle rowToggles that bottle in/out of selection
Select all button (above the list)Selects all currently visible bottles (respects active filter and search)
Tap Select all again when all are selectedDeselects all

Batch actions

Once ≥1 bottle is selected, an action bar slides up from the bottom of the screen:

ButtonSets status to
✂️ Subculturedsubcultured — also records today as last_transfer date
🌿 Growinggrowing
🌱 Rootingrooting
☣️ Contaminatedcontaminated
🗑 Discarddiscarded
✕ ClearDeselects all, hides bar

All changes are saved to GitHub in a single write after confirmation.

📅 Lab Schedule Overdue · this week · transfer due · GH attention

Nav drawer → Schedule. A single view showing everything that needs action, sorted by urgency.

SectionShows
⏰ OverdueBottles and notes where next_review date has passed. Days overdue shown in red. Sorted: most overdue first.
📅 Due this weekBottles and notes with next_review within 7 days. Sorted by days remaining.
✂️ Transfer dueActive bottles that have been inoculated for ≥28 days. Shows days since inoculation.
🏡 GH attentionGreenhouse plants with any health status other than 'healthy'.

Every row is clickable — tapping it navigates directly to that bottle, note, or plant.

Stat cards at the top give a count per category at a glance.

Press Ctrl+K from anywhere in the app, or go to nav drawer → Search. Type anything — the engine searches all 8 data sources simultaneously with relevance ranking and term highlighting.

Sources and fields indexed

SourceFields indexedNotes
📋 NotesTitle, species, tags, body preview, keyword stemsBody keyword index extracted at save time (250 words) — full content searchable without API calls
🫙 BottlesCode (FSL-YYYY-NNNN), species, stage, status, notes, flair noteStatus: growing/inoculated/contaminated etc.
🌿 AccessionsSpecies, accession number, source, origin, notes
🧪 RecipesName, base medium, target species, PGR text, prep notesPGR concentrations searchable
🏡 GreenhouseSpecies, source, location, health, notes
📓 JournalTitle, body, tagsNew in v3.11
📦 SuppliesName, unit, notesNew in v3.11
🌱 SpeciesScientific name, common name, category, sourceCustom taxonomy — new in v3.11

How matching works

FeatureHow to use
Multi-word ANDAll words must appear somewhere — "rajah growing" finds Nepenthes rajah bottles with growing status
Relevance rankingTitle match = 3pts, species = 2pts, tags = 2pts, body = 1pt — best matches appear first in each section
Term highlightingMatched words highlighted in yellow across all result rows
Field prefixesClick the prefix chips or type them: species: stage: status: code: tag: health: location:
Recent searchesLast 10 queries shown when the search box is empty
Saved filtersClick 💾 Save to name and pin a search — appears as a chip at the top. Up to 20 saved.
💡 Example queries: Nepenthes rajah growing · species:Drosera stage:II · status:contaminated FSL-2026 · BAP 1 mg (finds recipes with that PGR) · tag:transfer location:bench3

Results are grouped by section with colour-coded headers. Tap any result to navigate directly to that record. Min 2 characters to trigger search.

📦 Supply Inventory Media batches · expiry · cost/unit · TC perf correlation

Nav drawer → Supplies. Track media batches, reagents, gelling agents, hormones, and consumables. Data is stored encrypted in GitHub (supplies/data.enc) — synced across devices, version-controlled like all other lab data.

Adding a batch

Tap + Add batch. Fill in name, unit (bottles / L / g / etc.), quantity prepared, total cost (₹), preparation date, and expiry date. Cost per unit is calculated automatically.

Tracking use

ActionDoes
+ Use 1Increments used count by 1 and saves
Edit useSet the used count to any number (for bulk corrections)
✕ (top right)Delete the batch — prompts for confirmation

Status indicators

ColourMeaning
Green>50% remaining, not expiring soon
Yellow<50% remaining, or expiring within 14 days
RedDepleted or expired

Expiry warnings also appear in the Dashboard Alert strip before they become critical.

📥 Export & Backup Lab report CSV/print · ZIP · calendar export · GitHub history

Lab Report Export (v3.11)

ModeContents
📥 Export CSV (from Analytics)3 downloads: bottles.csv (code, species, stage, status, dates, recipe, parent), notes.csv (date, title, type, stage, species, cultures out, tags), greenhouse.csv (species, location, qty, health, size_grade)
🖨 Print Report (from Analytics)Styled HTML with stat band + active bottles table + GH plants table — opens in new window, auto-prints
Voice: "export report"Triggers CSV export immediately

Other exports

ExportContents
⬇ ZIP (top bar)All notes as decrypted Markdown + accessions + recipes as JSON
CSV (top bar)Note metadata CSV + accessions CSV
🖨 Print / PDFOpen any note or recipe → browser print dialog → Save as PDF
📅 Calendar exportThis week / This month → print-ready HTML with all entries

Your data is always in the GitHub repo as encrypted .enc files — every save is a git commit, giving you complete version history at github.com/Phyto-Evolution/tcplants.

💰 Value Vault Stage & size prices · live vault total · INR format

Nav drawer → 💰 Value. Estimates your current lab inventory value from bottles + greenhouse stock using configurable prices.

Prices Tab

Set base prices (₹) for 12 categories: bottles by stage (I, II, III, IV, Weaned, Rooting) and GH plants by size grade (🪴 Sapling, S, M, L, XL, XXL). Prices auto-save with an 800ms debounce after you stop typing.

Vault Tab

Live calculated total: Σ(bottles by stage × stage price) + Σ(GH plants × grade price × quantity). Full breakdown by category with species-level detail.

SettingWhere
Size grade (Sapling / S / M / L / XL / XXL)GH plant editor → Size Grade field. Old values (Seedling/Small/Medium/Adult) are remapped automatically.
Dashboard stat cardShows compact total (₹12k / ₹1.2L format), navigates to Value page
💡 Set the 8 base prices once in the Prices tab — the Vault auto-updates as you add or remove bottles and plants. Species-level overrides are planned for a future update.

🌡️ Weather Widget Open-Meteo · lab + greenhouse · 60s refresh · expandable popover

The weather chip in the nav bar shows current real-feel temperature at the lab. Data is fetched from Open-Meteo every 60 seconds — no API key needed.

LocationMetrics
🏛️ Lab — SARE, ThiruporurReal feel °C (large) · dry °C (small) · 💧 Humidity % · ☀️ UV Index · 🌧️ Rain probability %
🌿 Greenhouse — Thaiyur, KelambakkamSame metrics

Click the chip to expand the two-location popover. Click anywhere outside to close.

🔒 Privacy & Security AES-256-GCM · PBKDF2 600k · GitHub ciphertext only

WhatHow it works
EncryptionAES-256-GCM. Key derived via PBKDF2 (SHA-256, 600,000 iterations). Unique salt per save.
Where data livesOnly in your GitHub repo as .enc ciphertext. Even repo admins cannot read it without your passphrase.
PassphraseIn-memory only. Never sent to any server. Lost on lock or tab close.
Login CAPTCHAInline math gate (X + Y = ?) shown before Sign In. No external service. Regenerates on wrong answer and on every lock. Blocks automated access to the login screen.
GitHub tokensessionStorage only (cleared on tab close) unless Trusted Device is enabled — then stored in localStorage as token encrypted with your passphrase.
Session cacheDecrypted data cached in sessionStorage for 10 minutes for fast re-unlock. Cleared on lock and tab close.
Trusted DeviceToken encrypted with your passphrase and stored in localStorage. Remove via Settings → Forget this device.
⚠️ If you lose your passphrase, all data is permanently unrecoverable. There is no password reset. Keep a secure backup of your passphrase.
📓
Lab Journal
Write freely. Ravana reads it and extracts recipes, species, and lab notes from your words.
📅
Lab Schedule
Overdue · Due Soon · Transfers · GH Attention
📦
Supply Inventory
Batches · Stock Levels · Performance · Cost
💰
Value Inventory
In-vitro vault · Greenhouse vault · Total value

📋 Updates & Version Log

Loading commits…

🔗 Logic Flows

v3.52

System-wide dependency map. Search by function name, concept, or section. Expand any block to test it.

🔍 Search Logic Flows — function names, concepts, sections
🗺 Master System Flow — TC Plants Lab Architecture
👤 Shiva (user)
GitHub PAT
token = auth + key
login()
validate · migrate · cache check
Promise.all(8 loads)
notes · bottles · accs · recs · GH · supplies · journal · value
S{state}
global in-memory
↓ decrypted data flows into all sections
📋 Notes
🫙 Bottles
🌿 Registry
🧪 Recipes
🏡 Greenhouse
📓 Journal
📦 Supplies
💰 Value
↓ cross-cutting systems read from S and write back via GitHub API
🤖 Ravana AI
🔍 Search
📊 Analytics
🏠 Dashboard
🌍 GBIF
📥 Export
📅 Calendar
⏰ Reminders
↓ all writes encrypt via AES-256-GCM → ghPut() → GitHub commits
notes/index.enc + {id}.enc
bottles/data.enc
accessions/data.enc
recipes/data.enc
greenhouse/data.enc
supplies/data.enc
journal/entries.enc
value/data.enc
taxonomy/custom.enc
📦 Data 🔑 Login 🧭 Nav 📋 Notes 🫙 Bottles 🏡 GH 🏠 Dash 🤖 AI 📊 Analytics 🌍 GBIF 💰 Value 📥 Export ⏰ Alarms 🔗 Wires ⚠️ Gaps
📦
Data Architecture
State object S · GitHub enc/dec cycle · session cache · 16 fields tracked
S.* ghGet/ghPut AES-256-GCM 16 state fields

📦 Data Architecture

State Object S — all fields

FieldLoaded byRead byGitHub file
S.notes[]login → loadNotesIndex()renderNoteList, openNote, LabAnalytics, _buildAIContext, Reminders, Dashboard, Searchnotes/index.enc + notes/{id}.enc
S.accs[]login → loadAccessions()renderAccList, openAccession, openBottle, Searchaccessions/data.enc
S.recs[]login → loadRecipes()renderRecList, openRecipe, bottle form, Searchrecipes/data.enc
S.bottles[] + S.sessions[]login → loadBottles()renderBottleList, openBottle, Dashboard, Reminders, Analytics, Species, Searchbottles/data.enc
S.greenhouse[] + S.ghTransfers[]login → loadGreenhouse()renderGHList, openGHPlant, Dashboard, Reminders, Searchgreenhouse/data.enc
S.supplies[]login → loadSupplies()SupplyInventory.renderInventoryPage, calcPerformance, Dashboard low-stocksupplies/data.enc
S.journal[]login → loadJournal()renderJournalList, overnight parser, _buildAIContextjournal/entries.enc
S.valuelogin → loadValue() (8th load)renderValueSection, vault calculationvalue/data.enc
S.taxSpecies[]post-login loadTaxonomy()_taxSuggest, species picker, _buildAIContext, Species sectiontaxonomy/*.json packs (read-only)
S.taxCustomloadTaxonomy() → ghGetSpecies custom tab, _taxSuggest, flairs, GBIF importstaxonomy/custom.enc
S.analyticsLabAnalytics.calculate() at login + after saves_buildAIContext only (renderAnalytics recalculates inline)— (memory only)
S.weatherfetchWeather() at login, every 60sweather-chip display, _buildAIContext— (Open-Meteo API)
S_inv (separate object)loadInv() on Stock section visit (not at login)renderInv, saveInvinventory/data.enc
S.cur / S.editing / S.curBot / S.curGHopenNote, openBottle, openGHPlantdetail renderers, save actions (edit vs create branch)— (UI state)
S.bottleFilter / S.bottleSearch / S.botBatchFiltersetBotFilter, viewBatch, search inputrenderBottleList (priority: batchFilter → statusFilter → search)— (UI state)
S.sectionswitchSection()navTo, switchSection, mobile sidebar checks— (UI state)

Encryption cycle

JS object JSON.stringify enc() AES-256-GCM + PBKDF2(600k) ghPut() btoa → GitHub API PUT new sha stored in S.*Sha
ghGet() GitHub API GET ghDec() atob dec() crypto.subtle.decrypt JSON.parse → S.*

Session cache (10-min TTL)

_writeCache() saves notes + accs + recs + bottles + greenhouse + taxCustom to sessionStorage._lc. Cache restored on next login if <10 minutes old — skips 7 API calls. Supplies, journal, and value are NOT cached — re-fetched every session.

🔑
Login / Unlock Flow
Single-token auth · 8 parallel loads · device trust · migration tool
login() _readCache() _migrateEncryption() 8 data loads

🔑 Login / Unlock Flow

Login sequence — single path, 8 stages
PAT token
auth + encrypt key
validate()
HTTPS check · repo access
_readCache()
10-min TTL
HIT
restore 5 stores
⚡ instant
MISS
Promise.all( loadNotesIndex · loadAccessions · loadRecipes · loadBottles · loadGreenhouse · loadSupplies · loadJournal · loadValue )
_writeCache()
Hide #login → show #app
renderAll()
loadTaxonomy()
LabAnalytics.calculate()
fetchWeather()
_doWriteSummary()
1Read GitHub token → S.tok = S.pw = tok. If DEVICE_KEY in localStorage → restore token.
2Validate HTTPS (required for crypto.subtle). GET /repos/Phyto-Evolution/tcplants → verify access.
3Migration check: if dec() throws OperationError → show #lmigrate panel → _migrateEncryption(oldPw) re-encrypts all .enc files.
4Cache hit → restore from sessionStorage (skip 8 API calls). Cache miss → Promise.all 8 loads:
loadNotesIndex() · loadAccessions() · loadRecipes() · loadBottles() · loadGreenhouse() · loadSupplies() · loadJournal() · loadValue()
5Hide #login → show #app → render all lists → show UI chrome (FAB, AI btn, footer bar, actlog)
6loadTaxonomy() · loadEncryptedKeys() · fetchActivityLog() · device trust banner check
7LabAnalytics.calculate()S.analytics · _scheduleOvernightJournalParse() (8s delay) · fetchWeather() + 60s interval
8setTimeout(_doWriteSummary, 5000) → commits lab-summary.json

Device trust

ActionResult
Trust Yesenc(S.tok)localStorage.DEVICE_KEY — next visit restores token automatically
Trust NoBanner dismissed — token must be re-entered next visit
Forget devicelocalStorage.removeItem(DEVICE_KEY) — token field shown on login again
🧭
Navigation System
navTo() · switchSection() · drawer vs full-page · URL routing
navTo() switchSection() viewBatch()

🧭 Navigation System

Section typenavTo() behaviourExamples
List sectionsOpens nav drawer + switches to nd-{sec} panel. Drawer stays open as sidebar.notes, registry, recipes, bottles, greenhouse
Full-page sectionsCloses drawer, hides all nd panels, triggers section render function. schedule + search converted to fullpage class v3.29.reminders, calendar, contam, analytics, species, schedule, journal, supply, value, search, logicflows, changelog
stockRedirected → switchSection('supply'). Active highlight fixed (v3.14): ndtab toggle also matches data-sec="stock" when name is 'supply'.

Section render triggers (via switchSection)

SectionRender call
remindersrenderReminders()
contamrenderContamDashboard()
analyticsrenderAnalytics()
speciesrenderSpecies()
calendarrenderCalendar()
news_renderPersistedPins() + handleNewsToggle()
steriliseloadCustomSterilProtocol() or renderSterilPage()
valuerenderValueSection()
bottles, greenhouserenderBottleList() + showBotEmpty() / renderGHList()
v3.52: Math CAPTCHA gate — State: _captchaA, _captchaB (random 1-9), _captchaOk (bool). _newCaptcha(): generates new X+Y question, resets _captchaOk=false, clears answer input + border colour + verified label. Called at page load and after every lock(). _checkCaptcha(): called on oninput of #captcha-ans; if val===_captchaA+_captchaB → sets _captchaOk=true, border green, shows ✓ Verified; else if input length ≥ 2 → border red, schedules _newCaptcha() after 400ms. login(): bails early with "Please solve the math check first." if !_captchaOk. HTML: #captcha-box div above #ltok field — inline flex row with question label, number input #captcha-ans, and #captcha-ok span (hidden until verified). lock(): calls _newCaptcha() to reset gate on logout.
v3.49: Real token tracking + token pill + per-message badge — Worker fix: cloudflare-worker.js now extracts inTok/outTok from each provider response (Groq: d.usage.prompt_tokens/d.usage.completion_tokens; Claude: d.usage.input_tokens/d.usage.output_tokens; Gemini: d.usageMetadata.promptTokenCount/d.usageMetadata.candidatesTokenCount) and includes them in the response JSON. _callAIProxyFull(): return now passes through inTok:d.inTok||0, outTok:d.outTok||0. Token pill: #ai-tok-pill span added to AI panel header after #ai-mode-btn. _updateTokPill(): reads _aiSessionTok and AI_MODELS[_aiModel].tpmLimit (groq8/70: 6000, claude/gemini: null). Groq: shows N.Nk / 6k coloured green/yellow/red at 65%/90% of limit. Claude/Gemini: shows plain Ntok. Called from _aiUpdateSessionChip() after every call. Hidden when _aiSessionCalls===0. Per-message badge: .msg-tok-badge ↑inTok ↓outTok appended to each AI reply div when token data is available; hover title shows full breakdown. CSS: .msg-tok-badge (9px, muted, bg sf2, border, opacity .7).
v3.48: Thin/Robust mode toggle + Token efficiency — Thin/Robust toggle: _ravanaMode (localStorage: tcplants_ravana_mode, default 'thin'). _toggleRavanaMode(): cycles thin↔robust, calls _updateRavanaModeUI() to style the #ai-mode-btn button in AI panel header (purple highlight in robust). _buildRobustContext(query, onStep): 6-layer async builder — Layer 1: lab vitals (active/contaminated bottles, GH plants, low-stock supplies, _computeAlerts()); Layer 2: per-species breakdown (notes, active bottles, stages, contam rate); Layer 3: contamination history last 30d; Layer 4: supply inventory (low-stock, expiring 30d); Layer 5: schedule & overdue reminders (overdue notes+bottles, due-in-7d); Layer 6: currently open entry (content fetch, linked recipe) + user profile + PubMed if triggered. Each layer calls onStep(idx, 'start'|'done', labelOrSummary). In askAI(), robust mode replaces the thinking bubble with .rav-progress DOM (braille spinner ⣾⣽⣻⢿⡿⣟⣯⣷ animates via setInterval per step, cleared on step done; step turns green ✓ on completion). After all steps: bubble resets to "🔬 Sending…" and prompt is sent. Follow-up turns (both modes) use rolling history only — robust deep-scan only on first turn. Token efficiency (v3.48): rulebook compacted 6,673→3,339 chars; 90s context cache (_aiCtxCache, invalidated via _aiCtxMarkDirty() after saves); PubMed gated to _PUBMED_TRIGGERS regex; Groq models: useTools:false removes ~4,700t per request; _callAIExtract() lightweight path (no rulebook/context/tools); fallback chain unified — hidden 8B fallback removed from _callAIProxyFull; Gemini updated to gemini-2.5-flash. CSS: .rav-progress, .rav-step, .rav-step.done, .rav-spin.
v3.47: Workflow Gap Fixes — Plan→Execution bridge: _planWeeklyHTML() now includes a "📋 Plan Actions" section above the 7-day schedule. For each active production plan, calls _planCalcReq(plan) (required bottles per stage), _planActualCounts(plan.species) (live bottle counts), and _planCalcTimeline(plan) (date windows). Shows amber action cards when actual[stage] < required[stage] AND the stage is current or starting within 14 days; sorted urgency-first. Mother plant explant: _mothTakeExplant(id) — increments m.propagation_count, sets m.last_inspection=today, calls _mothSaveData(), toasts confirmation; prompts user to navigate to new bottle form with species pre-filled. "✂️ Take Explant" button added to each .moth-card (stopPropagation so card click still opens form). GH stock deduction on delivery: _dispatchDeliver(id) now iterates sale.items[] by species after saving the sale, decrements matching S.greenhouse[].quantity values (distributed across plants of that species, floored at 0), then calls saveGreenhouse() async and toasts "GH stock updated".
v3.46: Lab Orchestration Engine — _labOrchestrate(event, payload): central async dispatch called after lab actions. Events: 'subculture_done' payload {newBots[], recipe_id} — (1) sets next_review on each new bottle = date_prepared + STAGE_EXPECTED_DAYS['inoculated'] days; (2) if recipe has ingredients[], deducts usage from S.supplies[].used proportionally (qty_per_batch / batch_size × qty), saves supplies; (3) re-saves bottles with review patch; (4) calls updateReminderBadge(). 'status_changed' payload {bottleId, newStatus} — sets next_review = today + STAGE_EXPECTED_DAYS[newStatus] on the bottle, re-saves, calls updateReminderBadge(). Wired into: _subcultureConfirm() (after saveBottles), updateBottleStatus() (after save, before render). Batch Lifecycle Panel: renderBatchPanel() — groups S.bottles by batch_id (non-null, non-terminal); sorts overdue (pct≥100) first then oldest date_prepared; per-card: species, stage icons, bottle count, _calcBottleLifecycle() progress bar (green→blue→yellow/overdue), days/expected, next review, output value via _valCalcBottle(), "View →" calls viewBatch(batch_id)+toggleBatchPanel(). toggleBatchPanel(): toggles #bot-batch-panel visibility, calls renderBatchPanel() on open. Entry: "📦 Batches" button in bottles aside header (between ☑ All and 🏷 Labels). CSS: .batch-lc-card(.overdue), .batch-lc-bar, .batch-lc-fill, .batch-lc-meta.
v3.45: Supply Auto-Reorder Sheet — _supNeedsReorder(item, today): returns {low, expired, remaining, pct} — low if remaining ≤ min_stock (when set) or remaining/quantity ≤ 0.20 (fallback); expired if item.expiry ≤ today. _supReorderSheet(): filters S.supplies for items where low or expired; groups by item.supplier || 'Other / No supplier'; renders inline panel into #supply-reorder-panel with per-supplier table (columns: Item, In stock/Total, Reorder at, Order qty editable input, Unit price, Est. cost); unit price = cost_total/quantity; suggested qty defaults to original batch quantity; live est-cost update via input event listener on each #ror-qty-{id}; grand total per group + across all groups; panel toggles on repeated click. _supReorderCopy(): reads #ror-qty-{id} inputs for current quantities, formats structured plain text grouped by supplier with status labels, estimates, grand total; copies to clipboard via navigator.clipboard.writeText(). _supReorderPrint(): reads same inputs; opens window.open() with full HTML page containing per-supplier tables, styled for print (button hidden via @media print). Button in supply action bar shows yellow count badge when items need reorder; #supply-reorder-panel div added to supply page HTML after the grid.
v3.44: Accession Photo Gallery — switchAccTab('photos'): shows #av-photos-pane, hides details/timeline panes, calls _accPhotosRender(S.curAcc). New tab button #acc-tab-photos added to .acc-tabs. _accPhotosRender(accId): if !_galLoaded, shows "Load gallery" button (calls loadGallery then re-renders); otherwise filters _galItems where item.species_id.toLowerCase() === acc.species.toLowerCase() OR item.bottle_id is in the set of codes of bottles with bottle.accession_id === accId; renders .gal-thumb grid (same CSS as main gallery); clicking a thumb: navTo('gallery'); loadGallery().then(()=>_galOpenLightbox(id)); updates #acc-tab-photos label with photo count. _accGalleryAdd(species): calls navTo('gallery'), loadGallery(), then _galStartUpload(), then after 300ms sets #gal-species select to matching species option. openAccession() extended: before switchAccTab('details'), if _galLoaded && _galItems.length, computes photo count and updates tab label — so count is shown immediately when gallery is already in memory. No new fields on gallery items; existing photos link automatically via species_id match.
v3.43: Recipe Auto-Cost from Supply — _recipeIngCost(recipe): for each ingredient in recipe.ingredients[], finds matching supply by supply_id, computes unit_price = supply.cost_total/supply.quantity, multiplies by qty_per_batch; returns {total_batch_cost, cost_per_bottle, breakdown[]} or null if no matched supplies. Module-level state: _recIngredients[] (current form rows: {supply_id, supply_name, qty_per_batch, unit}), _recCostMode ('auto'|'manual'). _recIngListHTML(ings): renders ingredient rows as .rec-ing-row grid (supply autocomplete via datalist, qty input, unit label, × remove). _recIngAdd()/_recIngRemove(i): mutate _recIngredients and re-render. _recIngSupplyChanged(i,val): matches supply by name, sets supply_id + unit on the row. _recIngQtyChanged(i,val): updates qty. _recIngPreview(): computes live cost, shows/hides #re-ing-preview, toggles auto badge + mode button, disables cost input in auto mode. _recToggleCostMode(): flips _recCostMode, calls _recIngPreview. _recIngUpdateSupplyList(): rebuilds #re-ing-supply-list datalist from S.supplies. renderRecipeIngredientDetail(r): renders .rec-ing-detail-table in recipe detail view with per-ingredient breakdown + totals row + auto/manual badge. saveRecipe(): if cost_mode='auto', calls _recipeIngCost and writes result to media_cost_per_bottle before saving; stores ingredients[] and cost_mode fields. startEditRecipe(): populates _recIngredients from r.ingredients, sets _recCostMode, re-renders list + preview. startNewRecipe(): clears _recIngredients + _recCostMode. openRecipe(): appends renderRecipeIngredientDetail(r) to rv-body. Supply card: live "🧪 Used in: Recipe A, B" backlink computed from S.recs. CSS: .rec-ing-row, .rec-ing-unit, .rec-ing-preview (.warn variant), .rec-ing-detail-table.
v3.42: Vault View Toggle — renderValueSection() extended: computes totalCurrentVal (sum of qty × _valGetPrice('stage_'+b.status, b.species) per active bottle), renders 5th header card "Current inventory" alongside projected output / cost / margin / GH vault. _renderCurrentInventory(): new function; groups active bottles by species sorted by current value desc; per-bottle row: code (clickable → openBottle(id)), status, qty, unit price, current value; yellow "— set price" for unitPrice=0; species subtotals; grand total; footer note linking to Prices tab. _renderValueVaultPanel() rewritten: in-vitro section now has a two-button toggle (📊 Current inventory / 🔮 Projected output); #val-vault-current (default visible, contains _renderCurrentInventory()) and #val-vault-projected (default hidden, contains existing per-bottle chain table); bottle codes in projected table now clickable. _valVaultView(view): toggles display + button active state for the two inner panels; called by toggle buttons. Ravana get_value_inventory updated: computes totalCurrentVal + bySpCurr per species; returns current_inventory_total_inr and current_value_inr per species in by_invitro_species[].
v3.41: Contam Scan — openContamScan(): sets _contamScanMode=true, _contamScanBottleId=null, opens cam-modal, starts ZXing via toggleBarcodeMode(). _handleContamQRResult(text): matches ?bottle=id in QR URL, finds bottle in S.bottles, sets _contamScanBottleId, updates #cam-barcode-result with bottle info + ratio/contam_loss from _valCalcBottle(). capturePhoto(): if _contamScanMode, calls _contamCapture() instead. _contamCapture(): draws video to canvas, calls _galAnalyzeContam(canvas) (from gallery section), shows score + colour, renders confirm button if bottle identified. _contamConfirm(bottleId, score): sets bottle.status='contaminated', saves via saveBottles(), then calls saveNotesIndex() to push a Contamination Log note with bottle_id=bottleId; calls renderBottleList()+renderContamDashboard(). updateBottleStatus('contaminated') extended: after saveBottles, also calls saveNotesIndex() to create Contamination Log note with bottle_id — manual stepper path creates the log too. openBottle(id) extended: renders "☣️ Contamination Events" section from S.notes.filter(n=>n.bottle_id===id && n.type==='Contamination Log'). _traceQRScan(): lightweight one-shot ZXing in cam-modal (no contam mode), decodes bottle QR → populates #contam-trace-input → calls renderContamBatchTrace(). closeCamera() extended: resets _contamScanMode, _contamScanBottleId, restores capture button text. Contam HTML: "📷 Scan Bottle" → openContamScan(); Gallery Camera + Upload Photo preserved as secondary paths. Batch trace: 📷 button → _traceQRScan(). CSS: .cs-bottle-bar (found/contam/clean/unknown variants), .cs-score-bar, .cs-score-fill.
v3.40: Subculture Wizard — _subcultureOpen(parentId): renders wizard into #bot-detail; reads parent bottle + _valCalcBottle() to show ratio/expected offspring; form fields: qty, date, stage, media_volume, recipe_id, operator_id, notes, mark-parent-subcultured checkbox. _subcultureConfirm(parentId): reads wizard form, generates N bottles each with parent_id=parentId, shared batch_id=uid(), inherited species/accession_id, status='inoculated'; optionally sets parent status='subcultured'; calls saveBottles() then _subcultureResult(). _subcultureResult(newBots, parentId): shows offspring chips (click to open), Print batch labels button, Back to parent link, Subculture again shortcut; auto-calls printBatchLabels(batch_id). QR scan routing: _subcultureMode flag (let); openSubcultureScan(): sets flag, opens camera + toggleBarcodeMode; _handleNavQRResult() extended: if _subcultureMode, matches bottle QR → clears flag + calls navTo('bottles') + _subcultureOpen(id). Entry: "✂️ Subculture" button in openBottle() header for non-terminal bottles (not subcultured/contaminated/discarded). CSS: .sub-wiz-header, .sub-wiz-parent-bar, .sub-wiz-parent-meta, .sub-wiz-parent-code, .sub-wiz-parent-sub, .sub-wiz-body, .sub-wiz-field, .sub-wiz-row, .sub-wiz-actions, .sub-wiz-result-header, .sub-wiz-offspring-grid.
v3.39: Live Value Chain — _valNextStage(status): maps bottle status to next price key (prepared→inoculated→growing→subcultured→rooting→hardening→weaned→null). _planRatioForBottle(bottle): maps bottle.stage to plan stage I/II/III/IV, finds active plan matching bottle.species, returns plan.params[stage].mult_factor. _valCalcBottle(bottle): full per-bottle calculation — ratio cascade (bottle.multiplication_ratio → recipe.multiplication_ratio → plan mult_factor), media_cost from recipe.media_cost_per_bottle, labour_cost = (recipe.estimated_hours/recipe.batch_size) × operator.hourly_rate, mother_cost = mother.market_value/mother.propagation_count, contam_loss = offspring bottles with parent_id===bottle.id and status='contaminated', surviving = qty×ratio−contam_loss, output_value = surviving × _valGetPrice(nextStage, species), margin = output_value−production_cost. Returns {production_cost, media_cost, labour_cost, mother_cost, output_value, margin, ratio, ratio_source, surviving, contam_loss}. Finance: _finOps() normalises operators array (string→{name,hourly_rate:0}); operators now stored as [{name,hourly_rate}]; _finOpAdd()/_finOpRemove(i)_ manage table rows. Recipe new fields: multiplication_ratio, media_cost_per_bottle, estimated_hours, batch_size. Bottle new fields: multiplication_ratio (override, placeholder from recipe), operator_id. Mother plant new field: market_value. _botRecipeChanged(recId): updates ratio placeholder when recipe selected. renderValueSection(): header now shows projected_output / production_cost / margin / GH vault. _renderValueVaultPanel(byGHSp, spGHValue): in-vitro per-bottle chain table (species group header, rows: code, recipe, status, qty, ratio+source badge, contam_loss, output units+₹, prod cost, margin); GH still species×grade flat table. Dashboard _vaultINR: uses _valCalcBottle().output_value per bottle (falls back to flat price if no ratio). Ravana get_value_inventory: returns projected_output_total_inr, production_cost_total_inr, projected_margin_inr, bottles_without_ratio, by_invitro_species[] with output/cost/margin per species.
v3.38: Species-aware Value Inventory — _valGetPrice(key, species) helper: checks S.value.species_prices[species][key] first, falls back to S.value.prices[key], then 0. _valSpeciesPriceSet(species, key, val): debounced (800ms) save of per-species price overrides into S.value.species_prices. renderValueSection() rewritten: builds bySpStatus[sp][status] + byGHSp[sp][grade] maps, computes per-species vault value using _valGetPrice(). _renderValueVaultPanel(bySpStatus, byGHSp, spInvitroValue, spGHValue): species×stage grid table (rows=species sorted by value desc, cols=only stages with ≥1 bottle, "custom ₹" badge on overridden cells, totals row). _renderValuePricesPanel(prices) extended: species picker (#val-sp-picker) + _valRenderSpeciesOverride(species) renders per-stage+grade inputs with overridden/global badges. Dashboard _vaultINR fixed: now calls _valGetPrice('stage_'+b.status, b.species) per bottle. Ravana get_value_inventory fixed: removed hardcoded STAGE_VALS, uses _valGetPrice(), returns by_invitro_species[] + by_gh_species[] + has_species_custom_prices. CLAUDE.md RULE 7 added: every financial/value/analytics feature must be species-aware; use _valGetPrice() for all price lookups.
v3.37: Wire Pass + 7 modules — Wire Pass: _buildCalMap() now emits purple planning-stage dots and teal mother-inspection dots; _computeAlerts() adds planning stage-behind (crit) and mother plant overdue-inspection (warn) alerts; _dispatchDeliver() auto-updates linked bottle_ids[] to status='dispatched' via saveBottles(); askAI() captures last response into _ravLastResponse. New: _waSendDispatch(id) (wa.me URL, reads customer phone, pre-fills order details); _printPassport(id) (async QR gen via QRCode.toDataURL(), opens printable phytosanitary certificate window); _ravVoiceRead() (calls _ttsSpeak(_ravLastResponse), 🔊 button in AI panel head); _contamShowView(view) + _contamForensicsHtml() (species×stage heatmap, suspects list, Ravana root-cause chip); _planWeeklyHTML() (7-day bottle+note review lookahead); Mother Plant Registry: MOTH_F, _mothLoaded, _mothLoad(), _mothSaveData(), _mothRenderList(), _mothOpenForm(id), _mothSaveForm(id), _mothDelete(id). New Planning tabs: 📋 Weekly, 🌿 Mothers. New Ravana tool: get_mothers (loads mothers, filters by species). AI_TOOLS now 54 tools.
v3.36: The Awesome Update — Lab Timer system: _TIMER_PRESETS[], _labTimers[], _timerTogglePanel(), _timerStart(name,secs,icon,color), _timerTick() (interval tick: decrements, fires browser Notification + toast at 0), _timerRender(), _timerDelete(id), _timerCustom() (prompt-based input: 5m/45s/2:30 formats), _timerUpdateBadge(). Nav bar ⏱ button with red count badge. Bottle QR Labels: _botLabelsOpen() (modal with checkboxes of visible bottles), _botLabelsPrint() (async QRCode.toDataURL per bottle, opens printable window with 70mm×38mm label grid). IoT env strip: in _renderDashboard(), reads _iotState.devices, renders #dash-iot-strip with per-device online dot + readings. 3 Ravana IoT tools: get_iot_status, control_device (confirm-required, calls _iotCommercialCmd or _iotSendCmd), get_active_timers. AI_TOOLS_CONFIRM now 16.
v3.35: Commercial Suite Session 2 — Sales section now 6 tabs (added Analytics). New functions: _salesAnalyticsHtml() (6th tab: revenue bar chart + top customers + top species), _salesCalcAnalytics() (aggregates monthly revenue, custMap, spMap from S.sales). _saleLinkBottles(saleId) — saves checked bottle checkboxes to sale.bottle_ids[] via saveSales. _saleOpenDetail enhanced with 📄 Invoice button (calls existing _printInvoice) and bottle picker (checkboxes filtered by order species). _dispatchDeliver now shows Finance toast when order has total (Finance reads S.sales automatically via _finCalcMonth; field bug fixed: s.total||s.order_total). 4 Ravana CRM tools added to AI_TOOLS[]: get_customers, get_orders, create_order, update_order_stage — last 2 in AI_TOOLS_CONFIRM.
v3.34: Commercial Operations Suite — S.customers[] in sales/customers.enc (CUST_F). loadCustomers/saveCustomers loaded at login in Promise.all. Sales section expanded to 5 tabs: Pipeline (Kanban), Customers (CRM), New Sale, Dispatch, All Orders. Stage lifecycle: enquiry→confirmed→propagating→ready→dispatched→delivered. Key functions: _salesPipelineHtml, _salesCustomersHtml, _salesDispatchHtml, _saleAdvanceStage(id,stage), _saleOpenDetail(id), _custSave(id), _custDelete(id), _dispatchSale(id), _dispatchDeliver(id), _salePickCust(sel). Sale objects gain stage, customer_id, courier, tracking_number, date_dispatched, date_delivered fields.
v3.33: Ravana 45-tool suite — AI_TOOLS[] expanded from 15 → 45. New read: get_notes, get_greenhouse, get_supplies, get_reminders, get_accessions, get_journal, get_contam_history, get_finance_summary, get_production_plans, get_value_inventory, get_analytics. Intelligence: analyze_contamination_pattern, predict_weekly_actions, compare_species, identify_at_risk, get_daily_briefing, get_media_prep, project_output. Navigate: navigate_to, open_bottle, open_species. Write (confirm gate): add_reminder, add_journal_entry, batch_update_bottles, subcultural_transfer, add_supply_item, create_accession, add_greenhouse_plant, create_production_plan, mark_review_done. All dispatch through _executeTool() switch.
v3.32: Production Planning Engine — renderPlanning() entry, _planCalcReq(plan) back-calculates bottle requirements per stage, _planCalcTimeline(plan) computes stage date windows from target_date, _planCurrentStage(plan) returns current active stage, _planActualCounts(species) reads live bottle counts, _planGanttSVG(plan) renders inline SVG timeline. Data in planning/data.enc. Forms: _planFormHTML(id), _planSaveForm(id), _planEditForm(id), _planArchive(id). Tabs: plans / schedule / status / new via _planTab_(tab).
v3.31: Financial Engine — renderFinance() entry point, _finCalcMonth(y,m) computes revenue/labour/supply/overhead/P&L/cost-per-plantlet. Settings in finance/settings.enc via _finLoad()/_finSave(). Operators list also stored here for LIMS (v3.33).
v3.30: .page-head migration complete — Reminders, Contam, Analytics, Tools, Species, Sterilise, and News sections converted from inline h2/p headers to .page-head component. Sterilise grid changed to minmax(260px,320px) 1fr for narrower-screen compatibility.
v3.29: .page-head CSS added — Supply, Sales, Files, AI, Schedule, Value, Search sections now show styled section headers. Gallery auto-loads on navTo('gallery') via loadGallery().
✓ Canonical cross-section pattern: navTo('bottles'); openBottle(id)navTo opens the drawer first, then the detail function populates the main area. Calling openBottle(id) alone without navTo breaks because the section may not be active.

Nav drawer tabs (v3.14)

Tabdata-secNotes
📅 CalendarcalendarAdded v3.14 — was missing from drawer
📋 ChangelogchangelogAdded v3.14 — was missing from drawer
📦 StockstockRedirects to supply; active highlight fixed v3.14

Special navigation

viewBatch(batchId): sets S.botBatchFilter = batchId, clears other filters → openNavDrawer(); renderBottleList() — list shows only that batch.

_handleNavQRResult(): scans URL params on load — ?bottle=id → open bottle · ?note=id → open note · ?gh=id → open GH plant (v3.11)

📋
Notes — Full CRUD Flow
4 ops · draft · wiki-links · history · keyword stems
saveNoteAction()openNote()saveNotesIndex()

📋 Notes — Full CRUD Flow

CREATE — new note
startNewNote()
generic · tc
S.editing = null
editor shown · draft listener active
saveNoteAction()
read form · validate · disable btn
writeNote(id, body)
ghPut notes/{id}.enc
saveNotesIndex(ns.push(…))
✓ Saved
_writeCache · renderNoteList · toast
EDIT / UPDATE — existing note
editNote()
S.editing = S.cur · populate form
saveNoteAction()
S.editing set
ghGet(DIR/{id}.enc)
read current sha
writeNote(id, body, sha)
ghPut with existing sha
saveNotesIndex(patchFn)
update index entry
✓ Updated
saveNotesIndex() — serialised write lock
🔒 _notesSaveLock
promise chain · prevents racing saves
ghGet(IDX)
always reads fresh SHA
patchFn(notes[])
caller transforms the array
ghPut(IDX, enc, sha)
returns {sha, notes}
READ — open + render list
openNote(id)
S.cur = id
ghGet(DIR/{id}.enc)
cached in S.bodyCache
sanitizeMarkdown(_wikiPreprocess())
→ #nv-body.innerHTML
badges rendered
species · stage · tags · bottles · recipe · backlinks
DELETE · Draft · Wiki-links · History · Camera
OpFlow
DeletedeleteNote() → confirm → ghDel({id}.enc) → saveNotesIndex(remove) → renderNoteList
Draft_wireDraftListeners() oninput → debounced sessionStorage · restore banner on next startNewNote · _clearDraft() on save
Wiki-links_wikiPreprocess() finds [[title]]<span class="wikilink"> · click → openNote · renderBacklinks(id) shows reverse links
HistoryfetchNoteHistory(id) → GitHub commits API → list with sha/date → click → ghGet(sha) → dec → show modal → restore
Camera / QRopenCamera() → getUserMedia → ZXing BrowserQRCodeReader → decode → insertBarcodeResult → appended to ne-body
Pane toggle_setPaneMode(btn) — called via onclick on each pane button; sets #editor-body-wrap[data-pane] attribute; CSS attribute selectors drive Edit/Split/Preview layout; calls _updatePreview() on non-editor panes. _wirePaneToggle() wires only the textarea input listener for live preview refresh.
🫙
Bottles — Full CRUD & Lifecycle
3 creation modes · passage tracking · QR labels · filters
saveBottleAction()updateBottleStatus()_botNextSeq()

🫙 Bottles — Full CRUD & Lifecycle

Creation modes + lifecycle flow
+ Single
startNewBottle('single')
saveBottleAction()
1 bottle · FSL-YYYY-NNNN
openBottle() + QR inline
⊞ Batch
setBotMode('batch')
saveBottleAction()
N bottles · shared batch_id
_showSavedQRGrid()
🔥 Session
setBotMode('session')
saveSessionAction()
all batches · shared session_id
_showSavedQRGrid() grouped
Bottle lifecycle
🧫 Prepared
💉 Inoculated
date_inoculated set
🌱 Growing
✂️ Subcultured
last_transfer set
🏡 Send to GH
sendToGreenhouse()
or terminal:
☣️ Contaminated
|
🗑 Discarded
SINGLE bottle save
startNewBottle('single')
setBotMode('single') · show form
saveBottleAction()
read form · build base obj · code = FSL-{yr}-{seq}
saveBottles(S.bottles, sha)
ghPut bottles/data.enc
openBottle()
QR canvas · detail panel
BATCH save (qty > 1)
setBotMode('batch')
shows qty field
saveBottleAction()
loop qty: uid + FSL-{yr}-{seq+i} + shared batch_id
saveBottles()
one ghPut for all
_showSavedQRGrid()
QR label grid · click → openBottle
SESSION save (autoclave run)
setBotMode('session')
session builder div shown
addSessionBatch()
push {species,stage,recipe,qty} · capacity bar
saveSessionAction()
collect batches · generate bottles + session record
saveBottles(S.bottles, sha, S.sessions)
single ghPut
_showSavedQRGrid()
grouped by batch
EDIT · DELETE · Status stepper · Filters · QR labels · Cross-links
OpFlow
EditstartEditBottle(id) → populate form → S.editingBot → saveBottleAction edit branch → saveBottles → renderBottleList; openBottle
DeletedeleteBottle(id) → confirm → S.bottles.filter(remove) → saveBottles → renderBottleList → showBotEmpty
Status stepperupdateBottleStatus(id, newStatus) → set status + timestamps (date_inoculated, last_transfer) → saveBottles. Buttons: prepared → inoculated → growing → subcultured · terminal: contaminated · discarded · 🏡 sendToGreenhouse
Filter priority1. S.botBatchFilter (batch view) → 2. S.bottleFilter (status) → 3. S.bottleSearch (text) → 4. date desc sort
QR labels6 LABEL_CONFIGS (58mm-full/compact, 62mm, A4-4up/2up/QR-only). _toggleLabelPanel() → scope selector (single/batch) → _botLabelHtmlConfig(b, cfg) → print window
Cross-linksAccession → registry · Recipe → recipes · Linked note → notes · Source GH plant → greenhouse · Batch link → viewBatch()

Passage Tracking (v3.11)

WhereWhat
Bottle editor"Source bottle (passage)" text input → bot-parent-code. Live lookup hint shows species/status (green=found, red=not found).
saveBottleAction()Resolves parent code → parent_id stored on new bottle.
openBottle()Shows "Passaged from" row (link to parent) + "Passaged bottles" section (children with ↓ arrow chips).
🤖
Ravana AI — Context, Routing, Voice
6 tools · 2 STT modes · 18 voice commands · wake word · tool-use gate
askAI()_buildAIContext()_parseVoiceAction()

🤖 Ravana AI — Context, Routing, Tool-Use

AI request routing
askAI(prompt)
user message
_buildAIContext()
notes · bottles · species · analytics · weather · schedule
_selectAIProvider()
groq / claude / gemini
_callAIProxyFull()
→ plantking.ape.workers.dev
↓ response
text reply
renderAIReply()
tool_call
_handleToolCall() → _executeTool()
get_lab_summary
get_bottles
get_species_analysis
search_lab
update_bottle_status ⚠️ confirm
_buildAIContext() — what Ravana sees every call
🏷 Lab identity
📋 10 recent notes
🫙 10 active bottles
🌱 Species summary
📊 Analytics snapshot
⏰ Schedule + overdue
📔 3 journal entries
🧪 5 recipes
🏡 5 GH plants
🌡 Weather readings
📦 Low-stock supplies
↓ _sanitizeContextData() — strip special chars
AI prompt assembled
Provider selection + routing
askAI(prompt)
_aiModel ?
groq8 · groq70 · claude · gemini
_callAIProxyFull()
POST plantking.ape.workers.dev · {provider, model, messages, tools}
response
text reply OR tool_call
↓ if tool_call
_executeTool(name, params)
get_lab_summary
get_bottles
get_species_analysis
search_lab
get_schedule
update_bottle_status ⚠️ confirm gate
Voice STT paths + wake word
Native STT
SpeechRecognition (en-IN)
↓ onfinal
confirm? → _showVoiceConfirm
else → askAI(transcript)
Whisper STT (Groq)
MediaRecorder → blobs
↓ 3.5s silence
_transcribeWhisper() → proxy/whisper
transcript → askAI()
Wake word
SR always-on → "Hey Ravana"
↓ arm
full voice input cycle
↓ "bye Ravana"
disarm · #wake-armed-dot off

Voice Commands — full list (v3.11)

PatternActionAI call?
"home" / "dashboard"navTo('notes')No
"notes" / "bottles" / "greenhouse" / "recipes" / "registry" / "reminders" / "analytics"Navigate to that sectionNo
"settings" / "open scanner" / "sidebar" / "close"Open panel / close AINo
"read protocol N" / "read recipe N"TTS reads numbered recipe aloudNo
"what's overdue" / "how many bottles" / "latest note"TTS reads data summaryNo
"switch to deep" / "switch to Claude" / "switch to quick"Change AI modelNo
"whisper mode" / "native mode"Switch STT modeNo
"stop reading" / "stop"Stop TTS mid-sentenceNo
"add N bottles of [species]" (v3.11)startNewBottle('batch'), prefills species + qtyNo
"open bottle FSL-XXXX" (v3.11)Finds by code → navTo('bottles'); openBottleNo
"show species [name]" / "open species [name]" (v3.11)navTo('species'); openSpDetail(name)No
"export report" / "download report" (v3.11)exportLabReport('csv')No
Anything elseaskAI(transcript)Yes

Activity Log + Session Chip (v3.13)

Every call to askAI() records a log entry in localStorage.tcplants_ai_log (max 500 entries, newest-first). The Activity tab in Ravana Insights renders this as a terminal-style monospace list.

Function / FieldWhat it does
_aiRecordUsage(modelStr, reqRem, tokRem, tokUsed, opts)Writes log entry; opts carries: inTok, outTok, dur (ms), prompt (first 100 chars), fn (call type), err, errCode. Increments session counters.
_aiSessionCalls / _aiSessionTokModule-level counters reset on page load. Incremented by _aiRecordUsage().
_aiUpdateSessionChip()Updates #ai-session-chip in the Ravana panel header with "N calls · X tok".
_aiActivityFilterModule-level string: 'today' | 'week' | 'all' | 'errors'. Persists across tab switches within the session.
_aiSetActivityFilter(f)Sets _aiActivityFilter and re-renders the pane.
_aiTermExpand(row, idx, filter)Toggles an expand row below the clicked terminal row. Shows full timestamp, model, fn, token breakdown (in/out/total), duration, prompt snippet, rate-limit remaining.
_aiExportLog()Creates a JSON Blob of the full log, triggers browser download as ravana-log-YYYY-MM-DD.json.
_callAIDirect()Now returns {inTok, outTok} in addition to tokUsed. Groq: prompt_tokens/completion_tokens. Claude: input_tokens/output_tokens.
Error entriesErrors in askAI() catch block now call _aiRecordUsage() with err:true, errCode:e.status. Displayed with red ● dot in terminal.

AI Budget Control — Control tab (v3.13)

New 🎛 Control tab in AI Master Panel. Provides a master background ON/OFF switch and per-feature token gates. Chat, voice, and quick-chips are always unrestricted.

Key / FunctionWhat it does
_AI_BG_KEY (tcplants_ai_bg)'0' = background AI off; absent or '1' = on. Master switch.
_AI_BUDGET_KEY (tcplants_ai_budget)JSON object: {journal, insights, stock, inline} — each boolean. Default all true.
_AI_CYCLE_KEY (tcplants_ai_cycle)JSON: {freq:'daily'|'weekly', day:0–6, enabled:bool}. Controls journal parse schedule.
_aiBlocked(feat)Returns true if master is off OR feat is false in budget. All background callers call this at entry.
_aiBgEnabled()Reads _AI_BG_KEY from localStorage.
_aiBudget() / _aiBudgetSet(feat,val)Read/write feature budget with defaults merged in.
_aiCycleCfg() / _aiCycleSet(patch)Read/patch journal cycle config.
_journalCycleDue(ist)Returns true if cycle should fire now — checks daily/weekly mode, day-of-week match, and 6-day dedup window.
_aiNextCycleStr()Computes "Tue 29 Apr · 1:00am IST" display string for next scheduled run.
_aiSetBg(on)Writes master key + calls _aiPageControl() + toasts.
_aiPageControl()Renders the full Control tab: master card, journal cycle section, feature rows with toggle-switch + cap input + daily progress bar per feature, web search toggle, fallback toggle, context-saving toggle.
Guarded functions_batchParseUnparsedJournal, parseJournalWithRavana, RavanaInsights._runAnalysis, _stockRavanaInsight — all check _aiBlocked(feat) at entry and return early with a toast or inline message.

Token Caps + Usage Analytics (v3.14)

Daily token caps per feature, full usage analytics tab, chat fallback chain, web search, and context-saving mode.

Key / FunctionWhat it does
_AI_LIMITS_KEY (tcplants_ai_limits)JSON: {journal, insights, stock, inline} — each a daily token cap in tokens. 0 = unlimited. Defaults: journal 8k, insights 15k, stock 4k, inline 5k.
_AI_DAILY_KEY (tcplants_ai_daily)JSON: {date, journal, insights, stock, inline, chat, whisper} — cumulative tokens used today. Auto-resets when date changes.
_AI_WEB_SEARCH_KEY (tcplants_ai_web_search)'1' = web search tool enabled for Ravana. Stored in localStorage.
_aiLimits() / _aiLimitsSet(feat,val)Read/write feature caps. Triggers _aiPageControl() re-render.
_aiDailyUsage()Returns today's usage object. If stored date ≠ today, returns fresh zeroed object.
_aiDailyAdd(feat, toks)Increments today's token count for a feature. Called inside _aiRecordUsage() on successful calls.
_aiBlocked(feat) (updated)Now also checks daily cap: if _aiLimits()[feat] > 0 and _aiDailyUsage()[feat] ≥ cap, returns true and fires a toast warning.
_aiPageUsage()Renders the 📈 Usage tab: summary stat cards (today/week/all-time calls + tokens), feature bar chart, 7-day sparkline, model distribution ring, cap progress bars, export + clear buttons.
_doSingleAICall(provider,model,messages,key)Thin wrapper that dispatches to _callGemini, _callAIProxyFull, or _callAIDirect based on provider. Used by the fallback chain in askAI().
askAI() fallback chainBuilds a chain array: primary → gemini → groq-8b. On error, tries next. If a fallback was used, appends .ai-fallback-pill to the reply. Controlled by tcplants_ai_fallback key.
Context-saving modeWhen tcplants_ai_ctx_save==='1' and today's chat usage > 10k tokens, _buildAIContext() is skipped; replaced with a brief note. Toggle in Control tab.
search_web AI toolCalls /api/web/search on the Cloudflare worker. Worker uses Brave Search API (BRAVE_KEY env var) or DuckDuckGo instant answers as fallback. Returns {results:[{title,url,snippet}], source}.
_aiWebSearchEnabled() / _aiWebSearchSet(on)Read/write web search toggle. Toggle in Control tab → Internet Access.
🏡
Greenhouse — Plant Tracking & Transfers
Bidirectional bottle↔GH · QR tab (?gh=) · condition log · ledger
saveGHPlant()saveGHExplant()openGHPlant()

🏡 Greenhouse — Plant Tracking & Transfer Flows

Bidirectional GH ↔ Lab loop
🏡 GH Stock plant
S.greenhouse[] · quantity ledger
Take explant
startNewBottle() / saveSessionAction()
greenhouse_plant_id wired · 🏡 flair pre-selected
TC growth
sendToGreenhouse()
type:'lab_output_in' logged · quantity += N
Back to stock
🏡 GH Stock grows
saveGreenhouse() → greenhouse/data.enc
CRUD — create / edit / read
startNewGHPlant()
form: species · qty · location · source · health · next_inspection
saveGHPlant()
push S.greenhouse · saveGreenhouse(plants, sha, transfers)
openGHPlant(id)
tabs: overview · transfers · condition log · output plan
Transfers · Ledger · Condition · Save
OpFlow
Bottle → GHsendToGreenhouse(bottleId) → transfer form → saveGHExplant() → push {type:'explant_out', plant_id, bottle_id, qty} to S.ghTransfers → saveGreenhouse
GH → BottleBottle form has greenhouse_plant_id dropdown. On save: bottle.greenhouse_plant_id = plantId. openBottle shows "Source plant" link back.
Qty ledgeradjustQuantity(plantId, delta, reason) → p.quantity += delta → push {type:'quantity_adjust', delta} to ghTransfers → saveGreenhouse
Condition loglogCondition(plantId, health) → p.health + last_inspection → push {type:'health_log'} → saveGreenhouse
saveGreenhouse()data = {plants, transfers} → ghPut(GH_F, enc(JSON), sha). loadGreenhouse reads back raw.plants + raw.transfers

QR on GH Plants (v3.11)

WhereWhat
Plant detail → 📷 QR tabQR code canvas (url: ?gh={id}), plant name/location, Print + Copy buttons
ghTab('qr')Calls _renderGHQRCanvas(S.curGH)QRCode.toCanvas()
QR scanner / URL param?gh=idnavTo('greenhouse'); openGHPlant(id)

Smart CSV Import (v3.12)

ComponentWhat changed
File uploadRemoved accept=".csv,.txt" restriction — any file type accepted
URL fetchfetchGHCsvUrl() — fetches any public URL; Google Sheets edit URLs auto-converted to /export?format=csv
Delimiter detection_detectDelimiter(firstLine) counts tabs vs semicolons vs commas, picks the majority. Fixes copy-paste from Excel/Sheets (tab-separated).
_parseCSVLine(line, delim)Now accepts delim parameter (default ,). Handles quoted fields with embedded delimiters.
size_grade columnAdded to colMap (aliases: size, grade). Parsed + stored on imported plants. _normalizeGrade() applied to incoming values.
Column orderAlways flexible via header matching — was already flexible, but help text now correctly states this.

Size Grade Normalisation (v3.12)

FunctionPurpose
_normalizeGrade(g)Maps legacy values: seedling→sapling, small→s, medium→m, adult→l. Falls through to g itself if already a new value. Returns 's' for null/undefined.

Applied at: vault calculation (line 9191), form population (openGHPlant), import parser. Plant records with old grade values continue to function without any migration step.

🏠
Dashboard — Render Pipeline
10 stat cards · 5 alert types · active strips · 7 fun features · species grid
_renderDashboard()_computeAlerts()_contamStreak()_clInit()_pomoPanelToggle()

🏠 Dashboard — Render Pipeline

_renderDashboard() is called at login and after saves. Static helper: C(area, content, extra)<div class="dc" style="grid-area:area;extra">

10-Card Stat Band (v3.11)

CardValuenavTo
📋 NotesS.notes.lengthnotes
🫙 Bottlesactive count (inoculated + growing)bottles
🌿 AccessionsS.accs.lengthregistry
🏡 Greenhousetotal plant countgreenhouse
⏰ Overdueoverdue notes + bottles + GH plantsreminders
🧪 RecipesS.recs.lengthrecipes
📦 Supplieslow-stock count (yellow when >0)supply
☣️ Contam 30dcontam notes in last 30 days (red when >0)contam
💰 Vaultlive ₹ value (bottles × stage prices + GH × grade)value
📓 Journal 7djournal entries in last 7 daysjournal

Alert strip — _computeAlerts()

ConditionLevelnavTo
Species with >10% contam rate in 30dcritcontam
Any overdue next_review (bottles or notes)warnreminders
≥5 active bottles past 28d inoculationwarnbottles
GH plants with health ≠ healthywarngreenhouse
Supply batch expired or expiring <14dinfosupply

7 Fun Features (v3.12)

FeatureWhereStateKey functions
🔥 Contamination StreakDashboard panel grid-area:streakComputed from S.notes contam dates_contamStreak()
🌿 Species SpotlightDashboard panel grid-area:spotlightDate-seeded random from S.taxSpeciesInline in _renderDashboard()
☑ Daily ChecklistDashboard panel grid-area:checklistlocalStorage tcplants_checklist, auto-resets midnight_clInit() _clToggle() _clAdd() _clRemove()
🍅 PomodoroFloating FAB #pomo-fab, panel #pomo-panellocalStorage tcplants_pomo_sessions_pomoPanelToggle() _pomoToggle() _pomoTick() _playPomoChime()
🌳 Lineage TreeBottle detail — passage sectionComputed from S.bottles parent_id chain_treeAncestor() _renderLineageTree()
📸 Photo StripBottle detail — below lineageFiltered from S.gallery by bottle codeInline render in openBottle()
⚗️ Recipe Inline ScalerRecipe detail panelStateless — recomputes on each input change_recScaleRender() _recScaleUpdate()

Drag-and-Drop Layout (v3.18)

Each .dc card has draggable="true" and a hover-reveal grip handle. Swaps CSS grid-area values between two cards. Layout persisted in localStorage['tcplants_dash_layout'] as {panelKey:swappedArea}.

FunctionPurpose
_dashLayoutModule-level object, initialised from localStorage at page load
_dashGetArea(def)Returns saved area for panel key, or def if not remapped
_dashSaveLayout()Reads current DOM grid-area values, writes delta map to localStorage
_dashResetLayout()Clears localStorage key, resets _dashLayout={}, re-renders dashboard
_dashWireDrag(pEl)Called after pEl.innerHTML set; attaches dragstart/dragover/drop listeners to all .dc children
Settings resetSettings → Appearance → "↺ Reset" button calls _dashResetLayout()
📊
Analytics Engine
12 charts · species ranking · lab cost · lifecycle · freshness fixed
LabAnalytics.calculate()renderAnalytics()_buildSpeciesRankingCard()

📊 Analytics Engine

Two systems — both kept in sync (v3.10)

SystemWhen it runsOutputConsumed by
LabAnalytics.calculate()Login + after saveNoteAction + after saveBottleAction (fixed v3.10)S.analytics: contam rates, success rates, multiplication, culture timeline, alerts_buildAIContext() — AI prompt context
renderAnalytics()Analytics section visitSVG charts inline-calculated from S.notes + S.bottlesAnalytics page UI only

Analytics page cards

CardData source
Notes / Month, Stage Donut, Contam Trend, Recipe Usage, Tag CloudS.notes (inline calculation)
Species Success Rate, Stage Funnel, Avg Days Between Entries, Media EfficiencyS.notes + S.bottles
Culture Lifecycle StatusActive bottles vs expected duration (historical avg or subculture_interval_days)
Species Performance Ranking (v3.11)_buildSpeciesRankingCard(): score/100, contam%, multiplication×, avg cycle days, active bottles — ranked table for all species ≥2 notes
Lab Cost Tracking (v3.11)_buildLabCostCard(): S.supplies items with cost_total + date_prepared → 6-month bar chart + total spend + estimated ₹/bottle
Feature UsageSection-visit counters in localStorage
📓
Lab Journal — Entry, Parse, Extract
Voice dictation · overnight parser · extract cards · parse toggle
parseJournalWithRavana()saveJournalEntry()_scheduleOvernightJournalParse()

📓 Lab Journal — Entry, Parse, Extract

CREATE + EDIT journal entry
startNewJournalEntry()
clear form · show editor
saveJournalEntry()
new: {id,title,body,date,parseEnabled:true,parsed:false}
edit: update body · reset parsed=false
saveJournal(S.journal, sha)
ghPut journal/data.enc
renderJournalList()
Voice dictation in journal
_journalMicTap()
Whisper mode
record → _transcribeWhisper()
append to je-body
Native SR mode
SpeechRecognition live
final text appended
Ravana parse + overnight batch
parseJournalWithRavana()
or overnight: 8s after login
askAI(parse prompt)
JSON schema: recipes · species · notes
_renderJournalExtracts(data)
extract cards shown
_journalCreateRecipe() → recipes form
_journalCreateNote() → notes form
_journalAddSpecies() → species form
_journalCreateAll() → batch create
Parse toggle: _setJournalParseEnabled(id, bool) — paused entries show ⏸ badge, skipped by overnight parser
🌱
Taxonomy — Packs, Custom Species, Flairs
Pack loading · autocomplete · species picker · CSV import · flairs
loadTaxonomy()_taxSuggest()_importCsvTaxonomy()

🌱 Taxonomy — Packs, Custom Species, Flairs

loadTaxonomy() — pack loading
TAX_PACKS map
nepenthaceae · orchidaceae · musaceae · …
fetch taxonomy/{cat}/{pack}.json
enabled packs from localStorage
S.taxSpecies[]
canonical names · S.taxFlairs[]
ghGet(TAX_CUSTOM_FILE)
S.taxCustom.species + flairs
Autocomplete · Picker · CSV import · Flairs
FeatureFlow
Autocomplete_taxSuggest(inputEl) oninput → filter S.taxSpecies + taxCustom → #tax-dropdown → keyboard nav (↑↓ Enter) → _taxDropdownSelect(name) → fill + trigger related dropdowns
Species pickeropenSpeciesPicker(targetFieldId) → #species-picker-modal → browse by category or search → select → fill targetFieldId input
CSV import_importCsvTaxonomy(csvText) → parse cols Name,Category,CommonName,Source → validate → push to S.taxCustom.species → saveCustomTaxonomy() → ghPut TAX_CUSTOM_FILE
FlairsS.taxFlairs + S.taxCustom.flairs → _botFlairHtml(flair_ids) → colored pills in bottle detail + list. Flair picker in bottle editor: click to toggle flair_ids[]
Reminders — Badge, List, Navigation
3 sources (notes/bottles/GH) · urgency groups · nav badge count
renderReminders()updateReminderBadge()
— Badge, List, Navigation
Data sources → renderReminders()
🫙 Bottles (next_review + active status)
📋 Notes (next_review set)
🏡 GH plants (next_inspection set)
↓ renderReminders()
overdueBot — next_review < today
overdueNote — next_review < today
overdueGH — next_inspection < today
dueBot / dueNote — within 7 days
ghXferDue — inoculated > 28 days
↓ sort by days remaining → .rem-card divs
isGh=1 → navTo('greenhouse');openGHPlant
isBottle=1 → navTo('bottles');openBottle
else → navTo('notes');openNote
updateReminderBadge() — count overdue → #nd-rbadge. Called after: login · saveNoteAction · saveBottleAction · saveGHPlant · deleteNote. ⚠️ bottle edits do not auto-update badge
🔍
Search System — Cross-Section
8 sources · tokenized AND · field prefixes · highlighting · recent searches
SearchSystem._run()SearchSystem._hl()_extractKeywords()
System — Cross-Section
SearchSystem — cross-section query flow
renderSearchPage()
input + saved filter chips
_doSearch(query)
oninput
📋 notes: title/body/species/tags
🫙 bottles: code/species/notes
🌿 accs: species/id/source
🧪 recs: name/base/notes
🏡 GH: species/location/notes
↓ grouped results
Notes → navTo('notes');openNote(id)
Bottles → navTo('bottles');openBottle(id)
Accs → navTo('registry');openAccession(id)
Recipes → navTo('recipes');openRecipe(id)
GH → navTo('greenhouse');openGHPlant(id)
Saved filters — localStorage('tcplants_search_filters') → up to 20 filter chips · click chip → fill input → trigger search
📦
Inventory — Two Systems
Supply = primary · Stock redirects to Supply · batch perf correlation
SupplyInventory.renderInventoryPage()calcPerformance()

📦 Inventory — Two Systems

SystemStateFileLoadUse case
Stock (nav → 📦 Stock)
⚠️ Redirects to Supply since v3.11
S_inv = {items:[]} — separate object inventory/data.enc On section visit (not at login) Reagents with low-stock thresholds, category tabs
Supply Inventory (nav → 📦 Supplies) — primary S.supplies[] — part of main S supplies/data.enc loadSupplies() at login (8th Promise.all) Media prep batches + TC performance correlation
📦 Stock section now redirects to Supply via switchSection() early return. Supply is the primary inventory system — batch cards with expiry, cost/unit, and contamination correlation.

calcPerformance(item): finds bottles prepared within date_prepared → next_batch window → calculates contamRate + successRate vs lab average. Flags if >10% worse.

📅
Calendar — Month Grid & Day Panel
Month grid · day panel · create from date · ICS export
renderCalendar()_openCalPanel()
— Month Grid & Day Panel
Calendar render + day panel
renderCalendar()
S.calYear + S.calMonth → month grid · day cells show note+bottle counts
click day → _openCalPanel(date)
S.calSelDate = date
S.notes by date
S.bottles by date_prepared/inoculated
S.journal by date
S.greenhouse by next_inspection
↓ .cdp-card divs
isBottle=1 → navTo('bottles');openBottle
else → navTo('notes');openNote
New note from calendar: click day → "New entry for this date" → S.calDate = date → navTo('notes') → saveNoteAction uses date from S.calDate → S.calDate=null
Month nav: S.calMonth/calYear ±1 → re-renderCalendar · Export: _exportCalRange(start, end) → .ics / markdown
🌡️
Weather, Activity Log, Service Worker
Open-Meteo dual location · 60s interval · last 30 commits · SW cache
fetchWeather()fetchActivityLog()
, Activity Log, Service Worker
Weather — dual location, 60s polling
fetchWeather()
login + every 60s via window._wxInterval
geolocation or stored coords
lab coords + GH coords (separate settings)
fetch OpenMeteo API
temp · real-feel · humidity · UV · rain
S.weather = {lab, gh, updatedAt}
→ #weather-chip button text
Activity log + Service Worker
SystemFlow
Activity logfetchActivityLog() → GET GitHub commits API ?per_page=30 → render .al-clickable items → click → find note by title → closeActLog(); navTo('notes'); openNote. Auto-refreshes on login.
Service Workernavigator.serviceWorker.register('/sw.js'). Cache: network-first for index.html, cache-first for CDN assets. Bump SW_CACHE version to force refresh.
Offline modenavigator.onLine → #offline-pill shown. Write ops blocked via _checkOnline(). ⚠️ offline check inconsistently applied — only notes index is guarded
🌍
GBIF Integration — 6 Features (A–F)
Synonym resolver · common names · distributions · photo · hierarchy · bulk CP
_gbifImportSpecies()_loadSpDetailGBIF()_gbifBulkImportCP()

🌍 GBIF Integration — 6 Features (v3.10–v3.11)

FeatureWhere triggeredWhat it doesState written
A — Synonym resolverCustom species form (_taxGBIFLookup()) + species detail panel (_loadSpDetailGBIF())If GBIF returns synonym=true, shows yellow banner with accepted name + "Use accepted name →" swap buttonAuto-fills form
B — Common names_gbifImportSpecies()Fetches vernacularNames in parallel with IUCN. English name (or first available) auto-fills common_name fieldsp.common_name
C — Native distributions_loadSpDetailGBIF()Fetches distributions, filters NATIVE/ENDEMIC status, shows compact country tag row in species detail panelUI only
D — Reference photo_loadSpDetailGBIF()Fetches media?type=StillImage&limit=1, injects photo div with rights credit below Summary block. onerror hides silently.UI only
E — Taxonomy hierarchy_gbifImportSpecies() 3rd parallel fetchFetches GET /v1/species/{key} for order/class/kingdom. Saved on the sp object in taxCustom.sp.gbif_order, gbif_class, gbif_kingdom
F — Bulk CP genera import"🌿 Import CP Checklist" button in GBIF tab_gbifBulkImportCP(): fetches 5 family keys in parallel (Nepenthaceae, Droseraceae, Sarraceniaceae, Lentibulariaceae, Cephalotaceae). Deduplicates, skips already-imported, batch-inserts to taxCustom.S.taxCustom.species[]

All 6 features use Promise.allSettled() internally — network failures degrade gracefully without crashing the UI.

💰
Value Inventory — Vault Calculation
8 price tiers · live vault · GH size_grade · INR formatting
renderValueSection()_valPriceSet()loadValue()

💰 Value Inventory — Vault Calculation (v3.10)

Nav drawer → 💰 Value. Estimates live inventory value from bottles + greenhouse stock using user-configured base prices.

ComponentDetail
Prices tab12 base prices: bottles by stage (I–IV, Weaned, Rooting) + GH plants by size_grade (Sapling / S / M / L / XL / XXL). Saved to S.value.prices with 800ms debounce auto-save.
Vault tabLive calculated: Σ(bottles by stage × stagePrice) + Σ(GH plants × gradePrice × quantity). Species-level breakdown with overrides (pending). Vault total shown in INR.
Dashboard stat cardCompact ₹ value — formatted with fmtINR() (Indian locale: ₹12k / ₹1.2L)
GH plant size_grade6-tier grade: Sapling / S / M / L / XL / XXL. Old values remapped by _normalizeGrade() at read time. Keys: gh_sapling, gh_s, gh_m, gh_l, gh_xl, gh_xxl.
StateS.value = {prices:{}, overrides:{}} · S.valueSha — 8th load in login Promise.all
Filevalue/data.enc
📥
Export System (v3.11)
3-CSV download · print report · voice trigger · legacy ZIP/CSV
exportLabReport()_downloadText()

📥 Export System (v3.11)

FunctionModeOutput
exportLabReport('csv')CSV3 separate Blob downloads: bottles.csv (code/species/stage/status/dates/recipe/batch/parent/notes), notes.csv (date/title/type/stage/species/cultures_out/recipe/tags), greenhouse.csv (species/location/qty/health/size_grade). Uses Blob + <a download> pattern.
exportLabReport('print')PrintGenerates styled HTML with summary stat band + active bottles table + GH plants table. Opens in new window, auto-prints via window.onload.
Analytics export buttonsCSV + Print"📥 Export CSV" + "🖨 Print Report" in analytics header.
Voice command"export report" / "download report"Calls exportLabReport('csv') directly.

Legacy exports (always available)

ButtonOutput
⬇ ZIPAll notes as decrypted Markdown + accessions + recipes as JSON
CSV (top bar)Note metadata CSV + accessions CSV
📅 Calendar exportPrint-ready HTML: this week / this month
Calendar Alarms + Tray Chip (v3.12–3.13)
localStorage · IST tick · Web Audio · fire modal · widget tray · OS notification
_loadAlarms()_nextAlarmMinutes()_showAlarmModal()_openCalendarAlarms()

⏰ Calendar Alarms (v3.12)

Alarm state is persisted in localStorage key tcplants_alarms. A deduplication set _firedAlarms prevents re-firing within the same minute.

FunctionWhat it does
_loadAlarms()Parses localStorage.tcplants_alarms → Array of {id, time, label, enabled}
_saveAlarms(arr)JSON-stringifies and writes back to localStorage
_addAlarm()Reads time + label inputs → pushes new alarm → _saveAlarms()_renderAlarms()
_toggleAlarmEnabled(id)Flips alarm.enabled → saves
_removeAlarm(id)Filters by id → saves → re-renders
_renderAlarms()Rebuilds alarm card list in #cal-alarm-panel
_updateAlarmBtn()Shows/hides 🔔 badge on ⏰ Alarms button based on enabled alarms count
_playAlarmSound()Web Audio API: 3-pulse sine wave + harmonic. No external files. Ramps gain in/out.
_showAlarmModal(alarm)Populates #alarm-modal with time + label → adds class .on → plays sound
_dismissAlarm()Removes .on from modal → stops sound

Tick logic + tray chip (v3.13)

A setInterval(check, 1000) runs every second. Alarm matching only runs when ist.getSeconds() <= 4 (first 4 seconds of each minute). A _trayTick counter increments each second; every 10 ticks it calls _updateWidgetTray() to refresh the countdown chip. When an alarm fires: _showAlarmModal() sets #alarm-modal.on and calls _updateWidgetTray() immediately — the chip switches to red 🔔 Alarm!. _dismissAlarm() also calls _updateWidgetTray() to revert the chip back to the countdown.

FunctionWhat it does (v3.13 additions)
_nextAlarmMinutes()Finds the nearest enabled alarm — computes IST current minute, diffs against each alarm's HH:MM, wraps at 1440. Returns {alarm, diff} or null.
_openCalendarAlarms()Calls navTo('calendar') then after 250ms auto-opens the alarm panel if it's currently hidden.
_addAlarm()Now also requests Notification.permission on first alarm creation (if still 'default').
_showAlarmModal()Now also fires an OS notification via new Notification() if permission is 'granted'. Calls _updateWidgetTray().
_dismissAlarm()Now calls _updateWidgetTray() after removing .on.
_saveAlarms()Now calls _updateWidgetTray() so chip appears/disappears when alarms are added/removed/toggled.
🔗
Cross-section Wiring (v3.16)
6 reverse-wiring connections that propagate an action in one section to related data in another
_wireSalesToRegistry _remDone _sterilDeductOpen
WireTriggerTarget sectionFunctionMechanism
1 — Sales → Registry_saveSaleAction() on saveSpecies Registry (taxCustom)_wireSalesToRegistry(lines)Checks each line item species against S.taxSpecies + S.taxCustom.species. New names pushed to taxCustom, saveCustomTaxonomy() called. Toast after 200ms delay (doesn't conflict with main save toast).
2 — Bottle notes → RecipessaveBottleAction() new bottle, no recipe_id, notes.length > 20Recipes_toastAction (inline)After 3.4s delay (after main save toast fades), shows action toast. If accepted: navTo('recipes'), startNewRecipe(), pre-fills re-name and rr-notes from bottle notes.
3 — GH harvest → BottletakeExplantFromGH(plantId)BottlestakeExplantFromGH (existing)Already implemented: logs ghTransfer, navTo('bottles'), startNewBottle('single'), pre-fills species + GH plant dropdown via _populateGHDropdown. No new code needed.
4 — Contam note → JournalsaveNoteAction() where type==='Contamination Log'Lab Journal_toastAction (inline)After 3.4s delay, shows action toast. If accepted: navTo('journal'), startNewJournalEntry(), pre-fills je-title + je-body with contam details (species, stage, note ref, original body).
5 — Reminder Done → reschedule✅ Done button on rem-cardBottles / Notes / Greenhouse_remDone(id, isBottle, isGH)Prompts for days. Computes next date. Routes to saveBottles / saveNotesIndex / saveGreenhouse based on item type. Calls renderReminders + updateReminderBadge.
6 — Sterilise → Supplies_saveSterilRun() if run.status !== 'failed'Supply Inventory_sterilDeductOpen(run), _sterilDeductConfirm()After 3.4s delay, shows action toast. Opens overlay with supply list fuzzy-matched against load_contents. Checkboxes + qty inputs. Confirm decrements S.supplies[].qty and calls saveSupplies.
7 — Sales received✓ Received button on dispatched order cardSales (same)_saleMarkReceived(id)Sets status='received', received_at=ISO timestamp, prompts for payment_mode if unset. Calls saveSales(). New 'received' status in _SALE_STATUSES (4th value).
All async wires are fire-and-forget or use _toastAction delayed by 3.4s so they don't visually collide with the main save toast. Wire 1 fires synchronously post-save. The _toastAction helper creates a toast with an action button; clicking it clears the toast and runs the callback.
🔌
IoT Master Control Panel
ESP32/ESP8266/GSM · Tasmota/Shelly · Cameras · Swarm Deploy · Network Scan · Cloudflare Worker + D1
ESP32 GSM/4G Cameras Swarm

IoT Master Control Panel — Flow

navTo('iot') _iotInit() _iotPollStart() · _iotRefresh()
_iotRefresh() GET /api/iot/devices (D1) _iotRenderDevices() → device grid
User taps ON/OFF _iotSendCmd(deviceId, pinId, action) POST /api/iot/cmd/:deviceId _iotWaitConfirm() backoff poll
ESP heartbeat (30 s) POST /api/iot/heartbeat D1: store readings, eval rules + timers return pending cmds to device
Device executes cmd POST /api/iot/confirm/:cmdId { success, gpio_readback } D1: status = confirmed, pin.current_value updated
Browser polling (5 s) _iotWaitConfirm() → GET /api/iot/cmd-status/:cmdId status=confirmed → toast + UI update

Key Functions

FunctionPurpose
_iotInit()Entry point — check worker URL, start poll, fetch devices
_iotRefresh(silent)GET /api/iot/devices, update _iotState.devices, re-render grid
_iotSendCmd(devId,pinId,action)POST command to worker, trigger confirm-wait loop
_iotWaitConfirm(devId,cmdId,n)Backoff poll (2→3→5→8→13→15s) until confirmed or failed
_iotOpenDetail(deviceId)Slide-in panel with 5 tabs: Controls / Sensors / Timers / Automation / Cmd Log
_iotRenderSensors(dev)Calls _iotPinChannels() to expand multi-channel sensors to sub-channels; draws Chart.js line chart per channel querying /api/iot/sensors/:id?pin=channel_id — last 24 h from D1
_iotPinChannels(p)Returns [{id,label,unit}] for a pin — single-channel returns [{p.id,p.label,p.unit}]; multi-channel (BME280/DHT/SCD30/INA219 etc.) returns all sub-channel IDs (env_temp, env_hum, env_press…)
_iotRenderTimers / _iotSaveTimerGET/POST /api/iot/timers — evaluated server-side on heartbeat
_iotRenderRules / _iotSaveRuleGET/POST /api/iot/automations — if-then rules with hysteresis + cooldown
_iotSerialConnect / _iotFlashFirmwareWeb Serial API: open port, write firmware binary, write JSON config
_iotSaveDevice / _iotOpenAddRegister or update custom device + pins via /api/iot/register · /api/iot/devices/:id
_iotSwitchAddTab(tab)Switches add-device modal between 🔌 Custom and 🏭 Commercial panes; hides tab bar when editing existing device
_iotCommTypeHint()Updates hint text below the type select — explains API differences for auto / tasmota / shelly1 / shelly2
_iotDetectCommercial()Orchestrates detection: probes Tasmota REST then Shelly Gen1/Gen2; populates result area; enables Save button on success
_iotDetectTasmota(ip,port,user,pass)GET /cm?cmnd=Status%200 — extracts model, NumChannels; returns { dtype, model, channels[] } or null
_iotDetectShelly(ip,port,hint)GET /shelly — detects Gen1 (relays[]) vs Gen2 (src/gen field); Gen2 follows up with /rpc/Shelly.GetStatus for switch count
_iotSaveCommercial()Saves detected device to D1 via /api/iot/register with device_class=commercial and comm_config JSON; builds pin records from channels
_iotCommercialCmd(devId,pinId,action)Direct HTTP to device: Tasmota /cm?cmnd=Power; Shelly1 /relay/N?turn=; Shelly2 /rpc/Switch.Set; calls _iotCommercialRefresh on success
_iotCommercialRefresh(dev)Polls current relay state from device REST/RPC; updates dev.pins[].current_value; records poll time in _iotCommState[id]; re-renders grid. Auto-called on panel load for unpolled devices; also available via 🔄 Poll button on each commercial card.
_iotHelpOpen / _iotHelpCloseSlide-in help panel — 9 searchable cards covering hardware, flashing, pin types, lifecycle, automation, timers, troubleshooting, firmware studio
_iotHelpSearch(q)Filters .iot-hcard elements by data-tags + textContent — live search as user types
_iotTogglePanel()Toggles iotPanelOn in localStorage — UI load gate only, never affects physical devices or Worker
_iotRenderPanelState(on)Shows/hides #iot-panel-body vs #iot-panel-off placeholder; updates toggle button label
_fwOpen() / _fwClose()Show/hide #fw-studio fullscreen overlay; pre-fills Worker URL + key from IoT localStorage keys
_fwTab(name)Switch between Form / Editor / AI / Serial / Flash tabs (v3.28: 5 tabs). Lazy-loads CodeMirror on first Editor switch. Hides fw-left for Editor/Serial/Flash.
_fwPresetsLoad/Save/RenderNamed form presets stored in localStorage fw_presets. _fwPresetCapture() captures all form fields + _fwPins[]. _fwPresetLoad(id) restores them. Pills shown above Templates strip.
_fwFilesInit/Render/Switch/Add/RemoveMulti-file editor state: _fwFiles[] = [{name,content}]. Index 0 always main.ino. File tabs rendered in #fw-file-tabs (shown only in Editor tab). _fwFileSwitch saves CM6 state before switching EditorState.
_fwDownloadZip()When _fwFiles.length > 1: lazy-loads jszip from esm.sh, bundles all files into ZIP named after device, triggers download.
_fwSerialConnect/DisconnectWeb Serial API: requestPort → open at selected baud → TextDecoderStream read loop → line-buffered append to #fw-serial-out. TextEncoderStream writer stored for _fwSerialSend().
_fwSerialAppend(line)Timestamps + appends line to serial output; classifies as .err / .warn / .info by keyword scan; auto-scrolls if checkbox checked; caps DOM at 1000 lines.
_fwFlashConnect()Picks USB serial port via Web Serial requestPort(); stores in _fwFlashPort; unlocks flash step 2 + 3.
_fwFlashWriteConfig()Opens port @ 115200; sends SOH+CFG:{json}+EOT packet with WiFi/worker credentials; device firmware detects SOH, saves to EEPROM/NVS, reboots.
_fwFlashBin()Fetches .bin URL; writes to device in 512-byte chunks via WritableStream; progress bar fills. Works for devices with existing bootloader only.
_fwGenerate()Builds complete compilable .ino string from form state. Pin struct now has id field (label→lowercase+underscore). buildReadings() returns JSON array; sendHeartbeat() embeds it in heartbeat body so automations fire. pollCommands() calls /api/iot/cmd/:deviceId; confirmCommand() calls /api/iot/confirm/:cmdId. gpio resolved via gpioPinById() from PINS[].id.
gpioPinById() [generated]Generated C++ helper: iterates PINS[] comparing PINS[i].id to incoming command pin_id, returns PINS[i].gpio — maps Worker command to hardware GPIO at runtime
buildReadings() [generated]Generated C++ function: reads all sensors, returns JSON array string (no HTTP call). Called by sendHeartbeat() which embeds it in the heartbeat POST body
sendHeartbeat() [generated]Generated C++ function: calls buildReadings(), POSTs to /api/iot/heartbeat with readings embedded, parses returned commands array and executes each via gpioPinById() + confirmCommand()
pollCommands() [generated]Generated C++ function: GETs /api/iot/cmd/:deviceId (fires timer-scheduled commands), executes each command via gpioPinById(), confirms via confirmCommand()
_fwHighlight(code)Tokenising state-machine C++ highlighter (no regex) — 7 token types: comments, strings, preprocessor, keywords, Arduino constants, numbers, function calls
_fwAddPin() / _fwRemovePin(i)Manages _fwPins[] array; re-renders pin builder rows; triggers _fwGenerate()
_fwLoadEditor()Dynamic import() of codemirror + @codemirror/lang-cpp from esm.sh; creates EditorView in #fw-cm-host; syncs current code into editor
_fwSyncFromEditor()Reads CM6 EditorView state and updates _fwCode — called before copy/download when in Editor tab
_fwCopy() / _fwDownload()Copy raw .ino to clipboard / trigger browser file download with correct filename from #fw-filename
_fwWokwi()Opens Wokwi simulator deep-link in new tab with current code and a diagram.json built from pin builder state
_fwAiImprove()POSTs current code to Worker /api/ai-improve → Groq llama-3.3-70b; replaces code in both #fw-pre and CM6 editor on success
_fwAiPersist()Saves AI toggle state to localStorage fw_ai_on; enables/disables the Improve Code button
_fwIsGSM(chip)Regex test on chip string — matches a7670, sim800, sim7600, ec600, gsm, lte, 4g, cellular, tinygsm; used by _fwGenerate and _fwOnChipChange
_fwGsmModemDefine(chip)Maps chip string to correct #define TINY_GSM_MODEM_* constant: SIM800/SIM808/SIM900→SIM800, SIM7600/SIM7670→SIM7600, SIM7020→SIM7020, EC600/EC200→EC600, fallback A7670 for A7670E/A7672/A7680
_fwOnChipChange()Shows fw-gsm-section or fw-wifi-section based on _fwIsGSM(chip); triggers _fwGenerate()
_iotScanToggle()Shows/hides #iot-scan-panel; toggles Scan button label; clears previous results on hide
_iotScanParseCIDR(cidr)Expands CIDR notation (/16 to /30) into an array of IP strings to probe
_iotScanStart()Parallel fetch probes in batches of 50 (1 s timeout each); updates progress bar; calls _iotScanProbe per IP
_iotScanProbe(ip)Tries _iotDetectTasmota then _iotDetectShelly on the IP; returns { ip, dtype, model } or null on timeout/error
_iotScanAdd(result)Pre-fills commercial device add modal with IP + detected type from a scan result chip click
_iotSwarmOpen() / _iotSwarmClose()Show/hide #iot-swarm-overlay; resets form and seeds one default device row on open
_iotSwarmChipChange()Shows sw-wifi-fields or sw-gsm-fields block based on chip selection; also sets sw-gsm-mode checkbox
_iotSwarmAutoName()On project name change: auto-generates device names (R01, S01…) from project prefix for all rows
_iotSwarmAddRow() / _iotSwarmRemoveRow(i)Add/remove a device row from _swarmRows[]; re-renders the device table via _iotSwarmRenderRows()
_iotSwarmAddBatch()Reads N + template from batch controls; appends N identical rows; re-renders
_iotSwarmRenderRows()Renders the swarm device table from _swarmRows[]; each row has name input, template select, Remove button
_iotSwarmBuildFirmware(i)Saves current studio state, temporarily loads row i config into studio form, calls _fwGenerate(), captures code, then restores original state
_iotSwarmDownloadZip()Lazy-loads JSZip from esm.sh; calls _iotSwarmBuildFirmware for each row; bundles all .ino files into a ZIP download
_iotSwarmRegister()Calls /api/iot/register N times (one per swarm row) with device_class=custom; shows progress toast; refreshes device grid
_iotGuideOpen() / _iotGuideClose()Show/hide #iot-guide-overlay; calls _iotGuideTab(0) on open to reset to Overview tab
_iotGuideTab(n)Activates tab n: toggles .active on .iot-guide-tab buttons and .on on .iot-guide-section divs by index
_camToggle()Flips camPanelOn in localStorage; shows/hides #iot-cam-panel; calls _camRefresh() on first enable
_camRenderToggleState()Updates #iot-cam-btn class and title text based on _camState.on; called on init and after toggle
_camRefresh()GET /api/cams via _iotFetch; stores results in _camState.cameras; calls _camRender()
_camRender()Renders #iot-cam-grid: MJPEG cameras as live <img src>, snapshot cameras with setInterval polling + ?t= cache-bust
_camImgErr(img)Shows .cam-err overlay on stream/snapshot load failure; clears snapshot poll timer for that camera
_camOpenAdd(id) / _camSave()Opens #iot-cam-modal for add (id=null) or edit; saves via POST /api/cams or PUT /api/cams/cam_:id
_camDelete(id)DELETE /api/cams/cam_:id; refreshes camera grid
_camOpenRooms() / _camSetRoomPw(roomId, label)Opens #iot-cam-rooms-modal; _camSetRoomPw POSTs hashed password to /api/cams/rooms for a given room ID
_iotRulesOpen() / _iotRulesClose()Show/hide #iot-rules-overlay; populates device filter dropdown; calls _iotRulesLoadAll()
_iotRulesLoadAll()GET /api/iot/automations (no filter) — all rules across all devices with device names; renders via _iotRulesRenderAll()
_iotRulesRenderAll()Groups rules by trigger device, renders with enable toggle + delete, respects _iotRulesFilterDev
_iotToggleRule(id, enabled)PATCH /api/iot/automations/:id {enabled: !current}; refreshes per-device tab or global panel depending on context
_iotDeleteGlobalRule(id)DELETE from global panel; removes from _iotAllRules and re-renders without closing panel
_iotRenderOta(d)Renders 🔄 OTA tab — status card (queued/updating/complete/error), URL input, Queue Update button, Cancel button, recovery AP note. Auto-refresh every 3s while status=updating
_iotQueueOta(deviceId)Reads ota-url-[id] input; POST /api/iot/ota-push {device_id, firmware_url}; refreshes device state and card
_iotCancelOta(deviceId)DELETE /api/iot/ota-push/:deviceId; clears ota_url + ota_status in D1; refreshes device state
_iotDeleteDevice(deviceId)Confirms, calls DELETE /api/iot/devices/:id (cascades pins/commands/readings/rules in D1), removes from _iotState.devices, re-renders grid
_iotSubChanLabel(deviceId,pinId)Resolves sub-channel pin_id (e.g. "env_temp") to a friendly label ("Env Temp °C") using _iotPinChannels() on cached device state; used in automation rule display
loadDeviceId() [generated]Generated C++: reads saved device ID from ESP32 NVS (Preferences.h) or ESP8266 EEPROM on startup; sets registered=true if id found — prevents ghost devices on reboot
saveDeviceId() [generated]Generated C++: persists deviceId to NVS/EEPROM after successful Worker registration; called once at first registration

D1 Schema Tables

TableColumns
devicesid, name, location, chip, room, ip, last_seen, created_at, device_class (custom/commercial), comm_config (JSON), ota_url, ota_queued_at
pinsid, device_id, label, type, gpio_pin, unit, current_value
commandsid, device_id, pin_id, action, status, source, gpio_readback, created_at, received_at, confirmed_at, fire_at
sensor_readingsid, device_id, pin_id, value, ts
timersid, device_id, pin_id, action, time_hhmm, days (7-char bitmask), duration_s, created_at
automationsid, device_id, if_pin, operator, threshold, then_pin, then_action, hysteresis, cooldown_s, enabled, created_at
camerasid, name, location, room, ip, port, stream_path, snapshot_path, stream_type (mjpeg/snapshot), created_at
cams_roomsroom (PK), name, password_hash (SHA-256)
cams_sessionstoken (PK), room, expires_at — 8-hour session tokens for /cams portal room auth
⚠️
Known Gaps & Latent Bugs
12 fixed · 1 open
CSP pending

Known Gaps & Latent Bugs

IssueLocationImpactStatus
loadInv() missing ghDec()Fixed v3.9Stock data unreadable if inventory/data.enc exists✅ Fixed
S.analytics goes stale after savesFixed v3.10Now: LabAnalytics.calculate() called after saveNoteAction + saveBottleAction✅ Fixed
renderAnalytics() vs S.analytics disconnectrenderAnalytics()renderAnalytics() recalculates inline — S.analytics used only by AI context. Not a bug, but duplicate computation.Design debt
SupplyInventory.items dead assignmentFixed v3.9Dead code — renderInventoryPage() uses S.supplies directly✅ Fixed
validateBottleData wrong status listFixed v3.9Now includes all 10 statuses✅ Fixed
Batch checkbox invisibleFixed v3.9CSS width:100% scoped to text inputs only✅ Fixed
Offline check inconsistencyFixed v3.10Removed Haiku monkey-patches — all load functions fail naturally on network error✅ Fixed
updateReminderBadge not called after bottle savesFixed v3.8Badge now updates after all saves✅ Fixed
GH CSV import: tab-separated data parsed as single columnFixed v3.12_detectDelimiter() now auto-selects comma/tab/semicolon. Copy-paste from Excel/Sheets works.✅ Fixed
GH import: file accept restricted to .csv/.txtFixed v3.12Removed accept restriction — any file accepted✅ Fixed
_stockRavanaInsight data source mismatchFixed v3.27Was reading S_inv.items instead of S.supplies; field names (low_threshold→min_stock, expiry_date→expiry, price_*→cost_total) also corrected✅ Fixed
Alarm clock fires only while tab is openCalendar → ⏰ AlarmsNo service worker push — alarms rely on setInterval. Will not fire if tab is closed or sleeping on mobile.Design constraint
Agentic Ravana tool-useFixed — fully implementedcreate_note, set_review_date, log_contamination all save to GitHub with rollback on error; confirm gate wired via _executeToolWithConfirm()✅ Fixed
🤖
AI Master Control
Persona · Models · Voice · Functions · Tools · Context · Activity · System
📋
Sales & Invoicing
New Sale · Orders · Invoice
🗂
File Browser
Attachments · References · Exports
💹
Financial Engine
P&L · Cost per plantlet · Supply spend · Labour
🗓
Production Planning
Back-calculate bottle requirements · Stage timeline · Transfer schedule
🔌 IoT Control
🤖
Automation Rules
All if-then rules across every device — evaluated server-side on each heartbeat
Loading…
Swarm Deploy
Set up multiple devices for a new place in one shot
Place / Project
Shared Config
Devices
#Device NameRoom / BayTemplate
📖
IoT Control — How It Works
Click-by-click guide — works with any WiFi or GSM board
System Architecture
Your devices, the cloud Worker, and this browser all talk to each other in a loop. Here's the full picture.
┌─────────────────────────────────────────────────────┐ │ Your Devices (ESP32 / NodeMCU / commercial boards) │ │ │ │ Every 30 s → POST /api/iot/heartbeat │ │ Polls → GET /api/iot/commands/{id} │ └────────────────────────┬────────────────────────────┘ │ HTTPS / GPRS / LTE ▼ ┌─────────────────────────────────────────────────────┐ │ Cloudflare Worker (plantking.ape…) │ │ • Authenticates x-app-key header │ │ • Stores device state, commands in D1 (SQLite) │ │ • Runs automation rule eval on each heartbeat │ │ • Proxies camera MJPEG streams (CORS-safe) │ └────────────────────────┬────────────────────────────┘ │ GitHub Pages (HTTPS) ▼ ┌─────────────────────────────────────────────────────┐ │ This Browser — notes.tcplants.in │ │ • Polls Worker every 10 s to update device cards │ │ • Sends on/off/pulse commands (stored in D1) │ │ • Device fetches + executes command within 30 s │ └─────────────────────────────────────────────────────┘
Five things to know
1
Device ID — the unique identifier
Every device has a device_id (e.g. lab_relay_01). It's set in the firmware. Pick something short, lowercase, no spaces. Changing it later means re-registering the device.
2
x-app-key — the shared secret
All HTTP calls to the Worker use the X-App-Key header. Your key lives in Settings → IoT. The firmware must have the same key. Keep it private — anyone with this key can control all your devices.
3
Heartbeat = the device's clock
Every 30 seconds the device POSTs sensor readings to Worker. Worker marks it online and runs automation rules. Commands you send are queued in D1 — the device picks them up at the next heartbeat poll (within 30 s).
4
Connection mode — WiFi or GSM/4G
WiFi devices use Arduino's WiFiClient + HTTPClient. GSM boards use TinyGSM with the same HTTP calls. The Worker sees no difference — all it receives is JSON over HTTPS.
5
Panel toggle ≠ device off
The ○ Panel button only hides this UI. Devices, Worker, D1 and all automations keep running — they have no idea whether your browser is open. Closing the tab doesn't stop anything.
WiFi Devices — ESP32 / ESP32-S3 / NodeMCU (ESP8266)
First-time setup from zero: firmware studio → Arduino IDE → first heartbeat. Takes about 10 minutes.
1
Open Firmware Studio
Click 🔧 Studio in the IoT header. The full-screen editor opens.
2
Fill Device Identity
Pick a chip (ESP32, ESP32-S3, NodeMCU). Enter a Device ID (e.g. lab_relay_01), a human name (e.g. Lab Relay Board 1), and a room/bay (e.g. Lab Room 1). These appear in the device card.
3
Network — WiFi credentials
Enter your WiFi SSID and password. For lab/greenhouse devices use a 2.4 GHz network (ESP32 does not support 5 GHz).
4
Backend — Worker URL and IoT key
Pre-filled from Settings if you've set them. Worker URL is https://plantking.ape.workers.dev. IoT key is the x-app-key from Settings → IoT.
5
Add pins
Use the Pin Builder to add what's wired: relay (digital output), DHT22/DHT11 (temp+humidity), DS18B20 (temp), analog input, PWM, input. Assign the correct GPIO number for each.
6
Generate the sketch
Click ⚡ Generate. A complete .ino file appears in the right panel with all sections commented. Switch to the Editor tab if you need to make changes — it loads CodeMirror 6 for full C++ editing.
7
Install libraries in Arduino IDE
Go to Tools → Manage Libraries and install: ArduinoJson (by Benoit Blanchon). Optionally: DHT sensor library (Adafruit), DallasTemperature + OneWire (for DS18B20), Adafruit SSD1306 (for OLED). Copy the generated code into a new sketch.
8
Flash and watch Serial Monitor
Select your board and port, click Upload. Open Serial Monitor (115200 baud). You should see: WiFi connected → Device registered → Heartbeat sent. Within 30 s the device card appears in the IoT panel.
The device registers itself automatically on first boot. You don't need to "Add Device" manually — just flash and wait.
GSM / 4G Devices — Any cellular modem
Same protocol as WiFi — only the connection layer changes. Works with any board running TinyGSM: SIM800L, SIM900, SIM7600, A7670E, SIM7670E, and more.
Supported modems
ModemNetworkNotes
SIM800L2G GPRSBudget, very common; needs 4.2V 2A supply; no SSL hardware acceleration — use HTTP not HTTPS if unstable
SIM900 / SIM900A2G GPRSClassic reliable board; same caveats as SIM800L
SIM7600E / SIM7600G4G LTEFull LTE-FDD, India bands, stable; requires 5V 2A
A7670E (AI-Thinker)4G Cat-1LTE-M / Cat-1, good India coverage; 3.3–4.2V
SIM7670E / SIM7670G4G Cat-1Same as A7670E family; used on Waveshare ESP32-S3 board
Waveshare ESP32-S3-A7670E4G Cat-1ESP32-S3 + SIM7670E on one board; modem on UART1 (TX=17, RX=16); modem reset pin GPIO5
SIM7020 / BC66NB-IoTNarrowBand IoT — very low data rate; suitable only for sensors with infrequent heartbeats
India APN settings
OperatorAPNUserPassNotes
Airtelairtelgprs.com(blank)(blank)Use airtelnet.com if gprs doesn't work in your circle
Jiojionet(blank)(blank)4G only; SIM800L (2G) will not work on Jio
BSNLbsnlnetbsnlbsnlOr try prepaid.bsnl.in in some circles
Vi / Vodafonewww(blank)(blank)Or portalnmms in some circles
Ideainternet(blank)(blank)Now merged with Vi; try Vi APN if this fails
Jio is 4G-only (VoLTE). SIM800L and SIM900 are 2G modems and will not connect on Jio. Use Airtel or BSNL SIM for 2G boards.
External modem wiring (when not integrated)
ModemESP32 TXESP32 RXPowerExtra pins
SIM800LGPIO17 (UART2 TX)GPIO16 (UART2 RX)4.2V @ 2A peakGPIO5 → SIM800L RESET (optional)
SIM900GPIO17GPIO165V @ 2A
SIM7600EGPIO17GPIO165V @ 2APWRKEY → GPIO4 (toggle 200 ms to wake)
A7670E externalGPIO17GPIO163.3–4.2V @ 2ARESET → GPIO5 (optional)
Waveshare ESP32-S3GPIO17 (built-in)GPIO16 (built-in)USB-C 5VPWRKEY = GPIO5 (auto in firmware)
Steps — GSM setup
1
Open Firmware Studio → select GSM chip
Click 🔧 Studio. From the Chip dropdown choose the board that matches your hardware (e.g. ESP32-S3 + A7670E (4G)). GSM fields appear — WiFi fields hide.
2
Fill APN
Enter APN from the table above (e.g. airtelgprs.com). Leave User and Pass blank for Airtel and Jio. BSNL needs bsnl / bsnl.
3
Insert SIM with mobile data
Use a SIM with an active data plan. Even a basic 1 GB/month plan works — heartbeats are tiny (under 500 bytes each). Nano-SIM on most boards; check your board's SIM slot size.
4
Wire the modem (external boards only)
See wiring table above. For Waveshare ESP32-S3-A7670E the modem is on-board — just plug in USB-C. For external SIM800L: wire TX2/RX2, and provide a stable 4.2V 2A supply. A weak supply causes random GPRS disconnects.
5
Add TinyGSM library in Arduino IDE
Install TinyGSM (by Volodymyr Shymanskyy) from Library Manager. Also install ArduinoJson. The generated firmware handles the AT command sequence automatically.
6
Generate → Flash → watch Serial Monitor
Generate the sketch and flash at 115200 baud. Serial monitor shows: Modem init → SIM ready → GPRS attached → IP: 10.x.x.x → Device registered → Heartbeat sent. If it stops at "GPRS attached" check APN spelling.
The firmware automatically retries on GPRS drop. If signal is weak, reduce the heartbeat interval to 60 s so it reconnects faster after drops.
Commercial Devices — Tasmota · Shelly · Tuya
Add off-the-shelf smart switches, plugs, and relays that run Tasmota or Shelly firmware to the same panel as your custom ESP32 devices.
1
Connect device to your WiFi
Use the device's own setup app or access point to join it to the same network as this browser. Tasmota devices: connect to the tasmota-XXXX AP, enter your WiFi. Shelly: use Shelly Cloud app or Shelly AP.
2
Find the device's IP address
Use the 🔍 Scan button to auto-discover devices on your subnet (enter range like 192.168.1.0/24). Or check your router's DHCP table. Or use the Tasmota/Shelly app — it shows the IP.
3
Add via + Add Device → Commercial tab
Click + Add Device in the header, switch to the Commercial tab. Select device type (Tasmota / Shelly). Enter the IP address. Click Detect — the app queries the device's HTTP API and fills in name and status.
4
Or add from Scan results
When the scanner finds a Tasmota or Shelly device it shows an Add button directly on the scan card. Click it — the add form pre-fills with IP and detected type.
5
Save and control
Click Save. The device card appears in the panel. On/Off commands are sent directly to the device's local HTTP API — no Worker needed for control. Worker stores the last state.
Commercial devices are controlled via your local network from this browser. They don't send heartbeats to the Worker, so they won't trigger cross-device automation rules based on sensor readings. Use them for direct switching only.
Supported API types
TypeControl APIStatus API
TasmotaGET /cm?cmnd=Power+ONGET /cm?cmnd=Status+0
Shelly Gen 1GET /relay/0?turn=onGET /status
Shelly Gen 2/3POST /rpc/Switch.SetPOST /rpc/Switch.GetStatus
Firmware Studio — write, generate, and flash sketches
A full C++ Arduino IDE in the browser. Three tabs: Form (fill → generate), Editor (write/edit), AI (improve comments with Groq 70B).
1
Form tab — fill the template
Left panel: Device Identity, Network (WiFi or GSM), Backend (Worker URL + key), Intervals (read/heartbeat), Pin Builder, Features (OLED, OTA, DS18B20). Right panel updates live as a read-only syntax-highlighted preview.
2
Click ⚡ Generate
Generates a complete, compilable .ino with: branding header (By Shiva | Phyto Evolution Pvt Ltd), all required #includes, WiFi or TinyGSM connection, registerDevice(), sendHeartbeat(), pollCommands(), pin handlers, and OTA if enabled.
3
Editor tab — full C++ editing
Loads CodeMirror 6 with C++ syntax highlighting, line numbers, bracket matching, undo/redo, find/replace. Edit any part of the generated sketch. Changes sync back to the Form view's preview automatically.
4
AI ✦ tab — Groq 70B comment improvement
Toggle AI on (default off). Optionally add a prompt (e.g. "add detailed comments for a beginner"). Click ✦ Improve Code — sends to Groq llama-3.3-70b. Logic is never changed, only comments are deepened. Requires Worker with AI proxy configured.
5
Copy, Download, or Simulate
Copy puts the sketch in clipboard. ⬇ Download saves a .ino file. 🔬 Wokwi opens Wokwi (wokwi.com) with the code in clipboard — paste into a new ESP32 project to simulate wiring before flashing real hardware.
OTA (Over-the-air update) is enabled by default when you tick the OTA checkbox. After first USB flash, future updates can be pushed from the IoT panel's OTA tab without a physical USB cable.
Automation Rules — if/then, cross-device, server-side
Rules evaluate on every heartbeat inside the Cloudflare Worker. No browser required — automations run 24/7 even when you're offline.
1
Open a device → Rules tab
Click any device card to open its detail panel. Click the Rules tab. You'll see all rules for that device plus a + Add Rule button.
2
Set the IF condition
Select a sensor field (temperature, humidity, analog_0, etc.). Pick an operator (> < ==). Enter the threshold value (e.g. 28 for 28 °C).
3
Set the THEN action
Action: on, off, toggle, or pulse. Target: this device, or any other device (cross-device dropdown). Example: if Lab Temp > 28 °C → turn ON Fan Device.
4
Set hysteresis and cooldown
Hysteresis: dead-band that prevents rapid on/off cycling. E.g. hysteresis 2 on a >28 rule means it turns ON at 28 and won't turn back OFF until it drops below 26. Cooldown: minimum minutes between rule firings (prevents command spam).
5
Enable / Disable toggle
Each rule has an on/off toggle. Disabled rules are stored but never evaluated — useful for seasonal rules (e.g. winter heating rules you want to keep but not run in summer).
6
Global rules view — 🤖 Rules button
Click 🤖 Rules in the IoT header to see all rules across all devices in one panel. Filter by device. Enable/disable or delete from here too.
Rules fire inside Cloudflare Worker on each heartbeat — zero latency from the browser. Response time is: sensor reading → heartbeat POST → Worker eval → command queued → device polls within 30 s → pin changes state. Total lag: 0–60 s typically.
Camera Feeds — MJPEG streams + /cams portal
Add IP cameras (MJPEG or snapshot) to the IoT panel. All streams are proxied via the Worker to avoid CORS and mixed-content errors.
1
Add a camera
Click + Add DeviceCamera tab. Enter name, room, and either a MJPEG stream URL (ends in /mjpeg or /video) or a snapshot URL (JPG that refreshes). The Worker proxies the stream — enter the camera's local IP or public URL.
2
Show camera panel
Click 📷 Cams in the IoT header. Streams load only when the panel is on — saves bandwidth when you don't need them. The panel is a load gate, not a device toggle.
3
GSM-ESP32 camera streamer
For remote cameras (field, greenhouse perimeter) use an ESP32-CAM + SIM800L or A7670E. The ESP32-CAM serves a MJPEG stream locally; a second MCU or the ESP32-CAM itself POSTs a public ngrok/cloudflared URL to the Worker on registration. Add that URL as the camera stream URL.
4
/cams portal — for monitors and tablets
Open https://notes.tcplants.in/cams/ on any browser. Four rooms: Lab 1, Lab 2, Farm 1, Farm 2. Each room has its own password. Cameras grouped by room.
5
Set room passwords
In the camera panel, click the 🚪 Rooms button. Set a password for each room. Staff see only their room's cameras. Passwords are stored encrypted in D1.
6
Admin master token — bypass all room passwords
In Settings → IoT, set the admin master token. Entering this at the /cams login shows an All Rooms view — all cameras from all rooms in a grouped grid. For owners and supervisors who need full visibility.
Swarm Deploy — flash and register many devices at once
Setting up a new greenhouse or lab block? Swarm lets you define 3, 5, 10, or 20 devices in one form, generate all their sketches, download as a ZIP, and pre-register them all in D1 in one click.
1
Click ⚡ Swarm in the IoT header
The Swarm wizard overlay opens. One default device row is pre-seeded.
2
Fill project details and shared config
Enter a project name (e.g. Farm North Block) and location. Fill Worker URL and IoT key once — they apply to all devices. Select chip type and WiFi SSID/password (or APN for GSM swarm).
3
Add devices — row by row or in batch
Click + Row to add one device at a time. Or select a count (3/5/10/20) and a template, then click + Batch to add that many rows at once. Each row: device name, room/bay, template type.
4
Download ZIP
Click ⬇ Download ZIP. You get a .zip containing one .ino file per device, each with its device ID, name, and room baked in. Flash each file to the corresponding board via Arduino IDE or esptool.
5
Register All in D1
Click 💾 Register All in D1. All devices are pre-created in the database — they show up immediately as "offline" cards. When you flash and boot each board it transitions to "online" on first heartbeat.
6
GSM swarm — same flow, different transport
Tick Cellular board or choose a GSM chip. APN fields replace WiFi fields. All devices in the swarm get the same APN. Useful for deploying remote sensors across a farm with no WiFi.
After a swarm deploy you can push firmware updates OTA — no need to flash each board again via USB. One OTA queue entry per device from the IoT panel's OTA tab.
Devices
Online
Pending cmds
Automations
Alerts
Controls
Sensors
Timers
Automation
🔄 OTA
Cmd Log
Add / Edit Device
Pins / Channels
⚡ Firmware Flasher
1
Get Firmware Sketch
Download the Arduino sketch. Compile in Arduino IDE (ESP32 or ESP8266 board package). Commit the .bin to firmware/esp32/latest.bin or firmware/esp8266/latest.bin in your repo.
⬇ Download .ino sketch
2
Connect ESP via USB
Hold the BOOT button on your ESP while clicking Connect.
3
Detect Chip
Connect USB first.
4
Flash Firmware
Connect USB first.
5
Write WiFi & Device Config
IoT Key is injected automatically from your Worker settings.
IoT Help & Docs
🛒 What You Need
  • Board: ESP32 (recommended) or ESP8266 — any dev board with USB
  • Cable: USB-A to Micro-USB or USB-C (data cable, not charge-only)
  • Browser: Chrome or Edge only — Web Serial API is not supported in Safari or Firefox
  • Software: Arduino IDE 2.x for compiling firmware (one-time)
  • Power: 5V via USB or external supply for relay boards
ESP32ESP8266Chrome/Edge only
🔧 Arduino IDE Setup
Install these libraries via Tools → Manage Libraries:
LibraryByNeeded for
ArduinoJsonBenoit BlanchonAll — JSON parsing
DHT sensor libraryAdafruitDHT11 / DHT22 sensors
OneWirePaul StoffregenDS18B20 temp probes
DallasTemperatureMiles BurtonDS18B20 temp probes
Board packages (via Boards Manager):
  • ESP32: search esp32 by Espressif Systems
  • ESP8266: search esp8266 by ESP8266 Community
Open firmware/tcplants-iot.ino, select your board, compile. Export the binary via Sketch → Export Compiled Binary — commit the .bin to firmware/esp32/latest.bin or firmware/esp8266/latest.bin in the repo before using the browser flasher.
⚡ How the Browser Flasher Works
Click ⚡ Flash Firmware in the header. It runs a 5-step wizard entirely in your browser using the Web Serial API — no esptool, no command line.
1Prepare: Download the .ino source from GitHub. Compile in Arduino IDE and commit the .bin to the repo. The flasher fetches that binary at flash time — if it's not there, step 4 will fail.
2Connect USB: Plug the ESP into USB. Click "Connect USB Serial" — your browser shows a port picker. Select the ESP's COM/tty port. Baud rate is set to 115200 automatically.
3Detect chip: The app sends an ESP bootloader sync byte sequence and waits for a response to guess ESP32 vs ESP8266. If auto-detect fails (common), select the chip manually from the dropdown override.
4Flash firmware: Fetches firmware/esp32/latest.bin (or esp8266) from your GitHub repo raw URL, then writes it to the device in 512-byte chunks over serial. Progress bar fills as it writes.
5Write config: Sends your WiFi SSID, password, device name, location, and worker URL as a JSON packet over serial. Format: \x01CFG:{json}\x04. The device receives it, saves to EEPROM/Preferences, then reboots and connects to WiFi automatically.
⚠️ The browser flasher writes firmware in raw chunks — it is not the full esptool ROM protocol. It works for config writes and simple re-flashes after the device already has the bootloader. For a blank chip, use esptool or Arduino IDE for the first flash.
🔄 Device Lifecycle
1Boot: Device loads config from EEPROM (ESP8266) or Preferences (ESP32). Connects to WiFi.
2Register (first boot only): If no device_id is stored, sends POST /api/iot/register with name + location. Worker creates a row in D1 and returns an ID. Device saves the ID permanently.
3Load pin config: Fetches GET /api/iot/devices, finds its own entry, and loads the pin list (GPIO number, type, label). Sets up GPIO modes.
4Heartbeat (every READ_INTERVAL): sendHeartbeat() calls buildReadings() then POSTs {"device_id":"…","readings":[{"pin_id":"env_temp","value":26.5}…]} to POST /api/iot/heartbeat. Worker stores readings, evaluates automation rules against them, and returns any triggered commands. The device executes returned commands and confirms each via POST /api/iot/confirm/:cmdId. Timer-scheduled commands are polled separately via GET /api/iot/cmd/:deviceId every HBEAT_INTERVAL.
5Command poll (every 5s): Calls GET /api/iot/cmd/:deviceId. Worker returns pending commands and marks them as "received". Device executes each (ON/OFF/pulse/toggle) and confirms back via POST /api/iot/confirm/:cmdId with GPIO readback value.
Online = last heartbeat < 90s ago  Offline = no heartbeat for > 90s
📌 Pin Types Reference
TypeDirectionWhat it does
outputOUTDigital HIGH/LOW — LED, valve, buzzer
relayOUTSame as output — treated as ON/OFF switch. Cards show toggle buttons.
pwmOUTPWM output — fan speed, dimmer. Use action value 0–255.
inputINDigital read with INPUT_PULLUP — button, door sensor (0=triggered)
analogINAnalog read — moisture sensor, light sensor, potentiometer. ESP32: any ADC pin. ESP8266: A0 only.
dhtINDHT11 or DHT22 temperature + humidity. Requires Adafruit DHT library.
ds18b20INDS18B20 waterproof temp probe (OneWire). Good for nutrient solution temp.
GPIO numbers to avoid on ESP32: 6–11 (flash memory), 34–39 (input-only). Safe outputs: 2, 4, 5, 12–19, 21–23, 25–27, 32–33.
📡 Command Lifecycle
When you tap ON/OFF in the control panel, a command is created in D1 and tracked through 4 states:
StateMeaning
pendingCreated in D1, waiting for device to pick up
receivedDevice fetched it on its 5s poll
confirmedDevice executed it and reported GPIO readback ✓
failedDevice reported execution failure
Supported actions: on · off · toggle · pulse (500ms HIGH then LOW). The PWA polls for confirmation using backoff: 2→3→5→8→13→15s, up to 20 attempts.
🤖 Automation Rules
Rules are evaluated server-side on every heartbeat. Format: If [sensor pin] [operator] [threshold] → [action pin] [action]
FieldExample
if_pintemp sensor pin ID
operator> < >= <= ==
threshold28.5 (°C, %, raw value)
then_pinfan relay pin ID
then_actionon or off
cooldown_s300 — minimum seconds between firings
hysteresisOptional dead-band to prevent rapid toggling
Example: temp > 28°C → turn on fan. Cooldown 300s means it won't re-trigger for 5 minutes even if temp stays high.
⏱ Timers
Timers fire server-side when the device heartbeats at the matching minute. They don't fire if the device is offline at that exact moment.
  • Time: HH:MM in UTC (e.g. 01:30 = 7:00 AM IST)
  • Days: 7-character string 1111100 = Mon–Fri only (Mon=0, Sun=6)
  • Duration: Optional — if set, fires the opposite action after N seconds (e.g. turn on lights at 07:00, auto-off after 3600s)
  • Action: on or off
💡 IST is UTC+5:30, so subtract 5h30m from your local time to get the UTC value to enter. E.g. 6:00 AM IST = 00:30 UTC.
🔧 Config Over Serial (No Reflash)
After flashing firmware once, you can update WiFi credentials, device name, or worker URL without reflashing — just write a config packet over USB serial.
The protocol: send \x01CFG:{json}\x04 over serial at 115200 baud. The firmware detects the SOH byte (0x01), buffers until EOT (0x04), parses the JSON, saves to EEPROM/Preferences, and reboots.
Fields you can update: ssid · pass · worker_url · name · location. The device ID is cleared so it re-registers with the new name.
Step 5 of the browser flasher does this automatically — you don't need to send the packet manually.
🔴 Troubleshooting
ProblemFix
Device shows OfflineLast heartbeat was >90s ago. Check WiFi credentials and worker URL stored on device. Open Arduino serial monitor at 115200 to see boot logs.
Flash Firmware button missingChip detection failed. Use the manual chip dropdown override to select ESP32 or ESP8266, then retry.
Flash fails: "Firmware not found"The compiled .bin hasn't been committed to the repo yet. Compile in Arduino IDE → Sketch → Export Compiled Binary → commit the file to firmware/esp32/latest.bin.
Web Serial not availableYou're on Safari or Firefox. Open notes.tcplants.in in Chrome or Edge.
Commands stuck on "pending"Device isn't polling. Check it's online. Also verify IOT_KEY matches between the Cloudflare Worker env var and what's stored on the device.
Device registers every rebootConfig not saving. On ESP8266 check EEPROM size. On ESP32 check Preferences namespace. Serial logs will show "[IoT] Config saved" if working.
Automation not triggeringRules are evaluated on every heartbeat (every READ_INTERVAL, default 30s). Check the if_pin is a sensor that's actually sending readings. Check cooldown — rule won't re-fire within cooldown window.
🔧 Firmware Studio (v3.28)

In-browser IDE for writing, generating, and flashing custom .ino sketches for ESP32 / NodeMCU devices. Open via 🔧 Studio in the IoT header.

TabPurpose
FormDevice identity, network, pin builder, feature toggles. Code generates live. Presets strip above templates — save your own named configs for quick recall.
EditorFull CodeMirror 6 editor with multi-file tabs. main.ino + config.h by default. Add extra files (helpers.h etc.). Download as ZIP when multi-file.
AI ✦Enable AI Assist toggle → Groq llama-3.3-70b improves comments + readability. Logic never changed.
SerialLive serial monitor over Web Serial API (Chrome/Edge). Connect, choose baud (9600/115200/230400), read device output with colour-coded ERRORs + WARNs. Send commands to device.
FlashWrite WiFi/Worker credentials to device over serial without reflashing (config-write protocol). Or flash a .bin from URL in 512-byte chunks. Requires Chrome/Edge.

Ready-to-use example sketches are in firmware/examples/: greenhouse-relay, lab-temp-station, misting-controller, gsm-field-node, full-lab-station.

🔧 Firmware Studio untitled.ino
Presets
No presets yet — fill the form and click Save.
Templates
Device Identity
Network
Backend
Intervals
Pin Builder
Features
OLED Display (SSD1306)
OTA Updates (ArduinoOTA)
untitled.ino 0 lines

        
        
        
        
        
        
      

🌿 Choose Species

Hey Lab
Preview ⬇ Download
⏱ Lab Timers
--:--
⚡ Quick Log
🏛️ Lab — SARE, Thiruporur
real feel
💧
☀️UV —
🌧️Rain —
🌿 Greenhouse — Thaiyur
real feel
💧
☀️UV —
🌧️Rain —
📈 Climate History
Click Refresh to load.
🌿 Taxonomy ↗

Loading…

⚙️ Settings

Appearance
Theme
Dark or light mode
Dashboard layout
Drag panels to rearrange. Reset returns to default grid.
Ravana skin
Claude Design visual layer — gradient AI accents, enhanced button glow, richer depth. Experimental. Toggle off to revert instantly.
AI — Ravana
✓ Keys live in Cloudflare — API keys are server-side in the plantking Worker. Nothing to manage here.
Model
🌿
Forest Studio Labs
v2.0 · AES-256-GCM encrypted · GitHub-backed
Built for plant tissue culture labs.
Data never leaves your browser unencrypted.
⚡ Activity
tcplants@github — activity log
Loading…