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:
@@ -0,0 +1,71 @@
|
||||
/* ==========================================================================
|
||||
Brew Log — page behaviour
|
||||
Small, dependency-free helpers. Charts live in charts.js.
|
||||
========================================================================== */
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
/** Auto-focus the first empty field on a form. */
|
||||
function focusFirstEmpty() {
|
||||
var fields = document.querySelectorAll("input:not([type=hidden]), textarea, select");
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
if (!fields[i].value && !fields[i].readOnly) {
|
||||
fields[i].focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Default the reading date to today when the field is left empty. */
|
||||
function defaultDateFields() {
|
||||
var today = new Date().toISOString().slice(0, 10);
|
||||
var dateFields = document.querySelectorAll('input[type="date"][data-default-today]');
|
||||
for (var i = 0; i < dateFields.length; i++) {
|
||||
if (!dateFields[i].value) {
|
||||
dateFields[i].value = today;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Show/hide blocks driven by a checkbox. */
|
||||
function wireToggles() {
|
||||
var toggles = document.querySelectorAll("[data-toggle-target]");
|
||||
for (var i = 0; i < toggles.length; i++) {
|
||||
(function (toggle) {
|
||||
var target = document.getElementById(toggle.getAttribute("data-toggle-target"));
|
||||
if (!target) { return; }
|
||||
function apply() {
|
||||
target.classList.toggle("hidden", !toggle.checked);
|
||||
}
|
||||
toggle.addEventListener("change", apply);
|
||||
apply();
|
||||
})(toggles[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/** Confirm destructive actions. */
|
||||
function wireConfirms() {
|
||||
var forms = document.querySelectorAll("[data-confirm]");
|
||||
for (var i = 0; i < forms.length; i++) {
|
||||
forms[i].addEventListener("submit", function (event) {
|
||||
if (!window.confirm(this.getAttribute("data-confirm"))) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
focusFirstEmpty();
|
||||
defaultDateFields();
|
||||
wireToggles();
|
||||
wireConfirms();
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user