Working…
Encrypted plant tissue culture lab. Sign in with your credentials to continue.
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.
Enter a bottle code to find all related bottles (same batch, session, or parent) and see which are contaminated.
Select a saved recipe → enter bottle count → get scaled ingredient totals.
Derived from your notes and accession registry.
Select a topic and toggle on to load papers from PubMed.
May 2026 · Encrypted · GitHub-backed · Single-file PWA
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.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.↑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.useTools:false removes ~4,700 tokens/request. _callAIExtract() lightweight path. 6-message rolling history, cleared on panel close.gemini-2.5-flash._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.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.get_iot_status, control_device (write+confirm), get_active_timers.bottle_ids[]).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.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.finance/settings.enc..page-head component (was unstyled in Supply, Sales, Files, AI). All full-page sections now show a consistent titled header bar.loadGallery() — no manual "Load Gallery" click needed.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..ino files in firmware/examples/.id for the upsert.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.DELETE /api/iot/devices/:id Worker endpoint performs cascaded cleanup.sensor_readings rows older than 7 days with 5% probability per heartbeat, keeping D1 storage bounded without a separate cron job.pin to pin_id, gpio lookup via new PINS[].id field. Multi-channel sensor sub-channels in automation dropdown. Commercial device 🔄 Poll button.rootTask on Core 1 with OTA update support, recovery AP mode, and automatic rollback if new firmware crashes before initialisation completes..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.X-App-Key header added to Worker CORS allow-list.sales/sales-log.csv (plain text, one row per line item). Visible and editable in Files → Sales Log.attachments/<section>/general/. Drop hint highlights the target folder._normalizeGrade().size_grade column added.?gh=id deep link.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
| Element | What it does |
|---|---|
| Token field | Your 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 cache | If session cache is fresh (<10 min), data loads instantly without re-fetching from GitHub. |
| Button | Action |
|---|---|
| ☰ Menu | Open/close the navigation drawer |
| 🏠 Home | Return to the dashboard from anywhere |
| 🌡️ Weather chip | Current feel-like temp at lab & greenhouse. Click to expand details. |
| 📷 QR Scan | Open camera scanner — auto-routes to bottle or note on match |
| ⬇ ZIP | Export all notes as decrypted Markdown + accessions & recipes as JSON |
| CSV | Export notes and accessions as spreadsheet files |
| 🔒 Lock | Wipe token & passphrase from memory, return to login |
| ⚙️ Settings | Lab name, AI key, scan options, cache & device management |
| ☀️/🌙 Theme | Toggle dark/light mode |
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.
| Field | Required | Purpose |
|---|---|---|
| Title | ✓ | Shown in the drawer list and search results |
| Experiment Type | Categorises the entry; auto-loads a Markdown template into the body when empty | |
| TC Stage | Stage 0–IV, Self Notes, or N/A. Used in analytics, filters, and the stage funnel chart | |
| Species / Accession | Links to Registry accessions automatically. Supports taxonomy autocomplete + Browse modal | |
| Recipe Used | Pick a saved recipe — shows as a purple badge; powers recipe usage analytics | |
| Next Review Date | Creates a reminder in ⏰ Reminders with nav badge count | |
| Tags | Comma-separated. Autocomplete from existing tags. Powers the tag cloud in Analytics | |
| Cultures Out | Number of propagules produced. Used in media efficiency score (Avg Out column in Analytics) | |
| Body | Full Markdown — supports headings, lists, code blocks, tables, images, wiki-links |
| Button | View |
|---|---|
| ✏️ Edit | Textarea only — full width for writing |
| ⬛ Split | Editor left + live Markdown preview right (stacks on mobile) |
| 👁 Preview | Rendered view only — click any wiki-link to navigate |
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.
| Shortcut | Action |
|---|---|
| Ctrl+S | Save note to GitHub |
| Ctrl+B | Bold selected text |
| Ctrl+I | Italic selected text |
| Ctrl+K | Insert markdown link |
| Tab | Insert 2 spaces |
| Enter in search | Deep full-text search across all note bodies |
| Escape | Close Quick Log / Wiki picker |
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.
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.
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.
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.
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.
| Pill | Shows |
|---|---|
| All | Every entry |
| ☣️ | Contamination Log entries only |
| ⏰ | Entries with overdue review dates |
| I / II / III / IV | Entries at that TC Stage |
Track every plant accession — its origin, status, and full lab history. Any note whose Species field matches the accession species is automatically linked.
| Field | Purpose |
|---|---|
| Species | Scientific name — links to taxonomy autocomplete |
| Accession No. | Your internal identifier (e.g. VAN-001, NEP-RAJ-2024) |
| Origin | Geographic 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 acquired | When you received or collected it |
| Status | See status table below |
| Flairs | Coloured provenance labels (clone, form, etc.) |
| Notes | Free markdown — phenotype, permits, contacts |
| Status | Meaning |
|---|---|
| Active in culture | Currently propagating in vitro |
| In acclimatization | Stage IV — hardening off ex vitro |
| Dormant | Paused, not actively subcultured |
| Transferred out | Moved to another lab or collaborator |
| Lost | Culture failed or fully contaminated |
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.
Store complete, reusable media formulations. Each recipe records enough detail to reproduce the exact medium at any time.
| Field | Details |
|---|---|
| Name | Free text — shown in all recipe dropdowns throughout the app |
| Base Medium | Curated list: MS, LS, Knudson C, VW, Pierik, New Dogashima, B5, N6, MS-Musa, WPM, DKW, Anderson's, or Custom |
| Sucrose | g/L |
| Gelrite | g/L — defaults to 1.9 g/L on new recipes (formerly labelled "Agar/Gelrite") |
| pH (before autoclave) | Numeric |
| Growth Regulators | Free text — e.g. "BAP 1 mg/L, NAA 0.1 mg/L" |
| Autoclave params | Presets: 121°C/15m, 121°C/20m, 121°C/45m, 115°C/20m, Filter sterile — or type manually |
| Target species | Optional — creates a clickable link to the matching accession |
| Flairs | Optional coloured labels — tag recipes by stage, type, or workflow (e.g. "Stage II", "Rooting", "High PGR") |
| Preparation notes | Full Markdown — procedure steps, amendments, observations |
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.
| Mode | When to use | Creates |
|---|---|---|
| + Single | One specific bottle with full detail | 1 bottle |
| ⊞ Batch | N identical bottles, same media, same species and stage | N bottles sharing one batch_id |
| 🔥 Session | One autoclave run with multiple media batches for different species/stages | All bottles at once, linked by session_id |
| Field | Purpose |
|---|---|
| Species | Taxonomy autocomplete + Browse modal |
| Accession | Link to the Registry record for this species |
| Recipe | The media formulation used — links to Recipe Book |
| Stage | Stage I – IV, Mother Block |
| Status | See lifecycle below |
| Date prepared / inoculated | Timestamps for preparation and inoculation events |
| Media volume | mL per vessel |
| Explants per bottle | Optional integer |
| Next review date | Creates a reminder in ⏰ Reminders |
| Linked note | Associate this bottle with a lab note entry |
| Source greenhouse plant | Link to the GH stock plant the explant came from |
| Flairs + provenance note | Clone/form labels and free text for traceability |
| Notes | Markdown observations |
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.
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.
Every bottle detail shows a QR code. Before printing, choose a label size:
| Size | Use case |
|---|---|
| 58mm | Standard narrow thermal roll (Brother, Dymo, Zebra ZD220) |
| 62mm | Wider thermal roll — more space for species name |
| A4 sheet | Print 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.
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 Field | Purpose |
|---|---|
| Session date | Date of the autoclave run |
| Autoclave capacity | Total bottles your autoclave holds (default 120) |
| Temp / Time | Autoclave 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 bar | Live bar turns yellow ≥90 %, red if over capacity |
| Session notes | Optional 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.
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.
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.
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.
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.
| Filter pill | Shows |
|---|---|
| All | Every bottle |
| Prepared | Media made, not yet inoculated |
| Inoculated | Explants placed |
| Growing | Active cultures |
| Contam | Contaminated vessels |
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.
| Field | Purpose |
|---|---|
| Species | Taxonomy autocomplete + Browse modal. Hybrids (×) fully supported. |
| Quantity | Number of plants — tracked via the Quantity Ledger tab. |
| Health status | Healthy 🟢 / 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. |
| Source | 12 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 added | Logged as the first entry in the Quantity Ledger. |
| Greenhouse location | Bench, 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 date | Appears in ⏰ Reminders (🏡 icon). Overdue shows red badge in list; due ≤7 days shows yellow. |
| Linked accession | Optional link to Accession Registry record. |
| Notes | Markdown — phenotype, provenance, permits. |
| Tab | Contents |
|---|---|
| 📋 Overview | Core info table, Take Explant buttons, linked bottles list, notes body |
| 📦 Ledger | Full stock movement history as a running-balance table. + Adjust opens an inline form to log any change. |
| 🩺 Health | Timestamped condition timeline with coloured dots. + Log opens an inline form (health dropdown + date + observation text). |
| ⚗️ Plan | Production 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. |
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 type | When used |
|---|---|
| Received | New plants arrived — wild, nursery, exchange |
| Loss | Plants died, damaged, or lost |
| Sent out | Sold, gifted, or transferred to another party |
| Natural propagation | GH plant produced new plants on its own |
| Lab output | TC 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.
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.
Toggle between list view and bench view using the ≡ / ⊞ buttons in the list panel header.
| View | How it works |
|---|---|
| ≡ List | Standard sorted list with species, quantity, health dot, inspection badge |
| ⊞ Bench | Plants 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.
The Plan tab computes production capacity from your actual data:
| Stat | Source |
|---|---|
| GH stock | Current plant count |
| In culture | Bottles for this species in inoculated or growing status |
| Ready for GH | Subcultured bottles — lab output waiting to transfer back |
| Avg harvest yield | Average explants per event from explant history |
| Projected bottles | Stock × avg yield if all plants harvested |
| Sessions needed | Projected bottles ÷ 120 (autoclave capacity) |
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.
| Badge | Meaning |
|---|---|
| Red OVERDUE | Inspection date has passed |
| Yellow INSPECT | Due within 7 days |
| 🔔 chip in header | Exact days until / past inspection |
Click ⬆ CSV in the list panel header to open the import modal. Three ways to get data in:
| Method | How |
|---|---|
| 📂 Upload any file | Click "Upload any file" — accepts CSV, TSV, TXT, or any text format. No file-type restriction. |
| ⬇ Fetch from URL | Paste any public CSV link (e.g. Dropbox, raw GitHub). Google Sheets edit URLs are auto-converted to /export?format=csv. Click Fetch. |
| Paste data | Copy 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.
| Column | Required | Notes |
|---|---|---|
| species | ✓ | Scientific name or hybrid. Aliases: plant, name, scientific_name |
| quantity | Integer, default 1. Alias: qty, count | |
| health | healthy · stressed · dormant · declined (default: healthy). Alias: status | |
| size_grade | sapling · s · m · l · xl · xxl (default: s). Aliases: size, grade | |
| source | greenhouse · wild · lab · nursery · trade · own-cultivar · sister-lab · swap · commercial · botanical-garden · donated · rescue · seeds (default: greenhouse) | |
| location | Bench label — used for bench view grouping. Alias: bench | |
| next_inspection | YYYY-MM-DD. Alias: inspection_date | |
| accession_number | Stored as a reference note; does not auto-link to Registry. Alias: accession | |
| notes | Free 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.
A system flair named GH (emoji 🏡, colour green) is automatically created the first time you:
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.
| Button | What happens |
|---|---|
| 🌿 Take Explant → Bottle | Prompts 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 → Session | Same logging. Opens Autoclave Session builder with species in first batch row. |
Every bottle detail has a 🏡 Send to GH button. On tap:
| Where | What 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 strip | Healthy plants as chips; yellow "lab output ready" alert when subcultured bottles exist |
Always accessible via the 🏠 Home button. The dashboard is a real-time lab overview rebuilt around biological workflows.
| Button | Action |
|---|---|
| + New Entry | Open full lab note editor |
| 🫙 New Bottle | Open bottle editor (Single mode) |
| 🏡 GH Plant | Open greenhouse plant editor |
| 🧫 Sterilise | Jump to Sterilisation Lab |
| 📷 Scan QR | Open QR scanner in route mode |
| 📊 Analytics | Navigate to analytics |
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.
| Card | What it shows |
|---|---|
| ⚗️ TC Stage Distribution | SVG donut chart of all bottles by stage. Each segment is clickable — jumps to that filtered bottle list. Legend alongside. |
| ☣️ Contamination Heatmap | 14-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 Window | Large 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. |
| Column | Contents |
|---|---|
| 📊 7-day Activity | Bar chart of notes created per day for the last 7 days |
| ⏰ Reviews Due | Notes + bottles due within 14 days, urgency-sorted. Red = overdue, yellow = ≤3 days. |
| 📋 Recent Entries | 5 most recent notes — click to open |
Up to 8 species ranked by combined note + bottle activity. Each shows 📋 note count and 🫙 bottle count. Click any → Species hub.
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).
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.
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.
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).
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).
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.
Click 🌱 Species in the nav drawer. A live table lists every species found in your notes, accessions, and bottles.
| Column | Meaning |
|---|---|
| Species | Scientific name (bold if taxonomy pack has data) |
| Notes | Total lab notes for this species |
| Active bottles | Inoculated + growing bottles |
| Stage spread | Mini stage pills showing active work distribution |
| Contam rate | % of notes that are contamination logs |
| Avg out | Average "Cultures Out" across notes — proxy for multiplication rate |
Click any species row to open the detail panel showing:
| Section | Contents |
|---|---|
| Summary | Notes, active bottles, contam rate, avg cultures out |
| TC Stage Activity | Horizontal bar chart across Stage I–IV |
| 🏡 Greenhouse Plants | All GH entries for this species with health dots, quantity, and source. "+ Add plant" shortcut. |
| Accession Records | Registry entries — click to navigate |
| Bottles | Up to 8 bottles with status dots — click to open. "View all" link. |
| Notes | Most recent 10 lab notes — click to open |
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.
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.
| Field | Purpose |
|---|---|
| Scientific Name | Required. Any format — species, hybrid name, working name. Names with × are auto-detected as hybrids. |
| Common Name | Optional display name |
| Source / Origin | 12 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 Status | LC · NT · VU · EN · CR · DD · CV (Cultivar) — shows as a coloured badge in autocomplete. CV appears purple. |
| Flairs | Required — select at least one flair to label this species (provenance, clone ID, form, etc.). Flairs must be defined first in the Flairs tab. |
| Subculture Interval | Days between subcultures for this species (e.g. 28 = monthly). Auto-generates ✂️ subculture reminders in ⏰ Reminders when active bottles approach that age. |
| Photo | Optional — 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 Lookup | After 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.
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.
| Category | Colour | Examples |
|---|---|---|
| Provenance | Blue | 🌍 Wild collected · 🏔 Highland ecotype · 🤝 Exchange material |
| Conservation status | Red | 🔴 CR · 🟠 EN · 🟡 VU · ⚠️ CITES · 🏛 Type specimen |
| Geography / Ecotype | Green | 🌏 Borneo · Western Ghats · Sri Lanka endemic · Himalayan form |
| Culture performance | Purple | ⭐ Elite clone · ⚡ Fast multiplier · ✅ Certified clean |
| Plant trait | Yellow | 🌸 Flowering selected · 🍃 Variegated · 💪 Vigorous growth |
| Media / Recipe tag | Orange | 🧪 MS-based · ✂️ Multiplication medium · 🌱 Rooting medium |
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.
| Group | Condition | Colour |
|---|---|---|
| 🔴 Overdue | Date has passed | Red |
| 🟡 This week | Due within 7 days | Yellow |
| 📅 This month | Due within 30 days | — |
| 🗓 Later | More than 30 days away | — |
| Icon | Type | On tap |
|---|---|---|
| 📋 | Lab note | Opens the note |
| 🫙 | Bottle (active only) | Opens the bottle detail |
| 🏡 | Greenhouse plant inspection | Navigates to Greenhouse → opens that plant |
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.
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 / Card | What it shows |
|---|---|
| Notes / Month | Line chart — entries created per calendar month (last 6 months) |
| Stage Distribution | Donut — proportion of notes at each TC stage (I–IV, Contam, Unclassified) |
| Species Success Rate | Horizontal bars — % of non-contamination notes per species (≥2 notes required). Green >70%, yellow 40–70%, red <40% |
| Contamination Trend | Line chart — contamination entries per month (last 6 months) |
| Recipe Usage | Bars — how many notes reference each recipe (top 6) |
| Tag Cloud | 30 most-used tags as sized pills — larger = more frequent |
| Stage Funnel by Species | Stacked bars for top 5 species — Stage I → II → III → IV distribution |
| Avg Days Between Entries | Badge cards per species (≥3 notes) — approximate subculture interval |
| Media Efficiency | Ranked table: Recipe × Species × Stage. Score = (Avg Out / global avg) × (1 − contam rate) × 100 |
| Culture Lifecycle Status | Active 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 Usage | Bars — section visit frequency since tracking began (reset in Settings) |
Monthly view of all lab activity. Navigate with ‹ › or Today.
| Dot colour | Meaning |
|---|---|
| Blue | Lab entry created on this day |
| Yellow | Review due on this day |
| Red | Contamination entry on this day |
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.
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.
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.
| Action | How |
|---|---|
| Add alarm | Enter a time (HH:MM) and optional label → click + Add. Alarm appears in the list immediately. |
| Toggle on/off | Click the toggle switch on any alarm card. Off alarms are greyed out and will not fire. |
| Delete alarm | Click ✕ on the alarm card. |
| Dismiss | When 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).
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.
| Tool | How to use |
|---|---|
| Countdown Timers | + Add Timer → enter h/m/s + name → ▶ Start. Up to 5 simultaneous. Beeps on zero. ↺ Reset. |
| Stopwatches | Up to 4 simultaneous. ⏱ Lap records a split without stopping. ↺ resets. |
| 🧫 Sterilisation Stepper | Now a dedicated page — click Open Sterilisation Lab → from Tools, or tap 🧫 Sterilise in the nav. See section below. |
| Media Volume Scaler | Enter base volume + target volume + ingredients (one per line: "name: amount unit"). Click Scale → proportional quantities shown immediately. |
| BioChemistry Calculator | Molarity (MW/mass/volume), Dilution (C₁V₁=C₂V₂), % Solution (w/v), pH from [H⁺] or pOH, PGR converter (mg/L ↔ μM using molecular weight) |
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 action | Result |
|---|---|
| Click chip label | Navigate to Tools and restore the widget card |
| Click ✕ on chip | Close chip, restore card to Tools (timer keeps its state) |
| Timer chip | Shows live countdown or 🔔 when done |
| Stopwatch chip | Shows live elapsed time while running |
| Stepper chip | Shows 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.
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.
10 biologically accurate preset protocols, grouped by explant category:
| Category | Protocols |
|---|---|
| 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.
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.
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.
| Control | Action |
|---|---|
| + Step | Add a blank step to the current protocol |
| ▶ Start Protocol | Begin the timed walkthrough — each step counts down, beeps on completion, auto-advances |
| ⏸ Pause / ▶ Resume | Pause the active step timer |
| ↺ Reset | Return to step 1, clear active state |
| ▂ Minimise | Collapse to footer tray — stepper keeps running |
| Field | Purpose |
|---|---|
| Step name | Displayed as the active step label during the run |
| Duration (seconds) | Countdown timer for this step |
| Temperature (°C) | Optional bench reference — shown during the step |
| 📝 Note | Bench notes shown during the active step — technique reminders, cautions |
Tap 📷 in the top nav bar from any screen — opens the scanner in route mode.
| Scanned content | Result |
|---|---|
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 barcode | Shows decoded text with Copy and Open URL options |
The same scanner opens from the 📷 toolbar button inside the note editor for inserting barcodes inline.
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.
Fixed bottom-right bar visible after login — all AI and voice controls in one place:
| Button | Action |
|---|---|
| 🎤 | Voice input — tap to start, tap again to stop. Pulses red while listening, yellow while processing. |
| Model badge | Tap to cycle: ⚡ Quick → 🔬 Deep → ◇ Claude |
| ✦ | Open / close AI chat panel |
| + | Quick log (fast note) |
| Model | Use for |
|---|---|
| ⚡ Quick — Llama 3.1 8B | Voice chat, simple questions. Default for all voice input. Free, fast. |
| 🔬 Deep — Llama 3.3 70B | Complex protocols, multi-species analysis, detailed troubleshooting. |
| ◇ Claude | Hardest reasoning, undescribed species, nuanced protocol design. |
All keys encrypted with your passphrase and stored in GitHub — enter once, auto-loads on any device.
| Tab | Shows |
|---|---|
| 💬 Chat | Text conversation, quick-action chips, send field |
| 🎤 Voice | All voice session transcripts with timestamps — accumulates even when panel is closed |
| Mode | How 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. |
| Say | Does |
|---|---|
| "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 |
"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.
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.
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.
When a species note is open, the 3 most recent PubMed abstracts for that species + "tissue culture" are injected automatically.
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.
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.
| Chip | PubMed query covers |
|---|---|
| 🌿 TC & Micropropagation | Plant tissue culture, micropropagation, in vitro propagation, plant cell culture |
| 🪲 Carnivorous Plants Micro | CP + TC/micropropagation terms |
| 🪴 Carnivorous Plants | Nepenthes, Drosera, Sarracenia, Dionaea, Pinguicula, Heliamphora (broad) |
| 🏔 Nepenthes | Nepenthes specifically + TC terms |
| 🌸 Orchids | Paphiopedilum, Dendrobium, Phalaenopsis, Vanilla, orchid + TC terms |
| 🧬 Plant Mutation | Somaclonal variation, mutagenesis, in vitro mutation |
| 🍌 Banana / Musa | Musa, banana, plantain + TC terms |
| 🌼 Vanilla | Vanilla planifolia / Vanilla spp. + in vitro |
| 🔬 Somatic Embryogenesis | Somatic embryogenesis + plant |
| 🌱 Organogenesis | Organogenesis, shoot organogenesis, plant regeneration |
| 🧫 Callus Culture | Callus induction, callus culture, dedifferentiation |
| 🌿 Rooting | In vitro rooting, auxin rooting, root induction |
| 💉 PGRs | Plant growth regulators, cytokinin, auxin, BAP, NAA, TDZ + TC |
| 🧼 Sterilisation | Surface sterilisation, sodium hypochlorite, explant sterilisation |
| ☣️ Contamination | Fungal/bacterial contamination in tissue culture |
| ⚗️ Media | Murashige Skoog, culture media, macro/micro nutrients |
| ❄️ Cryopreservation | Plant cryopreservation, vitrification, slow cooling |
| ✏️ Custom | Type any PubMed query — free-form search |
Each article card shows: PMC PDF link (when available), DOI link, abstract toggle, and 📌 pin button.
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.
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.
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.
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.
| Icon | Commit 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.
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.
| Field | Purpose |
|---|---|
| Name | Reagent or item name (e.g. "Murashige & Skoog salts 10L kit") |
| Category | Media Salts · Growth Regulators · Gelling Agents · Sugars & Vitamins · Sterilisation · Glassware & Plastics · Equipment · Other |
| Quantity + Unit | Current 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. |
| Location | Where it's stored: Fridge, -20°C freezer, Shelf B3, etc. |
| Expiry Date | Shown in red when within 30 days. Useful for hormones, enzymes, antibiotics. |
| Notes | Supplier name, catalogue number, lot number, preparation notes |
Click ✏️ Edit on any item → +/− Adjust stock. Enter a positive number (received new stock) or negative (consumed). The current quantity updates immediately.
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.
Click ⚙️ in the top bar. All changes persist in localStorage.
| Setting | Effect |
|---|---|
| Theme | Dark / Light mode toggle |
| Lab Name | Shown in the dashboard header |
| Weather Location | City name for the weather widget |
| AI Keys | Groq (free default) + Claude (optional). Both encrypted in GitHub — enter once, auto-loads on any device. |
| Scan Enhancement | Adds contrast/brightness sliders in camera scan mode |
| Try Harder Mode | Slower but more thorough QR/barcode decoder |
| Clear Local Cache | Forces a full reload from GitHub on next login |
| Forget Trusted Device | Removes the encrypted saved token from this device |
| Reset Usage Stats | Clears section-visit counts shown in Analytics |
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.
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 mode | How it works |
|---|---|
| Native | Words appear live as you speak — final transcript appended to entry |
| Whisper | Records audio → transcribes via Cloudflare proxy → appended to entry |
Tap ✨ Parse with Ravana at the bottom of any entry. Ravana reads it and returns action cards:
| Found | Action card | What 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.
Unparsed entries (● grey) are automatically parsed in the background:
lab-summary.json is updatedOpen 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.
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.
Scroll down below the spec table to see the Culture Lifecycle strip:
| Source | Priority |
|---|---|
| Historical bottles of same species (avg days inoculated → subcultured) | Highest — requires ≥2 completed bottles |
Taxonomy species subculture_interval_days setting | Medium |
| Stage defaults (inoculated: 14d, growing/subcultured: 28d, rooting: 21d…) | Fallback |
| Status | Condition |
|---|---|
| 🟢 On track | Less than 70% of expected duration elapsed |
| 🟡 Due soon | 70–100% of expected duration |
| ⏰ Transfer due | 100–150% of expected |
| 🔴 Delayed | More than 150% of expected — needs attention |
Analytics section → Culture Lifecycle Status card. All active bottles grouped by status with counts and clickable rows that navigate directly to the bottle.
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.
| Alert | Condition | Goes to |
|---|---|---|
| ☣️ Contamination spike (red) | Any species with ≥3 notes in last 30 days and >10% contamination rate | Contam section |
| ⏰ Overdue reviews (yellow) | Any active bottle or note with a past-due next_review date | Reminders |
| ✂️ Transfer due (yellow) | ≥5 active bottles past 28 days since inoculation | Bottles |
| 🏡 GH attention (yellow) | Any greenhouse plant with health ≠ healthy | Greenhouse |
| 📦 Supply expiry (red if expired) | Any supply batch expired or expiring within 14 days | Supplies |
Select multiple bottles and update all of them in a single save — useful after an autoclave run or an end-of-day subculture pass.
| Action | Result |
|---|---|
| Tap checkbox on a bottle row | Toggles 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 selected | Deselects all |
Once ≥1 bottle is selected, an action bar slides up from the bottom of the screen:
| Button | Sets status to |
|---|---|
| ✂️ Subcultured | subcultured — also records today as last_transfer date |
| 🌿 Growing | growing |
| 🌱 Rooting | rooting |
| ☣️ Contaminated | contaminated |
| 🗑 Discard | discarded |
| ✕ Clear | Deselects all, hides bar |
All changes are saved to GitHub in a single write after confirmation.
Nav drawer → Schedule. A single view showing everything that needs action, sorted by urgency.
| Section | Shows |
|---|---|
| ⏰ Overdue | Bottles and notes where next_review date has passed. Days overdue shown in red. Sorted: most overdue first. |
| 📅 Due this week | Bottles and notes with next_review within 7 days. Sorted by days remaining. |
| ✂️ Transfer due | Active bottles that have been inoculated for ≥28 days. Shows days since inoculation. |
| 🏡 GH attention | Greenhouse 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.
| Source | Fields indexed | Notes |
|---|---|---|
| 📋 Notes | Title, species, tags, body preview, keyword stems | Body keyword index extracted at save time (250 words) — full content searchable without API calls |
| 🫙 Bottles | Code (FSL-YYYY-NNNN), species, stage, status, notes, flair note | Status: growing/inoculated/contaminated etc. |
| 🌿 Accessions | Species, accession number, source, origin, notes | — |
| 🧪 Recipes | Name, base medium, target species, PGR text, prep notes | PGR concentrations searchable |
| 🏡 Greenhouse | Species, source, location, health, notes | — |
| 📓 Journal | Title, body, tags | New in v3.11 |
| 📦 Supplies | Name, unit, notes | New in v3.11 |
| 🌱 Species | Scientific name, common name, category, source | Custom taxonomy — new in v3.11 |
| Feature | How to use |
|---|---|
| Multi-word AND | All words must appear somewhere — "rajah growing" finds Nepenthes rajah bottles with growing status |
| Relevance ranking | Title match = 3pts, species = 2pts, tags = 2pts, body = 1pt — best matches appear first in each section |
| Term highlighting | Matched words highlighted in yellow across all result rows |
| Field prefixes | Click the prefix chips or type them: species: stage: status: code: tag: health: location: |
| Recent searches | Last 10 queries shown when the search box is empty |
| Saved filters | Click 💾 Save to name and pin a search — appears as a chip at the top. Up to 20 saved. |
Nepenthes rajah growing · species:Drosera stage:II · status:contaminated FSL-2026 · BAP 1 mg (finds recipes with that PGR) · tag:transfer location:bench3Results are grouped by section with colour-coded headers. Tap any result to navigate directly to that record. Min 2 characters to trigger search.
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.
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.
| Action | Does |
|---|---|
| + Use 1 | Increments used count by 1 and saves |
| Edit use | Set the used count to any number (for bulk corrections) |
| ✕ (top right) | Delete the batch — prompts for confirmation |
| Colour | Meaning |
|---|---|
| Green | >50% remaining, not expiring soon |
| Yellow | <50% remaining, or expiring within 14 days |
| Red | Depleted or expired |
Expiry warnings also appear in the Dashboard Alert strip before they become critical.
| Mode | Contents |
|---|---|
| 📥 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 |
| Export | Contents |
|---|---|
| ⬇ ZIP (top bar) | All notes as decrypted Markdown + accessions + recipes as JSON |
| CSV (top bar) | Note metadata CSV + accessions CSV |
| 🖨 Print / PDF | Open any note or recipe → browser print dialog → Save as PDF |
| 📅 Calendar export | This 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.
Nav drawer → 💰 Value. Estimates your current lab inventory value from bottles + greenhouse stock using configurable prices.
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.
Live calculated total: Σ(bottles by stage × stage price) + Σ(GH plants × grade price × quantity). Full breakdown by category with species-level detail.
| Setting | Where |
|---|---|
| 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 card | Shows compact total (₹12k / ₹1.2L format), navigates to Value page |
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.
| Location | Metrics |
|---|---|
| 🏛️ Lab — SARE, Thiruporur | Real feel °C (large) · dry °C (small) · 💧 Humidity % · ☀️ UV Index · 🌧️ Rain probability % |
| 🌿 Greenhouse — Thaiyur, Kelambakkam | Same metrics |
Click the chip to expand the two-location popover. Click anywhere outside to close.
| What | How it works |
|---|---|
| Encryption | AES-256-GCM. Key derived via PBKDF2 (SHA-256, 600,000 iterations). Unique salt per save. |
| Where data lives | Only in your GitHub repo as .enc ciphertext. Even repo admins cannot read it without your passphrase. |
| Passphrase | In-memory only. Never sent to any server. Lost on lock or tab close. |
| Login CAPTCHA | Inline 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 token | sessionStorage only (cleared on tab close) unless Trusted Device is enabled — then stored in localStorage as token encrypted with your passphrase. |
| Session cache | Decrypted data cached in sessionStorage for 10 minutes for fast re-unlock. Cleared on lock and tab close. |
| Trusted Device | Token encrypted with your passphrase and stored in localStorage. Remove via Settings → Forget this device. |
System-wide dependency map. Search by function name, concept, or section. Expand any block to test it.
S — all fields| Field | Loaded by | Read by | GitHub file |
|---|---|---|---|
S.notes[] | login → loadNotesIndex() | renderNoteList, openNote, LabAnalytics, _buildAIContext, Reminders, Dashboard, Search | notes/index.enc + notes/{id}.enc |
S.accs[] | login → loadAccessions() | renderAccList, openAccession, openBottle, Search | accessions/data.enc |
S.recs[] | login → loadRecipes() | renderRecList, openRecipe, bottle form, Search | recipes/data.enc |
S.bottles[] + S.sessions[] | login → loadBottles() | renderBottleList, openBottle, Dashboard, Reminders, Analytics, Species, Search | bottles/data.enc |
S.greenhouse[] + S.ghTransfers[] | login → loadGreenhouse() | renderGHList, openGHPlant, Dashboard, Reminders, Search | greenhouse/data.enc |
S.supplies[] | login → loadSupplies() | SupplyInventory.renderInventoryPage, calcPerformance, Dashboard low-stock | supplies/data.enc |
S.journal[] | login → loadJournal() | renderJournalList, overnight parser, _buildAIContext | journal/entries.enc |
S.value | login → loadValue() (8th load) | renderValueSection, vault calculation | value/data.enc |
S.taxSpecies[] | post-login loadTaxonomy() | _taxSuggest, species picker, _buildAIContext, Species section | taxonomy/*.json packs (read-only) |
S.taxCustom | loadTaxonomy() → ghGet | Species custom tab, _taxSuggest, flairs, GBIF imports | taxonomy/custom.enc |
S.analytics | LabAnalytics.calculate() at login + after saves | _buildAIContext only (renderAnalytics recalculates inline) | — (memory only) |
S.weather | fetchWeather() at login, every 60s | weather-chip display, _buildAIContext | — (Open-Meteo API) |
S_inv (separate object) | loadInv() on Stock section visit (not at login) | renderInv, saveInv | inventory/data.enc |
S.cur / S.editing / S.curBot / S.curGH | openNote, openBottle, openGHPlant | detail renderers, save actions (edit vs create branch) | — (UI state) |
S.bottleFilter / S.bottleSearch / S.botBatchFilter | setBotFilter, viewBatch, search input | renderBottleList (priority: batchFilter → statusFilter → search) | — (UI state) |
S.section | switchSection() | navTo, switchSection, mobile sidebar checks | — (UI state) |
_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.
S.tok = S.pw = tok. If DEVICE_KEY in localStorage → restore token./repos/Phyto-Evolution/tcplants → verify access.dec() throws OperationError → show #lmigrate panel → _migrateEncryption(oldPw) re-encrypts all .enc files.Promise.all 8 loads:loadTaxonomy() · loadEncryptedKeys() · fetchActivityLog() · device trust banner checkLabAnalytics.calculate() → S.analytics · _scheduleOvernightJournalParse() (8s delay) · fetchWeather() + 60s intervalsetTimeout(_doWriteSummary, 5000) → commits lab-summary.json| Action | Result |
|---|---|
| Trust Yes | enc(S.tok) → localStorage.DEVICE_KEY — next visit restores token automatically |
| Trust No | Banner dismissed — token must be re-entered next visit |
| Forget device | localStorage.removeItem(DEVICE_KEY) — token field shown on login again |
| Op | Flow |
|---|---|
| Delete | deleteNote() → 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 |
| History | fetchNoteHistory(id) → GitHub commits API → list with sha/date → click → ghGet(sha) → dec → show modal → restore |
| Camera / QR | openCamera() → 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. |
| Op | Flow |
|---|---|
| Edit | startEditBottle(id) → populate form → S.editingBot → saveBottleAction edit branch → saveBottles → renderBottleList; openBottle |
| Delete | deleteBottle(id) → confirm → S.bottles.filter(remove) → saveBottles → renderBottleList → showBotEmpty |
| Status stepper | updateBottleStatus(id, newStatus) → set status + timestamps (date_inoculated, last_transfer) → saveBottles. Buttons: prepared → inoculated → growing → subcultured · terminal: contaminated · discarded · 🏡 sendToGreenhouse |
| Filter priority | 1. S.botBatchFilter (batch view) → 2. S.bottleFilter (status) → 3. S.bottleSearch (text) → 4. date desc sort |
| QR labels | 6 LABEL_CONFIGS (58mm-full/compact, 62mm, A4-4up/2up/QR-only). _toggleLabelPanel() → scope selector (single/batch) → _botLabelHtmlConfig(b, cfg) → print window |
| Cross-links | Accession → registry · Recipe → recipes · Linked note → notes · Source GH plant → greenhouse · Batch link → viewBatch() |
| Where | What |
|---|---|
| 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). |
| Pattern | Action | AI call? |
|---|---|---|
| "home" / "dashboard" | navTo('notes') | No |
| "notes" / "bottles" / "greenhouse" / "recipes" / "registry" / "reminders" / "analytics" | Navigate to that section | No |
| "settings" / "open scanner" / "sidebar" / "close" | Open panel / close AI | No |
| "read protocol N" / "read recipe N" | TTS reads numbered recipe aloud | No |
| "what's overdue" / "how many bottles" / "latest note" | TTS reads data summary | No |
| "switch to deep" / "switch to Claude" / "switch to quick" | Change AI model | No |
| "whisper mode" / "native mode" | Switch STT mode | No |
| "stop reading" / "stop" | Stop TTS mid-sentence | No |
| "add N bottles of [species]" (v3.11) | startNewBottle('batch'), prefills species + qty | No |
| "open bottle FSL-XXXX" (v3.11) | Finds by code → navTo('bottles'); openBottle | No |
| "show species [name]" / "open species [name]" (v3.11) | navTo('species'); openSpDetail(name) | No |
| "export report" / "download report" (v3.11) | exportLabReport('csv') | No |
| Anything else | askAI(transcript) | Yes |
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 / Field | What 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 / _aiSessionTok | Module-level counters reset on page load. Incremented by _aiRecordUsage(). |
_aiUpdateSessionChip() | Updates #ai-session-chip in the Ravana panel header with "N calls · X tok". |
_aiActivityFilter | Module-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 entries | Errors in askAI() catch block now call _aiRecordUsage() with err:true, errCode:e.status. Displayed with red ● dot in terminal. |
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 / Function | What 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. |
Daily token caps per feature, full usage analytics tab, chat fallback chain, web search, and context-saving mode.
| Key / Function | What 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 chain | Builds 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 mode | When 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 tool | Calls /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. |
| Op | Flow |
|---|---|
| Bottle → GH | sendToGreenhouse(bottleId) → transfer form → saveGHExplant() → push {type:'explant_out', plant_id, bottle_id, qty} to S.ghTransfers → saveGreenhouse |
| GH → Bottle | Bottle form has greenhouse_plant_id dropdown. On save: bottle.greenhouse_plant_id = plantId. openBottle shows "Source plant" link back. |
| Qty ledger | adjustQuantity(plantId, delta, reason) → p.quantity += delta → push {type:'quantity_adjust', delta} to ghTransfers → saveGreenhouse |
| Condition log | logCondition(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 |
| Where | What |
|---|---|
| Plant detail → 📷 QR tab | QR code canvas (url: ?gh={id}), plant name/location, Print + Copy buttons |
ghTab('qr') | Calls _renderGHQRCanvas(S.curGH) → QRCode.toCanvas() |
| QR scanner / URL param | ?gh=id → navTo('greenhouse'); openGHPlant(id) |
| Component | What changed |
|---|---|
| File upload | Removed accept=".csv,.txt" restriction — any file type accepted |
| URL fetch | fetchGHCsvUrl() — 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 column | Added to colMap (aliases: size, grade). Parsed + stored on imported plants. _normalizeGrade() applied to incoming values. |
| Column order | Always flexible via header matching — was already flexible, but help text now correctly states this. |
| Function | Purpose |
|---|---|
_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.
_renderDashboard() is called at login and after saves. Static helper: C(area, content, extra) → <div class="dc" style="grid-area:area;extra">
| Card | Value | navTo |
|---|---|---|
| 📋 Notes | S.notes.length | notes |
| 🫙 Bottles | active count (inoculated + growing) | bottles |
| 🌿 Accessions | S.accs.length | registry |
| 🏡 Greenhouse | total plant count | greenhouse |
| ⏰ Overdue | overdue notes + bottles + GH plants | reminders |
| 🧪 Recipes | S.recs.length | recipes |
| 📦 Supplies | low-stock count (yellow when >0) | supply |
| ☣️ Contam 30d | contam notes in last 30 days (red when >0) | contam |
| 💰 Vault | live ₹ value (bottles × stage prices + GH × grade) | value |
| 📓 Journal 7d | journal entries in last 7 days | journal |
_computeAlerts()| Condition | Level | navTo |
|---|---|---|
| Species with >10% contam rate in 30d | crit | contam |
| Any overdue next_review (bottles or notes) | warn | reminders |
| ≥5 active bottles past 28d inoculation | warn | bottles |
| GH plants with health ≠ healthy | warn | greenhouse |
| Supply batch expired or expiring <14d | info | supply |
| Feature | Where | State | Key functions |
|---|---|---|---|
| 🔥 Contamination Streak | Dashboard panel grid-area:streak | Computed from S.notes contam dates | _contamStreak() |
| 🌿 Species Spotlight | Dashboard panel grid-area:spotlight | Date-seeded random from S.taxSpecies | Inline in _renderDashboard() |
| ☑ Daily Checklist | Dashboard panel grid-area:checklist | localStorage tcplants_checklist, auto-resets midnight | _clInit() _clToggle() _clAdd() _clRemove() |
| 🍅 Pomodoro | Floating FAB #pomo-fab, panel #pomo-panel | localStorage tcplants_pomo_sessions | _pomoPanelToggle() _pomoToggle() _pomoTick() _playPomoChime() |
| 🌳 Lineage Tree | Bottle detail — passage section | Computed from S.bottles parent_id chain | _treeAncestor() _renderLineageTree() |
| 📸 Photo Strip | Bottle detail — below lineage | Filtered from S.gallery by bottle code | Inline render in openBottle() |
| ⚗️ Recipe Inline Scaler | Recipe detail panel | Stateless — recomputes on each input change | _recScaleRender() _recScaleUpdate() |
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}.
| Function | Purpose |
|---|---|
_dashLayout | Module-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 reset | Settings → Appearance → "↺ Reset" button calls _dashResetLayout() |
| System | When it runs | Output | Consumed 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 visit | SVG charts inline-calculated from S.notes + S.bottles | Analytics page UI only |
| Card | Data source |
|---|---|
| Notes / Month, Stage Donut, Contam Trend, Recipe Usage, Tag Cloud | S.notes (inline calculation) |
| Species Success Rate, Stage Funnel, Avg Days Between Entries, Media Efficiency | S.notes + S.bottles |
| Culture Lifecycle Status | Active 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 Usage | Section-visit counters in localStorage |
_setJournalParseEnabled(id, bool) — paused entries show ⏸ badge, skipped by overnight parser| Feature | Flow |
|---|---|
| Autocomplete | _taxSuggest(inputEl) oninput → filter S.taxSpecies + taxCustom → #tax-dropdown → keyboard nav (↑↓ Enter) → _taxDropdownSelect(name) → fill + trigger related dropdowns |
| Species picker | openSpeciesPicker(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 |
| Flairs | S.taxFlairs + S.taxCustom.flairs → _botFlairHtml(flair_ids) → colored pills in bottle detail + list. Flair picker in bottle editor: click to toggle flair_ids[] |
| System | State | File | Load | Use 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 |
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.
| System | Flow |
|---|---|
| Activity log | fetchActivityLog() → 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 Worker | navigator.serviceWorker.register('/sw.js'). Cache: network-first for index.html, cache-first for CDN assets. Bump SW_CACHE version to force refresh. |
| Offline mode | navigator.onLine → #offline-pill shown. Write ops blocked via _checkOnline(). ⚠️ offline check inconsistently applied — only notes index is guarded |
| Feature | Where triggered | What it does | State written |
|---|---|---|---|
| A — Synonym resolver | Custom species form (_taxGBIFLookup()) + species detail panel (_loadSpDetailGBIF()) | If GBIF returns synonym=true, shows yellow banner with accepted name + "Use accepted name →" swap button | Auto-fills form |
| B — Common names | _gbifImportSpecies() | Fetches vernacularNames in parallel with IUCN. English name (or first available) auto-fills common_name field | sp.common_name |
| C — Native distributions | _loadSpDetailGBIF() | Fetches distributions, filters NATIVE/ENDEMIC status, shows compact country tag row in species detail panel | UI 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 fetch | Fetches 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.
Nav drawer → 💰 Value. Estimates live inventory value from bottles + greenhouse stock using user-configured base prices.
| Component | Detail |
|---|---|
| Prices tab | 12 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 tab | Live calculated: Σ(bottles by stage × stagePrice) + Σ(GH plants × gradePrice × quantity). Species-level breakdown with overrides (pending). Vault total shown in INR. |
| Dashboard stat card | Compact ₹ value — formatted with fmtINR() (Indian locale: ₹12k / ₹1.2L) |
| GH plant size_grade | 6-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. |
| State | S.value = {prices:{}, overrides:{}} · S.valueSha — 8th load in login Promise.all |
| File | value/data.enc |
| Function | Mode | Output |
|---|---|---|
exportLabReport('csv') | CSV | 3 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') | Generates styled HTML with summary stat band + active bottles table + GH plants table. Opens in new window, auto-prints via window.onload. | |
| Analytics export buttons | CSV + Print | "📥 Export CSV" + "🖨 Print Report" in analytics header. |
| Voice command | "export report" / "download report" | Calls exportLabReport('csv') directly. |
| Button | Output |
|---|---|
| ⬇ ZIP | All notes as decrypted Markdown + accessions + recipes as JSON |
| CSV (top bar) | Note metadata CSV + accessions CSV |
| 📅 Calendar export | Print-ready HTML: this week / this month |
Alarm state is persisted in localStorage key tcplants_alarms. A deduplication set _firedAlarms prevents re-firing within the same minute.
| Function | What 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 |
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.
| Function | What 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. |
| Wire | Trigger | Target section | Function | Mechanism |
|---|---|---|---|---|
| 1 — Sales → Registry | _saveSaleAction() on save | Species 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 → Recipes | saveBottleAction() new bottle, no recipe_id, notes.length > 20 | Recipes | _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 → Bottle | takeExplantFromGH(plantId) | Bottles | takeExplantFromGH (existing) | Already implemented: logs ghTransfer, navTo('bottles'), startNewBottle('single'), pre-fills species + GH plant dropdown via _populateGHDropdown. No new code needed. |
| 4 — Contam note → Journal | saveNoteAction() 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-card | Bottles / 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 card | Sales (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). |
| Function | Purpose |
|---|---|
| _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 / _iotSaveTimer | GET/POST /api/iot/timers — evaluated server-side on heartbeat |
| _iotRenderRules / _iotSaveRule | GET/POST /api/iot/automations — if-then rules with hysteresis + cooldown |
| _iotSerialConnect / _iotFlashFirmware | Web Serial API: open port, write firmware binary, write JSON config |
| _iotSaveDevice / _iotOpenAdd | Register 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 / _iotHelpClose | Slide-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/Render | Named 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/Remove | Multi-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/Disconnect | Web 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 |
| Table | Columns |
|---|---|
| devices | id, name, location, chip, room, ip, last_seen, created_at, device_class (custom/commercial), comm_config (JSON), ota_url, ota_queued_at |
| pins | id, device_id, label, type, gpio_pin, unit, current_value |
| commands | id, device_id, pin_id, action, status, source, gpio_readback, created_at, received_at, confirmed_at, fire_at |
| sensor_readings | id, device_id, pin_id, value, ts |
| timers | id, device_id, pin_id, action, time_hhmm, days (7-char bitmask), duration_s, created_at |
| automations | id, device_id, if_pin, operator, threshold, then_pin, then_action, hysteresis, cooldown_s, enabled, created_at |
| cameras | id, name, location, room, ip, port, stream_path, snapshot_path, stream_type (mjpeg/snapshot), created_at |
| cams_rooms | room (PK), name, password_hash (SHA-256) |
| cams_sessions | token (PK), room, expires_at — 8-hour session tokens for /cams portal room auth |
| Issue | Location | Impact | Status |
|---|---|---|---|
| Fixed v3.9 | Stock data unreadable if inventory/data.enc exists | ✅ Fixed | |
| Fixed v3.10 | Now: LabAnalytics.calculate() called after saveNoteAction + saveBottleAction | ✅ Fixed | |
| renderAnalytics() vs S.analytics disconnect | renderAnalytics() | renderAnalytics() recalculates inline — S.analytics used only by AI context. Not a bug, but duplicate computation. | Design debt |
| Fixed v3.9 | Dead code — renderInventoryPage() uses S.supplies directly | ✅ Fixed | |
| Fixed v3.9 | Now includes all 10 statuses | ✅ Fixed | |
| Fixed v3.9 | CSS width:100% scoped to text inputs only | ✅ Fixed | |
| Fixed v3.10 | Removed Haiku monkey-patches — all load functions fail naturally on network error | ✅ Fixed | |
| Fixed v3.8 | Badge now updates after all saves | ✅ Fixed | |
| Fixed v3.12 | _detectDelimiter() now auto-selects comma/tab/semicolon. Copy-paste from Excel/Sheets works. | ✅ Fixed | |
| Fixed v3.12 | Removed accept restriction — any file accepted | ✅ Fixed | |
| Fixed v3.27 | Was 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 open | Calendar → ⏰ Alarms | No service worker push — alarms rely on setInterval. Will not fire if tab is closed or sleeping on mobile. | Design constraint |
| Fixed — fully implemented | create_note, set_review_date, log_contamination all save to GitHub with rollback on error; confirm gate wired via _executeToolWithConfirm() | ✅ Fixed |
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.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.WiFiClient + HTTPClient. GSM boards use TinyGSM with the same HTTP calls. The Worker sees no difference — all it receives is JSON over HTTPS.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.https://plantking.ape.workers.dev. IoT key is the x-app-key from Settings → IoT.| Modem | Network | Notes |
|---|---|---|
| SIM800L | 2G GPRS | Budget, very common; needs 4.2V 2A supply; no SSL hardware acceleration — use HTTP not HTTPS if unstable |
| SIM900 / SIM900A | 2G GPRS | Classic reliable board; same caveats as SIM800L |
| SIM7600E / SIM7600G | 4G LTE | Full LTE-FDD, India bands, stable; requires 5V 2A |
| A7670E (AI-Thinker) | 4G Cat-1 | LTE-M / Cat-1, good India coverage; 3.3–4.2V |
| SIM7670E / SIM7670G | 4G Cat-1 | Same as A7670E family; used on Waveshare ESP32-S3 board |
| Waveshare ESP32-S3-A7670E | 4G Cat-1 | ESP32-S3 + SIM7670E on one board; modem on UART1 (TX=17, RX=16); modem reset pin GPIO5 |
| SIM7020 / BC66 | NB-IoT | NarrowBand IoT — very low data rate; suitable only for sensors with infrequent heartbeats |
| Operator | APN | User | Pass | Notes |
|---|---|---|---|---|
| Airtel | airtelgprs.com | (blank) | (blank) | Use airtelnet.com if gprs doesn't work in your circle |
| Jio | jionet | (blank) | (blank) | 4G only; SIM800L (2G) will not work on Jio |
| BSNL | bsnlnet | bsnl | bsnl | Or try prepaid.bsnl.in in some circles |
| Vi / Vodafone | www | (blank) | (blank) | Or portalnmms in some circles |
| Idea | internet | (blank) | (blank) | Now merged with Vi; try Vi APN if this fails |
| Modem | ESP32 TX | ESP32 RX | Power | Extra pins |
|---|---|---|---|---|
| SIM800L | GPIO17 (UART2 TX) | GPIO16 (UART2 RX) | 4.2V @ 2A peak | GPIO5 → SIM800L RESET (optional) |
| SIM900 | GPIO17 | GPIO16 | 5V @ 2A | — |
| SIM7600E | GPIO17 | GPIO16 | 5V @ 2A | PWRKEY → GPIO4 (toggle 200 ms to wake) |
| A7670E external | GPIO17 | GPIO16 | 3.3–4.2V @ 2A | RESET → GPIO5 (optional) |
| Waveshare ESP32-S3 | GPIO17 (built-in) | GPIO16 (built-in) | USB-C 5V | PWRKEY = GPIO5 (auto in firmware) |
airtelgprs.com). Leave User and Pass blank for Airtel and Jio. BSNL needs bsnl / bsnl.192.168.1.0/24). Or check your router's DHCP table. Or use the Tasmota/Shelly app — it shows the IP.| Type | Control API | Status API |
|---|---|---|
| Tasmota | GET /cm?cmnd=Power+ON | GET /cm?cmnd=Status+0 |
| Shelly Gen 1 | GET /relay/0?turn=on | GET /status |
| Shelly Gen 2/3 | POST /rpc/Switch.Set | POST /rpc/Switch.GetStatus |
registerDevice(), sendHeartbeat(), pollCommands(), pin handlers, and OTA if enabled..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.temperature, humidity, analog_0, etc.). Pick an operator (> < ==). Enter the threshold value (e.g. 28 for 28 °C).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./mjpeg or /video) or a snapshot URL (JPG that refreshes). The Worker proxies the stream — enter the camera's local IP or public URL.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..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..bin to firmware/esp32/latest.bin or firmware/esp8266/latest.bin in your repo.| Library | By | Needed for |
|---|---|---|
| ArduinoJson | Benoit Blanchon | All — JSON parsing |
| DHT sensor library | Adafruit | DHT11 / DHT22 sensors |
| OneWire | Paul Stoffregen | DS18B20 temp probes |
| DallasTemperature | Miles Burton | DS18B20 temp probes |
esp32 by Espressif Systemsesp8266 by ESP8266 Communityfirmware/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..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.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.\x01CFG:{json}\x04. The device receives it, saves to EEPROM/Preferences, then reboots and connects to WiFi automatically.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.GET /api/iot/devices, finds its own entry, and loads the pin list (GPIO number, type, label). Sets up GPIO modes.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.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.| Type | Direction | What it does |
|---|---|---|
| output | OUT | Digital HIGH/LOW — LED, valve, buzzer |
| relay | OUT | Same as output — treated as ON/OFF switch. Cards show toggle buttons. |
| pwm | OUT | PWM output — fan speed, dimmer. Use action value 0–255. |
| input | IN | Digital read with INPUT_PULLUP — button, door sensor (0=triggered) |
| analog | IN | Analog read — moisture sensor, light sensor, potentiometer. ESP32: any ADC pin. ESP8266: A0 only. |
| dht | IN | DHT11 or DHT22 temperature + humidity. Requires Adafruit DHT library. |
| ds18b20 | IN | DS18B20 waterproof temp probe (OneWire). Good for nutrient solution temp. |
| State | Meaning |
|---|---|
| pending | Created in D1, waiting for device to pick up |
| received | Device fetched it on its 5s poll |
| confirmed | Device executed it and reported GPIO readback ✓ |
| failed | Device reported execution failure |
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.| Field | Example |
|---|---|
| if_pin | temp sensor pin ID |
| operator | > < >= <= == |
| threshold | 28.5 (°C, %, raw value) |
| then_pin | fan relay pin ID |
| then_action | on or off |
| cooldown_s | 300 — minimum seconds between firings |
| hysteresis | Optional dead-band to prevent rapid toggling |
01:30 = 7:00 AM IST)1111100 = Mon–Fri only (Mon=0, Sun=6)on or off\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.ssid · pass · worker_url · name · location. The device ID is cleared so it re-registers with the new name.| Problem | Fix |
|---|---|
| Device shows Offline | Last 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 missing | Chip 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 available | You'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 reboot | Config not saving. On ESP8266 check EEPROM size. On ESP32 check Preferences namespace. Serial logs will show "[IoT] Config saved" if working. |
| Automation not triggering | Rules 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. |
In-browser IDE for writing, generating, and flashing custom .ino sketches for ESP32 / NodeMCU devices. Open via 🔧 Studio in the IoT header.
| Tab | Purpose |
|---|---|
| Form | Device identity, network, pin builder, feature toggles. Code generates live. Presets strip above templates — save your own named configs for quick recall. |
| Editor | Full 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. |
| Serial | Live 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. |
| Flash | Write 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.
Loading…
plantking Worker. Nothing to manage here.