The RUNBOOK still said the workflow did not deploy to the cluster. It does now - it builds, pushes, applies the manifests, rolls the pods and verifies the app from inside the pod. The manual deploy path is documented alongside it, along with the dry-run step that catches the schema errors kubectl apply silently swallows.
178 lines
6.2 KiB
Markdown
178 lines
6.2 KiB
Markdown
# Brew Log
|
||
|
||
Stores specific-gravity data for home-brewed beer runs in Mongo and turns it into
|
||
useful figures — alcohol by volume, days in the fermenter, gravity trends.
|
||
|
||
Flask app, Mongo backend, no build step for the front end.
|
||
|
||
## Running it locally
|
||
|
||
```bash
|
||
python -m venv .venv
|
||
. .venv/bin/activate
|
||
pip install -r requirements-dev.txt
|
||
|
||
export MONGO_HOST=192.168.178.229
|
||
export MONGO_USER=root
|
||
export MONGO_PASS=...
|
||
export SECRET_KEY=anything-for-local-use
|
||
|
||
cd src/flask
|
||
python -m beer_data # dev server on :5000
|
||
# or, production-style:
|
||
python -m waitress --listen=0.0.0.0:5000 --call beer_data:create_app
|
||
```
|
||
|
||
Then open <http://localhost:5000/> and sign in.
|
||
|
||
## Tests
|
||
|
||
```bash
|
||
. .venv/bin/activate
|
||
python -m pytest tests/ -q
|
||
```
|
||
|
||
The tests use `mongomock`, so they need no database and no network.
|
||
|
||
## The pages
|
||
|
||
| Page | What it is for |
|
||
|---|---|
|
||
| `/login` | Sign in |
|
||
| `/brews` | The brew process: record a reading, review every run |
|
||
| `/active` | What is in the fermenter right now — tap, gravity, projected strength |
|
||
| `/history` | How every brew has gone, with charts and the standard-vs-legacy ABV comparison |
|
||
| `/analysis` | How consistently the brews ferment — attenuation, fermentation curves, brew timings |
|
||
|
||
### `/active` — the active beers page
|
||
|
||
A card per brew that is still in the fermenter (a run with no reading marked
|
||
final), showing:
|
||
|
||
- **the tap number** it is on,
|
||
- **the current gravity**, as a decimal reading with the point scale beneath,
|
||
- **the projected alcohol**, worked out from the run's original gravity,
|
||
- **how many days** it has been brewing,
|
||
- a **sparkline** of the gravity so far,
|
||
- **the latest comment** on a reading.
|
||
|
||
A run with only one reading and no recorded original gravity has no reliable
|
||
estimate, so the alcohol figure shows a dash rather than a misleading number.
|
||
Record the OG on the run (or take a second reading) and it appears.
|
||
|
||
### `/analysis` — how the brews ferment
|
||
|
||
The page that turns the history into something you can brew better from:
|
||
|
||
- **Attenuation by brew** — what fraction of the original gravity was
|
||
fermented away, for every finished brew.
|
||
- **Fermentation curves** — the typical shape of a fermentation, drawn as a
|
||
median curve with the min–max envelope behind it. Plotting every brew
|
||
individually gives sixteen near-identical lines and an unreadable legend.
|
||
- **Every finished brew** — OG, FG, ABV, attenuation, days in the fermenter,
|
||
days to reach 1.010, and the finish date.
|
||
- **Readings that need a second look** — readings outside the plausible
|
||
hydrometer range.
|
||
- Headline figures: average attenuation and its spread, average brew time,
|
||
average time to 1.010.
|
||
|
||
A reading that is physically impossible — a final gravity below the original
|
||
gravity by more than the wort itself could have — is excluded from the
|
||
averages rather than silently distorting them, and the run is listed on the
|
||
page so it can be corrected.
|
||
|
||
### Run metadata
|
||
|
||
Some details cannot be derived from the readings, so they are stored per run in
|
||
the `beer_runs` collection and edited on the run detail page:
|
||
|
||
- the **canonical name** the brew is known by (overriding the drifting
|
||
per-reading name),
|
||
- the **tap number**,
|
||
- the **original gravity**,
|
||
- free-text **brew notes**.
|
||
|
||
### Populating the run metadata from the existing readings
|
||
|
||
```bash
|
||
# dry run - prints what it would do, writes nothing
|
||
python scripts/backfill_run_metadata.py
|
||
|
||
# write it
|
||
python scripts/backfill_run_metadata.py --apply
|
||
```
|
||
|
||
The script is idempotent and non-destructive: it only inserts into
|
||
`beer_runs`, never touches `beer_data`, and never overwrites a name you have
|
||
already set by hand (pass `--force` if you want it to).
|
||
|
||
## Layout
|
||
|
||
```
|
||
src/flask/
|
||
beer_data.py entry point; exposes `app` and `create_app`
|
||
app_factory.py application factory, request-scoped Mongo repository
|
||
brewing.py pure domain logic — gravity parsing, ABV, run aggregation
|
||
views/ blueprints: auth, brews, analysis
|
||
mongo/repository.py Mongo access for the beer_data and beer_runs collections
|
||
templates/ Jinja templates
|
||
static/ CSS, JS, fonts, logo (all served locally - no CDN)
|
||
tests/ pytest suite
|
||
scripts/ operational scripts (the run-metadata backfill)
|
||
```
|
||
|
||
`brewing.py` holds every piece of arithmetic and has no Flask or Mongo
|
||
dependency, so the maths is unit-testable on its own.
|
||
|
||
## Data model
|
||
|
||
### `beer_data` — one document per gravity reading
|
||
|
||
| Field | Type | Notes |
|
||
|---|---|---|
|
||
| `_id` | string | 24 random characters |
|
||
| `beer_run_id` | string | The run this reading belongs to, e.g. `"20"` |
|
||
| `beer_type` | string | Beer/cider name. **Free text, per reading** — it drifts across a run |
|
||
| `sg` | string | Specific gravity in *points*: `1039` means 1.039 |
|
||
| `date` | string | `YYYY-MM-DD` |
|
||
| `final_reading` | string | `"True"` / `"False"` — marks the last reading of a run |
|
||
| `comment` | string | Free text |
|
||
|
||
The unique key is `(date, beer_run_id)`: re-submitting a reading for the same
|
||
run and day updates it in place rather than creating a duplicate.
|
||
|
||
A run is **active** while it has no reading with `final_reading == "True"`.
|
||
|
||
### `beer_runs` — one document per run (metadata)
|
||
|
||
Added alongside the readings to hold what cannot be derived from them: the
|
||
canonical beer name, the tap number, the recorded original gravity, and brew
|
||
notes. This collection is only ever inserted into, never written over by the
|
||
backfill.
|
||
|
||
## Alcohol by volume
|
||
|
||
Standard homebrew formula:
|
||
|
||
```
|
||
ABV% = (OG - FG) × 131.25
|
||
```
|
||
|
||
With gravity stored in points, the drop is already in points, so
|
||
`ABV% = points_drop × 0.13125`.
|
||
|
||
> The previous version of this app used `(max - min) / 7.36 + 0.5`, which is not
|
||
> a recognised brewing formula and overstated every result. The old figure is
|
||
> still computed by `brewing.legacy_abv_percent()` so the history page can show
|
||
> the difference between the two.
|
||
|
||
For a run still in the fermenter the figure is an **estimate**: it uses the
|
||
run's first reading as the original gravity. A run with a single reading and no
|
||
recorded OG has no reliable estimate, and the UI shows a dash rather than a
|
||
misleading number.
|
||
|
||
## Deployment
|
||
|
||
The image is built and pushed by `.gitea/workflows/build_push.yml` on push to
|
||
`master`. See [RUNBOOK.md](RUNBOOK.md) for the cluster side.
|