diff --git a/app.py b/app.py index 016b3b8..26a0250 100644 --- a/app.py +++ b/app.py @@ -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//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//capacity-test//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 # ------------------------------------------------------------------ # diff --git a/models.py b/models.py index 76e0a97..6cd25fa 100644 --- a/models.py +++ b/models.py @@ -49,6 +49,11 @@ class Battery(Base): storage_location = Column(String(100), nullable=True) # where stored when not installed device = relationship("Device", back_populates="batteries") + capacity_tests = relationship( + "CapacityTest", back_populates="battery", + order_by="CapacityTest.tested_date", + cascade="all, delete-orphan", + ) def is_available(self): return self.status == "available" @@ -61,3 +66,18 @@ class Battery(Base): def __repr__(self): return f"" + + +class CapacityTest(Base): + __tablename__ = "capacity_test" + + id = Column(Integer, primary_key=True, autoincrement=True) + battery_id = Column(Integer, ForeignKey("battery.id", ondelete="CASCADE"), nullable=False) + tested_capacity_mah = Column(Integer, nullable=False) + tested_date = Column(String(10), nullable=False) # YYYY-MM-DD + notes = Column(Text, nullable=True) + + battery = relationship("Battery", back_populates="capacity_tests") + + def __repr__(self): + return f"" diff --git a/templates/battery_detail.html b/templates/battery_detail.html index 94dafc9..f4ef0b5 100644 --- a/templates/battery_detail.html +++ b/templates/battery_detail.html @@ -76,6 +76,78 @@ + +
+

Capacity History

+ + {% if capacity_tests|length >= 2 %} + + {% endif %} + + {% if capacity_tests %} +
+ + + + + + {% if battery.capacity_mah %}{% endif %} + + + + + + {% for t in capacity_tests|sort(attribute='tested_date', reverse=True) %} + + + + {% if battery.capacity_mah %} + {% set pct = (t.tested_capacity_mah / battery.capacity_mah * 100)|round|int %} + {% if pct >= 80 %}{% set hc = "health-good" %} + {% elif pct >= 60 %}{% set hc = "health-warn" %} + {% else %}{% set hc = "health-bad" %}{% endif %} + + {% endif %} + + + + {% endfor %} + +
DateCapacityHealthNotes
{{ t.tested_date }}{{ t.tested_capacity_mah }} mAh{{ pct }}%{{ t.notes or '—' }} +
+ +
+
+
+ {% else %} +

No test records yet.

+ {% endif %} + +

Add Test Record

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+

Edit Details

@@ -123,18 +195,6 @@ value="{{ battery.charge_cycles or '' }}" placeholder="e.g. 50">
-
- - -
- -
- - -
-
← Back to Dashboard