Files
battery-tracker-app/templates/battery_detail.html
T

415 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}{{ battery.label }} — Battery Tracker{% endblock %}
{% block content %}
<h1>{{ battery.label }}</h1>
{% macro meta_row(label, value) %}
{% if value %}
<tr>
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">{{ label }}</td>
<td style="border:none;">{{ value }}</td>
</tr>
{% endif %}
{% endmacro %}
<div class="card">
<table style="width:auto;border:none;">
<tr>
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Label</td>
<td style="border:none;">{{ battery.label }}</td>
</tr>
<tr>
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Brand</td>
<td style="border:none;">{{ battery.brand }}</td>
</tr>
<tr>
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Status</td>
<td style="border:none;"><span class="badge badge-{{ battery.status }}">{{ battery.status|capitalize }}</span></td>
</tr>
<tr>
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Device</td>
<td style="border:none;">
{% if battery.device %}
<a href="{{ url_for('device_detail', device_id=battery.device.id) }}">{{ battery.device.name }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
{{ meta_row("Size", battery.size) }}
{{ meta_row("Chemistry", battery.chemistry) }}
{% if battery.capacity_mah %}
<tr>
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Capacity</td>
<td style="border:none;">
{{ battery.capacity_mah }} mAh
{% if battery.tested_capacity_mah %}
{% set pct = (battery.tested_capacity_mah / battery.capacity_mah * 100)|round|int %}
{% if pct >= 80 %}{% set health_class = "health-good" %}
{% elif pct >= 60 %}{% set health_class = "health-warn" %}
{% else %}{% set health_class = "health-bad" %}
{% endif %}
&rarr; tested <strong class="{{ health_class }}">{{ battery.tested_capacity_mah }} mAh ({{ pct }}%)</strong>
{% if battery.tested_date %}<span class="text-muted">on {{ battery.tested_date }}</span>{% endif %}
{% endif %}
</td>
</tr>
{% elif battery.tested_capacity_mah %}
<tr>
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Tested Capacity</td>
<td style="border:none;">
{{ battery.tested_capacity_mah }} mAh
{% if battery.tested_date %}<span class="text-muted">on {{ battery.tested_date }}</span>{% endif %}
</td>
</tr>
{% endif %}
{{ meta_row("Charge Cycles", battery.charge_cycles) }}
{{ meta_row("Purchase Date", battery.purchase_date) }}
{{ meta_row("Storage", battery.storage_location) }}
{% if battery.battery_percentage is not none %}
<tr>
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Battery %</td>
<td style="border:none;">
{% if battery.battery_percentage < 20 %}
<span class="badge badge-warning">⚠ {{ battery.battery_percentage }}% — consider replacing</span>
{% else %}
{{ battery.battery_percentage }}%
{% endif %}
</td>
</tr>
{% endif %}
{% if battery.notes %}
<tr>
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Notes</td>
<td style="border:none;">{{ battery.notes }}</td>
</tr>
{% endif %}
</table>
</div>
<!-- Capacity History -->
<div class="card">
<h2>Capacity History</h2>
{% if capacity_tests|length >= 2 %}
<canvas id="capacity-chart"
style="width:100%;max-width:500px;height:140px;display:block;margin-bottom:1rem;"
width="500" height="140"></canvas>
{% endif %}
{% if capacity_tests %}
<div class="table-wrap">
<table class="responsive-table">
<thead>
<tr>
<th>Date</th>
<th>Capacity</th>
{% if battery.capacity_mah %}<th>Health</th>{% endif %}
<th>Notes</th>
<th></th>
</tr>
</thead>
<tbody>
{% for t in capacity_tests|sort(attribute='tested_date', reverse=True) %}
<tr>
<td data-label="Date">{{ t.tested_date }}</td>
<td data-label="Capacity">{{ t.tested_capacity_mah }} mAh</td>
{% 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 %}
<td data-label="Health"><span class="{{ hc }}">{{ pct }}%</span></td>
{% endif %}
<td data-label="Notes" class="text-muted">{{ t.notes or '—' }}</td>
<td data-label="">
<form class="inline" method="post"
action="{{ url_for('battery_capacity_test_delete', battery_id=battery.id, test_id=t.id) }}"
data-confirm="Delete this test record?" data-confirm-ok="Delete">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted" style="margin-bottom:0.75rem;">No test records yet.</p>
{% endif %}
<h3 style="font-size:1rem;margin:1rem 0 0.5rem;color:var(--text-h2);">Add Test Record</h3>
<form method="post" action="{{ url_for('battery_capacity_test_add', battery_id=battery.id) }}"
style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:flex-end;">
<div class="form-group" style="margin:0;flex:1;min-width:120px;">
<label>Capacity (mAh)</label>
<input type="number" name="tested_capacity_mah" min="1" placeholder="e.g. 1850" required>
</div>
<div class="form-group" style="margin:0;flex:1;min-width:140px;">
<label>Date</label>
<input type="date" name="tested_date" required>
</div>
<div class="form-group" style="margin:0;flex:2;min-width:160px;">
<label>Notes (optional)</label>
<input type="text" name="notes" placeholder="e.g. after 50 cycles">
</div>
<div style="padding-bottom:1rem;">
<button class="btn btn-primary" type="submit">Add</button>
</div>
</form>
</div>
<!-- Charge History -->
<div class="card">
<h2>Charge History</h2>
{% if charge_logs %}
<div class="table-wrap">
<table class="responsive-table">
<thead>
<tr>
<th>Date</th>
<th>+Cycle</th>
<th>Notes</th>
<th></th>
</tr>
</thead>
<tbody>
{% for log in charge_logs %}
<tr>
<td data-label="Date">{{ log.charged_date }}</td>
<td data-label="+Cycle">{{ '✓' if log.increment_cycles else '—' }}</td>
<td data-label="Notes" class="text-muted">{{ log.notes or '—' }}</td>
<td data-label="">
<form class="inline" method="post"
action="{{ url_for('battery_charge_log_delete', battery_id=battery.id, log_id=log.id) }}"
data-confirm="Delete this charge log entry?" data-confirm-ok="Delete">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted" style="margin-bottom:0.75rem;">No charge log entries yet.</p>
{% endif %}
<h3 style="font-size:1rem;margin:1rem 0 0.5rem;color:var(--text-h2);">Add Charge Entry</h3>
<form method="post" action="{{ url_for('battery_charge_log_add', battery_id=battery.id) }}"
style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:flex-end;">
<div class="form-group" style="margin:0;flex:1;min-width:140px;">
<label>Date</label>
<input type="date" name="charged_date" required>
</div>
<div class="form-group" style="margin:0;align-self:flex-end;padding-bottom:1rem;">
<label style="display:flex;align-items:center;gap:0.4rem;font-weight:normal;cursor:pointer;">
<input type="checkbox" name="increment_cycles" value="1" checked>
Increment charge cycles
</label>
</div>
<div class="form-group" style="margin:0;flex:2;min-width:160px;">
<label>Notes (optional)</label>
<input type="text" name="notes" placeholder="e.g. trickle charge overnight">
</div>
<div style="padding-bottom:1rem;">
<button class="btn btn-primary" type="submit">Add</button>
</div>
</form>
</div>
<!-- Edit Details -->
<div class="card">
<h2>Edit Details</h2>
<form method="post" action="{{ url_for('battery_edit_details', battery_id=battery.id) }}">
<div class="form-grid-2col" style="display:grid;grid-template-columns:1fr 1fr;gap:0 1rem;">
<div class="form-group">
<label>Size</label>
<select id="size-select" onchange="metaSelectChanged(this,'size')">
<option value="">— none —</option>
{% for opt in ['AA','AAA','C','D','9V','18650','21700','14500','26650','CR2032','CR123A'] %}
<option value="{{ opt }}" {% if battery.size == opt %}selected{% endif %}>{{ opt }}</option>
{% endfor %}
<option value="__new__" {% if battery.size and battery.size not in ['AA','AAA','C','D','9V','18650','21700','14500','26650','CR2032','CR123A'] %}selected{% endif %}>Other…</option>
</select>
<input type="text" id="size" name="size" value="{{ battery.size or '' }}"
placeholder="Enter size"
style="display:{% if battery.size and battery.size not in ['AA','AAA','C','D','9V','18650','21700','14500','26650','CR2032','CR123A'] %}''{% else %}none{% endif %};margin-top:0.4rem;">
</div>
<div class="form-group">
<label>Chemistry</label>
<select id="chemistry-select" onchange="metaSelectChanged(this,'chemistry')">
<option value="">— none —</option>
{% for opt in ['NiMH','Alkaline','Li-ion','LiFePO4','NiCd','Zinc-Carbon','Li-MnO2'] %}
<option value="{{ opt }}" {% if battery.chemistry == opt %}selected{% endif %}>{{ opt }}</option>
{% endfor %}
<option value="__new__" {% if battery.chemistry and battery.chemistry not in ['NiMH','Alkaline','Li-ion','LiFePO4','NiCd','Zinc-Carbon','Li-MnO2'] %}selected{% endif %}>Other…</option>
</select>
<input type="text" id="chemistry" name="chemistry" value="{{ battery.chemistry or '' }}"
placeholder="Enter chemistry"
style="display:{% if battery.chemistry and battery.chemistry not in ['NiMH','Alkaline','Li-ion','LiFePO4','NiCd','Zinc-Carbon','Li-MnO2'] %}''{% else %}none{% endif %};margin-top:0.4rem;">
</div>
<div class="form-group">
<label for="capacity_mah">Capacity (mAh)</label>
<input type="number" id="capacity_mah" name="capacity_mah" min="0"
value="{{ battery.capacity_mah or '' }}" placeholder="e.g. 2000">
</div>
<div class="form-group">
<label for="charge_cycles">Charge Cycles</label>
<input type="number" id="charge_cycles" name="charge_cycles" min="0"
value="{{ battery.charge_cycles or '' }}" placeholder="e.g. 50">
</div>
<div class="form-group">
<label for="purchase_date">Purchase Date</label>
<input type="date" id="purchase_date" name="purchase_date"
value="{{ battery.purchase_date or '' }}">
</div>
<div class="form-group">
<label for="battery_percentage">Battery % (optional)</label>
<input type="number" id="battery_percentage" name="battery_percentage" min="0" max="100"
value="{{ battery.battery_percentage if battery.battery_percentage is not none else '' }}"
placeholder="e.g. 85">
</div>
</div>
<div class="form-group">
<label>Storage Location</label>
<select id="storage-select" onchange="metaSelectChanged(this,'storage_location')">
<option value="">— none —</option>
{% for loc in storage_locations|default([]) %}
<option value="{{ loc }}" {% if battery.storage_location == loc %}selected{% endif %}>{{ loc }}</option>
{% endfor %}
<option value="__new__" {% if battery.storage_location and battery.storage_location not in storage_locations|default([]) %}selected{% endif %}> New location…</option>
</select>
<input type="text" id="storage_location" name="storage_location"
value="{{ battery.storage_location or '' }}"
placeholder="e.g. Drawer 2, Toolbox, Shelf A"
style="display:{% if battery.storage_location and battery.storage_location not in storage_locations|default([]) %}''{% else %}none{% endif %};margin-top:0.4rem;">
</div>
<div class="form-group">
<label for="notes">Notes</label>
<textarea id="notes" name="notes" placeholder="No notes yet…">{{ battery.notes or '' }}</textarea>
</div>
<button class="btn btn-primary" type="submit">Save Details</button>
</form>
</div>
<!-- Actions -->
<div class="card">
<h2>Actions</h2>
<div class="form-actions">
{% if battery.is_available() %}
<a class="btn btn-primary" href="{{ url_for('battery_assign', battery_id=battery.id) }}">Assign to Device</a>
{% endif %}
{% if battery.is_installed() %}
<form method="post" action="{{ url_for('battery_unassign', battery_id=battery.id) }}">
<button class="btn btn-warning" type="submit">Unassign</button>
</form>
{% endif %}
{% if not battery.is_retired() %}
<form method="post" action="{{ url_for('battery_retire', battery_id=battery.id) }}">
<button class="btn btn-secondary" type="submit">Retire Battery</button>
</form>
{% else %}
<form method="post" action="{{ url_for('battery_unretire', battery_id=battery.id) }}">
<button class="btn btn-secondary" type="submit">Unretire Battery</button>
</form>
{% endif %}
<a class="btn btn-danger" href="{{ url_for('battery_delete', battery_id=battery.id) }}">Delete Battery</a>
</div>
</div>
<a class="text-muted" href="{{ url_for('dashboard') }}">&larr; Back to Dashboard</a>
<script>
(function() {
var canvas = document.getElementById('capacity-chart');
if (!canvas) return;
var tests = {{ capacity_tests | map(attribute='tested_capacity_mah') | list | tojson }};
var labels = {{ capacity_tests | map(attribute='tested_date') | list | tojson }};
if (tests.length < 2) return;
var s = getComputedStyle(document.documentElement);
var lineColor = s.getPropertyValue('--link').trim() || '#2563eb';
var textColor = s.getPropertyValue('--text-muted').trim() || '#6b7280';
var gridColor = s.getPropertyValue('--border').trim() || '#e2e8f0';
var dpr = window.devicePixelRatio || 1;
var W = canvas.offsetWidth || 500, H = 140;
canvas.width = W * dpr; canvas.height = H * dpr;
canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
var ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
var PAD = {top: 12, right: 16, bottom: 28, left: 52};
var cW = W - PAD.left - PAD.right;
var cH = H - PAD.top - PAD.bottom;
var minV = Math.min.apply(null, tests), maxV = Math.max.apply(null, tests);
var pad = (maxV - minV || 100) * 0.1;
minV -= pad; maxV += pad;
var range = maxV - minV;
function xOf(i) { return PAD.left + (i / (tests.length - 1)) * cW; }
function yOf(v) { return PAD.top + cH - ((v - minV) / range) * cH; }
// Horizontal grid lines
ctx.lineWidth = 0.5;
[0, 0.5, 1].forEach(function(t) {
var y = PAD.top + cH * (1 - t);
ctx.strokeStyle = gridColor;
ctx.beginPath(); ctx.moveTo(PAD.left, y); ctx.lineTo(PAD.left + cW, y); ctx.stroke();
ctx.fillStyle = textColor; ctx.font = '10px system-ui'; ctx.textAlign = 'right';
ctx.fillText(Math.round(minV + t * range) + '', PAD.left - 4, y + 3);
});
// Line
ctx.strokeStyle = lineColor; ctx.lineWidth = 2; ctx.lineJoin = 'round';
ctx.beginPath();
tests.forEach(function(v, i) {
i === 0 ? ctx.moveTo(xOf(i), yOf(v)) : ctx.lineTo(xOf(i), yOf(v));
});
ctx.stroke();
// Dots + date labels (first and last only)
tests.forEach(function(v, i) {
ctx.fillStyle = lineColor;
ctx.beginPath(); ctx.arc(xOf(i), yOf(v), 3, 0, Math.PI * 2); ctx.fill();
if (i === 0 || i === tests.length - 1) {
ctx.fillStyle = textColor; ctx.font = '9px system-ui';
ctx.textAlign = i === 0 ? 'left' : 'right';
ctx.fillText(labels[i], xOf(i), H - 4);
}
});
}());
function metaSelectChanged(sel, inputId) {
var input = document.getElementById(inputId);
if (sel.value === '__new__') {
input.style.display = '';
input.value = '';
input.focus();
} else {
input.style.display = 'none';
input.value = sel.value;
}
}
</script>
{% endblock %}