# Brew Fermentation Dashboard

Self-contained tracker for the TiltBridge at `192.168.1.171`. No n8n, Snowflake,
or MQTT required to get live SG/temp/rate monitoring — those remain optional
add-ons (see "Where this fits the bigger plan" below).

## What's here

- `poll_tiltbridge.sh` — polls the TiltBridge's live JSON endpoint and appends
  one line per Tilt color to `history/<Color>.jsonl`. Run on a schedule (cron).
- `history/` — the append-only data store the poller writes and the dashboard
  reads. One file per Tilt color, `.jsonl` (one JSON object per line).
- `dashboard.html` — the dashboard itself. Static HTML/CSS/JS, no build step,
  no external CDN dependencies (works even if the LAN loses internet).

## How the pieces fit

```
TiltBridge (192.168.1.171/api/json/)
        │  polled every N min by cron
        ▼
poll_tiltbridge.sh  →  history/<Color>.jsonl   (append-only, one file/Tilt)
                              │
                              │  fetched by the browser (same-origin, via nginx)
                              ▼
                       dashboard.html
                              │
                    brew profiles (OG, yeast temp
                    range, start date) saved in the
                    browser's localStorage, with
                    Export/Import for backup + sharing
```

**Why a poller instead of the device's push feature:** the doc's original plan
routed data through an n8n webhook (device pushes → n8n). Turns out the
TiltBridge also serves a live JSON endpoint at `/api/json/` that we can just
poll directly — one less moving part, no n8n workflow needed. Confirmed live
schema (differs from the doc's guessed shape):

```json
[{"color":"Green","temp":"12.8","tempUnit":"C","uncalibratedGravity":"1.0040",
  "calibratedGravity":"1.0040","latestGravity":"1.0040","weeks_on_battery":157,
  "sends_battery":true,"rssi":-94,"mac":"CE:C6:77:1D:0A:66", ...}]
```

The poller normalizes temp to Celsius and casts all numeric fields (the device
sends them as strings), and uses `calibratedGravity` (the TiltBridge's own
built-in calibration) rather than the raw reading.

**Why brew profiles live in the browser, not a server file:** the poller only
needs *write* access (cron, no HTTP server needed for it). Keeping profiles
(name, OG, yeast temp range, start date) in `localStorage` means the dashboard
stays 100% static — nothing needs to accept writes over HTTP. Use "Export
profiles" any time to download a JSON backup, and "Import" to restore or move
them to another browser. **Back this up** — clearing browser data wipes it.

## Test it locally right now

```bash
cd brew-dashboard
./poll_tiltbridge.sh          # grab one live reading
python3 -m http.server 8099   # don't just open the file:// — fetch() needs http(s)://
```

Open `http://localhost:8099/dashboard.html`, click **+ New brew**, set Tilt
color to **Green** (the currently-live one), fill in the OG you actually
pitched at, and you should see the current reading plus data start
accumulating with each poll.

## Deploy to the server (192.168.1.213)

1. Copy this whole `brew-dashboard/` folder to the server, e.g.
   `~/brew-dashboard/`.
2. `chmod +x poll_tiltbridge.sh` if the permission didn't survive the copy.
3. Add a cron entry (`crontab -e`) — every 5 minutes is a reasonable default,
   the Tilt itself only updates every few minutes anyway:
   ```
   */5 * * * * /home/neilm/brew-dashboard/poll_tiltbridge.sh >> /home/neilm/brew-dashboard/poll.log 2>&1
   ```
4. Serve the folder as a static site — same pattern as the SwingTrader
   dashboard (nginx on a dedicated port, exposed through Nginx Proxy Manager
   at `brew.neilm-home.duckdns.org`). Minimal nginx server block:
   ```nginx
   server {
       listen 8xxx;                       # pick a free port, match SwingTrader's pattern
       root /home/neilm/brew-dashboard;
       index dashboard.html;
       location / { try_files $uri $uri/ =404; }
   }
   ```
   Then in NPM: add a proxy host for `brew.neilm-home.duckdns.org` → that port,
   same as the existing dashboard.
5. In NPM/nginx, no reverse proxy to `192.168.1.171` is needed — the dashboard
   never talks to the TiltBridge directly, only to its own `history/` folder,
   so there's no CORS concern once it's deployed.

This only touches the server's cron table and adds a new nginx site/NPM proxy
host — it doesn't modify n8n, Home Assistant, or anything already running.
Say the word when you want me to SSH in and do this deployment step.

## What's computed

- **ABV** = `(OG − SG) × 131.25`
- **Apparent attenuation** = `(OG − SG) / (OG − 1) × 100%`
- **Daily fermentation rate** = day-over-day average SG drop (points/day)
- **Stall/done detection** = flags if SG moved < 1 point in the last 24h
  *and* meaningful attenuation has already happened (so it won't false-alarm
  during the lag phase right after pitching)
- **Projected FG / days-to-target** — assumes the daily rate is decaying
  geometrically (each day's drop is roughly a fixed fraction of the day
  before's) and extrapolates the remaining drop from that trend. It's a trend
  extrapolation, not a physical kinetic model — treat it as directional, and
  it needs a few days of real deceleration in the data before it'll produce
  anything (shows "not enough data" until then). Tested against a synthetic
  exponential fermentation curve and landed within 0.0004 SG of the true
  final gravity.
- **Temp excursion** — flags if current temp is outside the profile's yeast
  range (±0.5° buffer)

## One thing worth checking on the hardware side

The live reading right now shows **RSSI −94 dBm** on the Green Tilt — notably
*worse* than the "~-85 dBm, fixed by unplugging HDMI" issue mentioned in the
original doc, and well outside the expected -40s/-50s at the fermenter. The
dashboard will flag this as a "weak signal" warning once you're watching a
real brew, but might be worth a quick look now (Tilt battery, distance from
bridge, something else re-introducing 2.4GHz noise).

## Where this fits the bigger plan

Everything above is a complete, working loop on its own. The doc's other
pieces are still there if you want them *in addition*, not instead:
- **ntfy alerts** — could hook onto the same `history/*.jsonl` files (a
  second small script/cron job that checks the latest line and pushes a
  ntfy notification) instead of routing through n8n.
- **Snowflake** — cross-batch long-term storage; the JSONL files are the
  source of truth either way, Snowflake would just be a mirror.
- **MQTT → Home Assistant** — independent of all of this, can be turned on
  in parallel via the TiltBridge's own config UI.
