Add capacity test history and chart to battery detail

- New CapacityTest model (battery_id FK CASCADE, mah, date, notes)
- DB migration: create capacity_test table, migrate existing single-test data
- Two new routes: add and delete capacity test records
- Battery.tested_capacity_mah/tested_date kept in sync with latest test
  so dashboard display requires no changes
- Battery detail: Capacity History card with sortable table, health %
  per reading, and a canvas line chart (shown when >= 2 records)
- Chart uses CSS variables for colors — works in light and dark mode
- Remove tested_capacity_mah/tested_date from Edit Details form
- 3 new acceptance tests (48 total)
This commit is contained in:
2026-04-13 04:15:55 -05:00
parent 86fb342b0d
commit 2f8a8a2b77
4 changed files with 239 additions and 16 deletions
+58 -4
View File
@@ -2,7 +2,7 @@ from flask import Flask, render_template, redirect, url_for, request, flash, abo
from sqlalchemy import create_engine, func
from sqlalchemy.orm import scoped_session, sessionmaker
from models import Base, Battery, Device
from models import Base, Battery, Device, CapacityTest
def create_app(config_object="config"):
@@ -105,8 +105,13 @@ def create_app(config_object="config"):
.filter(Battery.storage_location.isnot(None))
.distinct().order_by(Battery.storage_location).all()
]
capacity_tests = (db.query(CapacityTest)
.filter_by(battery_id=battery_id)
.order_by(CapacityTest.tested_date, CapacityTest.id)
.all())
return render_template("battery_detail.html", battery=battery,
storage_locations=storage_locations)
storage_locations=storage_locations,
capacity_tests=capacity_tests)
# ------------------------------------------------------------------ #
# Battery — edit notes
@@ -130,8 +135,6 @@ def create_app(config_object="config"):
battery.size = f.get("size", "").strip() or None
battery.chemistry = f.get("chemistry", "").strip() or None
battery.capacity_mah = _int("capacity_mah")
battery.tested_capacity_mah = _int("tested_capacity_mah")
battery.tested_date = f.get("tested_date", "").strip() or None
battery.charge_cycles = _int("charge_cycles")
battery.purchase_date = f.get("purchase_date", "").strip() or None
battery.storage_location = f.get("storage_location", "").strip() or None
@@ -140,6 +143,57 @@ def create_app(config_object="config"):
flash("Details updated.", "success")
return redirect(url_for("battery_detail", battery_id=battery_id))
# ------------------------------------------------------------------ #
# Battery — capacity test history
# ------------------------------------------------------------------ #
def _sync_latest_test(battery):
latest = (db.query(CapacityTest)
.filter_by(battery_id=battery.id)
.order_by(CapacityTest.tested_date.desc(), CapacityTest.id.desc())
.first())
battery.tested_capacity_mah = latest.tested_capacity_mah if latest else None
battery.tested_date = latest.tested_date if latest else None
@app.route("/battery/<int:battery_id>/capacity-test/add", methods=["POST"])
def battery_capacity_test_add(battery_id):
battery = db.get(Battery, battery_id)
if battery is None:
abort(404)
mah_raw = request.form.get("tested_capacity_mah", "").strip()
date_val = request.form.get("tested_date", "").strip()
notes = request.form.get("notes", "").strip() or None
if not mah_raw or not date_val:
flash("Capacity (mAh) and date are required.", "error")
return redirect(url_for("battery_detail", battery_id=battery_id))
try:
mah = int(mah_raw)
if mah <= 0:
raise ValueError
except ValueError:
flash("Capacity must be a positive integer.", "error")
return redirect(url_for("battery_detail", battery_id=battery_id))
db.add(CapacityTest(battery_id=battery_id, tested_capacity_mah=mah,
tested_date=date_val, notes=notes))
db.flush()
_sync_latest_test(battery)
db.commit()
flash("Test record added.", "success")
return redirect(url_for("battery_detail", battery_id=battery_id))
@app.route("/battery/<int:battery_id>/capacity-test/<int:test_id>/delete", methods=["POST"])
def battery_capacity_test_delete(battery_id, test_id):
battery = db.get(Battery, battery_id)
test = db.get(CapacityTest, test_id)
if battery is None or test is None or test.battery_id != battery_id:
abort(404)
db.delete(test)
db.flush()
_sync_latest_test(battery)
db.commit()
flash("Test record deleted.", "success")
return redirect(url_for("battery_detail", battery_id=battery_id))
# ------------------------------------------------------------------ #
# Battery — assign
# ------------------------------------------------------------------ #