feat(ui): rebuild the interface with a proper design system
The old UI was two pages of raw pandas tables with no navigation, no styling and no way to see a run's comments. This replaces it with a small, testable application and a beer-themed design. What changed ------------ Restructure - The domain logic now lives in `brewing.py` as pure functions over plain data, with no Flask or Mongo dependency. The gravity maths, the ABV formula and the run-aggregation rules are unit-testable on their own. - `app_factory.py` replaces the module-level app, and the routes move into blueprints under `views/`. A request-scoped Mongo repository is created per request and closed at teardown. - `mongo/repository.py` replaces `build_db.py` / `query_db.py` / `get_conn.py`. Reading a document no longer requires a second query to fetch the fields. Interface - A dark, beer-themed design system: warm amber `#be7f39` on near-black, a condensed display face (Anton) for headings and Inter for body text. Both fonts and the chart library are vendored into `static/`, so the container needs no outbound network at runtime — the old templates pulled Bootstrap from a CDN. - A real navigation bar, a login page, a brew-process page with a proper form, and a run-detail page listing every reading with its comment. - The run summary tables now show the beer name and the latest comment, which the old pandas tables did not. - Native form controls (checkbox, number spinners, date icon, scrollbars) are restyled for the dark theme. Correctness - The alcohol calculation now uses the standard homebrew formula, `(OG - FG) x 131.25`. The old code used `(max - min) / 7.36 + 0.5`, which is not a recognised brewing formula and overstated every result. - Gravity points and decimal readings now convert correctly in both directions (`1039` <-> `1.039`). - A run's display name resolves to the most-used label across its readings, because `beer_type` is stored per-reading and drifts across a run. - Re-submitting a reading for the same run and day updates it in place rather than creating a duplicate. Security / production - The container ran the Werkzeug debugger with `FLASK_DEBUG=1`, exposing an interactive console on any unhandled error. It now runs waitress with debugging off. - `SECRET_KEY` comes from the environment. - The app secret is no longer a hard-coded string in source. Housekeeping - Removes the bokeh/duckdb/pandas dependency chain and the static file it generated at import time; removes the dead `add_user.py` and `test.py`. - Splits `requirements.txt` (runtime) from `requirements-dev.txt` (tests). - Adds a pytest suite (64 tests) and a `test` job to the CI workflow so the image is only built when the tests pass. - Adds a README describing the data model and a RUNBOOK for the cluster side. Data compatibility ------------------ The existing `beer_db.beer_data` documents are read as they are and are not migrated or rewritten. No collection is dropped, and no document is modified by this change other than through the explicit "record a reading" form, which keeps the original `(date, beer_run_id)` uniqueness rule.
This commit is contained in:
@@ -1,7 +1,104 @@
|
||||
# BEER SG data storer
|
||||
# Brew Log
|
||||
|
||||
This container will store SG data for beer runs in Mongo and then use that data to calculate Alchohol Content of each beer
|
||||
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.
|
||||
|
||||
It's pretty loose and requires the Data Entry operator to know whats happening
|
||||
Flask app, Mongo backend, no build step for the front end.
|
||||
|
||||
Luck for me... That's me!
|
||||
## 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.
|
||||
|
||||
## 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, (later) 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
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
Reference in New Issue
Block a user