Add MOT/insurance renewal hints, table service card, and favicon

- Renewal tracking: compute due/overdue status from the most recent
  mot/insurance event (renewal_months, renewal_due_soon_days settings),
  surfaced on the service card and dashboard.
- Service-status card metrics now render as a two-column label/value
  table with row separators instead of wrapping auto-fill grid cells.
- Add a bicycle favicon (favicon.svg) to base + login templates.

Co-Authored-By: opencode <opencode@users.noreply.github.com>
This commit is contained in:
2026-09-10 10:01:17 +01:00
co-authored by opencode
parent 99c15e23bb
commit aced0a1a3b
10 changed files with 225 additions and 1 deletions
+99
View File
@@ -0,0 +1,99 @@
from datetime import date, timedelta
from app import db as d
from app.config import get_settings
from app.services import compute_renewal_due
def _conn():
settings = get_settings()
settings.data_dir.mkdir(parents=True, exist_ok=True)
d.init_schema(settings.resolved_db_path)
return d.connect(settings.resolved_db_path)
def _bike(conn, name="B"):
return d.get_bike(conn, d.create_bike(conn, name, None))
def _log(conn, bike_id, type, day):
d.create_event(conn, bike_id, type, day, None, None, None)
def test_no_data_when_no_renewal_event():
conn = _conn()
b = _bike(conn)
_log(conn, b.id, "mot", date(2024, 6, 1))
r = compute_renewal_due(conn, b, "insurance")
assert r.status == "no-data"
assert r.due_date is None
assert r.days_until is None
conn.close()
def test_due_is_twelve_months_after_renewal():
conn = _conn()
b = _bike(conn)
_log(conn, b.id, "mot", date(2025, 6, 1))
r = compute_renewal_due(conn, b, "mot", today=date(2025, 6, 1))
assert r.renewal_date == date(2025, 6, 1)
assert r.due_date == date(2026, 6, 1)
assert r.status == "ok"
assert r.days_until > get_settings().renewal_due_soon_days
conn.close()
def test_due_soon_within_lead_time():
# Due 30 days after today; renewal was 12 months before that.
conn = _conn()
b = _bike(conn)
today = date(2026, 1, 15)
_log(conn, b.id, "insurance", date(2025, 2, 14)) # due 2026-02-14
r = compute_renewal_due(conn, b, "insurance", today=today)
assert r.status == "due-soon"
assert 0 <= r.days_until <= get_settings().renewal_due_soon_days
conn.close()
def test_exactly_at_lead_time_is_due_soon():
conn = _conn()
b = _bike(conn)
lead = get_settings().renewal_due_soon_days
due = date(2026, 4, 1)
_log(conn, b.id, "mot", date(2025, 4, 1)) # due 2026-04-01
r = compute_renewal_due(conn, b, "mot", today=due - timedelta(days=lead))
assert r.days_until == lead
assert r.status == "due-soon"
conn.close()
def test_just_outside_lead_time_is_ok():
conn = _conn()
b = _bike(conn)
lead = get_settings().renewal_due_soon_days
due = date(2026, 4, 1)
_log(conn, b.id, "mot", date(2025, 4, 1)) # due 2026-04-01
r = compute_renewal_due(conn, b, "mot", today=due - timedelta(days=lead + 1))
assert r.days_until == lead + 1
assert r.status == "ok"
conn.close()
def test_overdue_when_renewal_is_thirteen_months_old():
conn = _conn()
b = _bike(conn)
_log(conn, b.id, "mot", date.today() - timedelta(days=365 + 31))
r = compute_renewal_due(conn, b, "mot")
assert r.status == "overdue"
assert r.days_until < 0
conn.close()
def test_renewal_badge_shown_on_bike_page(client):
client.login()
bike_id = client.create_bike("Roadie")
result = client.create_event(bike_id, type="mot", event_date=date.today().isoformat())
assert result["status"] == 200
page = client.get(f"/bikes/{bike_id}").text
assert "MOT" in page
assert "Insurance" in page