Initial commit: Flask battery tracker app
- Flask + SQLAlchemy (MariaDB-compatible schema) battery tracking web app - 40 pre-seeded batteries (Eneloop, BONAI, Energizer NiMH) across 5 devices - Business rules: block retired assignment, brand-mix warnings, capacity checks - Mobile-friendly Jinja2 templates with inline CSS - waitress WSGI server via systemd user service (sbin/install-service.sh) - SQLite → MariaDB migration script (migrate_to_mariadb.py) - 26 passing acceptance tests (pytest + Flask test client) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Assign {{ battery.label }} — Battery Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Assign {{ battery.label }}</h1>
|
||||
<p class="text-muted" style="margin-bottom:1rem;">Brand: {{ battery.brand }}</p>
|
||||
|
||||
<div class="card">
|
||||
{% if devices %}
|
||||
<form method="post" action="{{ url_for('battery_assign', battery_id=battery.id) }}">
|
||||
<div class="form-group">
|
||||
<label>Select Device</label>
|
||||
{% for device in devices %}
|
||||
{% set full = device.installed_count() >= device.battery_slots %}
|
||||
{% set mix = device.installed_brands() and battery.brand not in device.installed_brands() %}
|
||||
<div style="margin-bottom:0.75rem;padding:0.6rem 0.75rem;border:1px solid #e2e8f0;border-radius:4px;
|
||||
{% if full %}opacity:0.5;{% endif %}background:#fff;">
|
||||
<label style="display:flex;align-items:center;gap:0.6rem;font-weight:normal;cursor:{% if full %}not-allowed{% else %}pointer{% endif %};">
|
||||
<input type="radio" name="device_id" value="{{ device.id }}"
|
||||
{% if full %}disabled{% endif %}
|
||||
style="cursor:{% if full %}not-allowed{% else %}pointer{% endif %};">
|
||||
<span>
|
||||
<strong>{{ device.name }}</strong>
|
||||
<span class="text-muted">({{ device.installed_count() }}/{{ device.battery_slots }} slots used)</span>
|
||||
{% if full %}
|
||||
<span class="badge badge-retired">Full</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</label>
|
||||
{% if mix and not full %}
|
||||
<p class="text-warning" style="margin-top:0.3rem;margin-left:1.6rem;">
|
||||
⚠ Already has {{ device.installed_brands()|join(', ') }} — mixing brands not recommended.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit">Assign Battery</button>
|
||||
<a class="btn btn-secondary" href="{{ url_for('battery_detail', battery_id=battery.id) }}">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="text-muted">No devices exist yet. <a href="{{ url_for('device_add') }}">Add a device first.</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Battery Tracker{% endblock %}</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: system-ui, sans-serif; font-size: 15px; background: #f5f5f5; color: #222; }
|
||||
a { color: #2563eb; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
/* Nav */
|
||||
nav {
|
||||
background: #1e40af;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
nav .brand { font-weight: 700; font-size: 1.1rem; margin-right: auto; color: #fff; }
|
||||
nav a { color: #bfdbfe; font-size: 0.9rem; padding: 0.25rem 0.5rem; border-radius: 4px; }
|
||||
nav a:hover { background: #1d4ed8; color: #fff; text-decoration: none; }
|
||||
|
||||
/* Layout */
|
||||
.container { max-width: 960px; margin: 1.5rem auto; padding: 0 1rem; }
|
||||
|
||||
/* Flash messages */
|
||||
.flash { padding: 0.6rem 1rem; border-radius: 4px; margin-bottom: 1rem; font-size: 0.9rem; }
|
||||
.flash.success { background: #dcfce7; color: #166534; border: 1px solid #86efac; }
|
||||
.flash.error { background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5; }
|
||||
.flash.warning { background: #fef9c3; color: #854d0e; border: 1px solid #fde047; }
|
||||
|
||||
/* Cards / boxes */
|
||||
.card { background: #fff; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,.1); padding: 1.25rem; margin-bottom: 1rem; }
|
||||
|
||||
/* Tables */
|
||||
.table-wrap { overflow-x: auto; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th { text-align: left; padding: 0.5rem 0.75rem; background: #f1f5f9; font-size: 0.8rem; text-transform: uppercase; letter-spacing: .05em; color: #64748b; border-bottom: 2px solid #e2e8f0; }
|
||||
td { padding: 0.5rem 0.75rem; border-bottom: 1px solid #e2e8f0; vertical-align: middle; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tr:hover td { background: #f8fafc; }
|
||||
|
||||
/* Status badges */
|
||||
.badge { display: inline-block; padding: 0.2em 0.55em; border-radius: 999px; font-size: 0.75rem; font-weight: 600; }
|
||||
.badge-available { background: #dcfce7; color: #166534; }
|
||||
.badge-installed { background: #dbeafe; color: #1e40af; }
|
||||
.badge-retired { background: #f1f5f9; color: #64748b; }
|
||||
.badge-warning { background: #fef9c3; color: #854d0e; }
|
||||
|
||||
/* Buttons */
|
||||
.btn { display: inline-block; padding: 0.4rem 0.9rem; border-radius: 4px; border: none; cursor: pointer; font-size: 0.875rem; font-family: inherit; text-decoration: none; }
|
||||
.btn:hover { text-decoration: none; }
|
||||
.btn-primary { background: #2563eb; color: #fff; }
|
||||
.btn-primary:hover { background: #1d4ed8; }
|
||||
.btn-danger { background: #dc2626; color: #fff; }
|
||||
.btn-danger:hover { background: #b91c1c; }
|
||||
.btn-warning { background: #d97706; color: #fff; }
|
||||
.btn-warning:hover { background: #b45309; }
|
||||
.btn-secondary { background: #e2e8f0; color: #334155; }
|
||||
.btn-secondary:hover { background: #cbd5e1; }
|
||||
.btn-sm { padding: 0.25rem 0.6rem; font-size: 0.8rem; }
|
||||
|
||||
/* Forms */
|
||||
.form-group { margin-bottom: 1rem; }
|
||||
label { display: block; font-size: 0.875rem; font-weight: 600; margin-bottom: 0.3rem; color: #374151; }
|
||||
input[type=text], input[type=number], select, textarea {
|
||||
width: 100%; padding: 0.45rem 0.65rem; border: 1px solid #d1d5db;
|
||||
border-radius: 4px; font-size: 0.9rem; font-family: inherit;
|
||||
}
|
||||
input:focus, select:focus, textarea:focus { outline: 2px solid #3b82f6; border-color: #3b82f6; }
|
||||
textarea { min-height: 80px; resize: vertical; }
|
||||
.form-actions { display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap; margin-top: 1.25rem; }
|
||||
|
||||
/* Headings */
|
||||
h1 { font-size: 1.5rem; margin-bottom: 1rem; color: #1e293b; }
|
||||
h2 { font-size: 1.15rem; margin-bottom: 0.75rem; color: #334155; }
|
||||
|
||||
/* Inline form (for POST buttons in tables) */
|
||||
form.inline { display: inline; }
|
||||
|
||||
/* Warning text */
|
||||
.text-warning { color: #b45309; font-size: 0.8rem; }
|
||||
.text-danger { color: #dc2626; }
|
||||
.text-muted { color: #6b7280; font-size: 0.85rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<span class="brand">Battery Tracker</span>
|
||||
<a href="{{ url_for('dashboard') }}">Dashboard</a>
|
||||
<a href="{{ url_for('device_list') }}">Devices</a>
|
||||
<a href="{{ url_for('battery_add') }}">+ Battery</a>
|
||||
<a href="{{ url_for('device_add') }}">+ Device</a>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,41 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Add Battery — Battery Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Add Battery</h1>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="{{ url_for('battery_add') }}">
|
||||
<div class="form-group">
|
||||
<label for="label">Label <span class="text-danger">*</span></label>
|
||||
<input type="text" id="label" name="label" value="{{ form_label|default('') }}"
|
||||
placeholder="e.g. ENL-17" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="brand">Brand <span class="text-danger">*</span></label>
|
||||
<input type="text" id="brand" name="brand" value="{{ form_brand|default('') }}"
|
||||
placeholder="e.g. Panasonic Eneloop" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="status">Initial Status</label>
|
||||
<select id="status" name="status">
|
||||
<option value="available" {% if form_status|default('available') == 'available' %}selected{% endif %}>Available</option>
|
||||
<option value="installed" {% if form_status|default('') == 'installed' %}selected{% endif %}>Installed</option>
|
||||
<option value="retired" {% if form_status|default('') == 'retired' %}selected{% endif %}>Retired</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="notes">Notes</label>
|
||||
<textarea id="notes" name="notes" placeholder="Optional notes…">{{ form_notes|default('') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit">Add Battery</button>
|
||||
<a class="btn btn-secondary" href="{{ url_for('dashboard') }}">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Delete {{ battery.label }} — Battery Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Delete Battery</h1>
|
||||
|
||||
<div class="card">
|
||||
<p style="margin-bottom:1rem;">
|
||||
You are about to <strong>permanently delete</strong> battery
|
||||
<strong>{{ battery.label }}</strong> ({{ battery.brand }}).
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
|
||||
{% if battery.is_installed() %}
|
||||
<p class="text-warning" style="margin-bottom:1rem;">
|
||||
⚠ This battery is currently installed in
|
||||
<strong>{{ battery.device.name }}</strong>.
|
||||
Deleting it will free that slot.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-actions">
|
||||
<form method="post" action="{{ url_for('battery_delete', battery_id=battery.id) }}">
|
||||
<button class="btn btn-danger" type="submit">Confirm Delete</button>
|
||||
</form>
|
||||
<a class="btn btn-secondary" href="{{ url_for('battery_detail', battery_id=battery.id) }}">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,70 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ battery.label }} — Battery Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ battery.label }}</h1>
|
||||
|
||||
<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>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="card">
|
||||
<h2>Notes</h2>
|
||||
<form method="post" action="{{ url_for('battery_edit_notes', battery_id=battery.id) }}">
|
||||
<div class="form-group">
|
||||
<textarea name="notes" placeholder="No notes yet…">{{ battery.notes or '' }}</textarea>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Save Notes</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>
|
||||
{% 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') }}">← Back to Dashboard</a>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,88 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard — Battery Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Battery Dashboard</h1>
|
||||
|
||||
{% set total = batteries|length %}
|
||||
{% set available = batteries|selectattr('status','eq','available')|list|length %}
|
||||
{% set installed = batteries|selectattr('status','eq','installed')|list|length %}
|
||||
{% set retired = batteries|selectattr('status','eq','retired')|list|length %}
|
||||
|
||||
<div style="display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1.25rem;">
|
||||
<div class="card" style="flex:1;min-width:120px;text-align:center;">
|
||||
<div style="font-size:1.8rem;font-weight:700;">{{ total }}</div>
|
||||
<div class="text-muted">Total</div>
|
||||
</div>
|
||||
<div class="card" style="flex:1;min-width:120px;text-align:center;">
|
||||
<div style="font-size:1.8rem;font-weight:700;color:#166534;">{{ available }}</div>
|
||||
<div class="text-muted">Available</div>
|
||||
</div>
|
||||
<div class="card" style="flex:1;min-width:120px;text-align:center;">
|
||||
<div style="font-size:1.8rem;font-weight:700;color:#1e40af;">{{ installed }}</div>
|
||||
<div class="text-muted">Installed</div>
|
||||
</div>
|
||||
<div class="card" style="flex:1;min-width:120px;text-align:center;">
|
||||
<div style="font-size:1.8rem;font-weight:700;color:#64748b;">{{ retired }}</div>
|
||||
<div class="text-muted">Retired</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>Brand</th>
|
||||
<th>Status</th>
|
||||
<th>Assigned To</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for b in batteries %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('battery_detail', battery_id=b.id) }}"><strong>{{ b.label }}</strong></a></td>
|
||||
<td>{{ b.brand }}</td>
|
||||
<td>
|
||||
<span class="badge badge-{{ b.status }}">{{ b.status|capitalize }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if b.device %}
|
||||
<a href="{{ url_for('device_detail', device_id=b.device.id) }}">{{ b.device.name }}</a>
|
||||
{% if b.device.has_mixed_brands() %}
|
||||
<span class="badge badge-warning" title="Mixed brands in this device">⚠ mixed</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="white-space:nowrap;">
|
||||
<a class="btn btn-sm btn-secondary" href="{{ url_for('battery_detail', battery_id=b.id) }}">View</a>
|
||||
|
||||
{% if b.is_available() %}
|
||||
<a class="btn btn-sm btn-primary" href="{{ url_for('battery_assign', battery_id=b.id) }}">Assign</a>
|
||||
{% endif %}
|
||||
|
||||
{% if b.is_installed() %}
|
||||
<form class="inline" method="post" action="{{ url_for('battery_unassign', battery_id=b.id) }}">
|
||||
<button class="btn btn-sm btn-warning" type="submit">Unassign</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if not b.is_retired() %}
|
||||
<form class="inline" method="post" action="{{ url_for('battery_retire', battery_id=b.id) }}">
|
||||
<button class="btn btn-sm btn-secondary" type="submit">Retire</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="text-muted" style="text-align:center;padding:1rem;">No batteries found. <a href="{{ url_for('battery_add') }}">Add one.</a></td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Add Device — Battery Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Add Device</h1>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="{{ url_for('device_add') }}">
|
||||
<div class="form-group">
|
||||
<label for="name">Device Name <span class="text-danger">*</span></label>
|
||||
<input type="text" id="name" name="name" value="{{ form_name|default('') }}"
|
||||
placeholder="e.g. Game Controller" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="battery_slots">Battery Slots <span class="text-danger">*</span></label>
|
||||
<input type="number" id="battery_slots" name="battery_slots"
|
||||
value="{{ form_slots|default(1) }}" min="1" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="notes">Notes</label>
|
||||
<textarea id="notes" name="notes" placeholder="Optional notes…">{{ form_notes|default('') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit">Add Device</button>
|
||||
<a class="btn btn-secondary" href="{{ url_for('device_list') }}">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,69 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ device.name }} — Battery Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ device.name }}</h1>
|
||||
|
||||
<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;">Slots</td>
|
||||
<td style="border:none;">{{ device.installed_count() }} / {{ device.battery_slots }} used</td>
|
||||
</tr>
|
||||
{% if device.has_mixed_brands() %}
|
||||
<tr>
|
||||
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Warning</td>
|
||||
<td style="border:none;"><span class="badge badge-warning">⚠ Mixed brands installed</span></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if device.notes %}
|
||||
<tr>
|
||||
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Notes</td>
|
||||
<td style="border:none;">{{ device.notes }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Installed Batteries</h2>
|
||||
{% set installed = device.batteries | selectattr('status', 'eq', 'installed') | list %}
|
||||
{% if installed %}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Label</th><th>Brand</th><th>Notes</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for b in installed %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('battery_detail', battery_id=b.id) }}">{{ b.label }}</a></td>
|
||||
<td>{{ b.brand }}</td>
|
||||
<td class="text-muted">{{ b.notes or '—' }}</td>
|
||||
<td>
|
||||
<form class="inline" method="post" action="{{ url_for('battery_unassign', battery_id=b.id) }}">
|
||||
<button class="btn btn-sm btn-warning" type="submit">Unassign</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted">No batteries installed.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Delete Device</h2>
|
||||
<p style="margin-bottom:1rem;" class="text-muted">
|
||||
Deleting this device will unassign all installed batteries and mark them available.
|
||||
</p>
|
||||
<form method="post" action="{{ url_for('device_delete', device_id=device.id) }}">
|
||||
<button class="btn btn-danger" type="submit">Delete {{ device.name }}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<a class="text-muted" href="{{ url_for('device_list') }}">← Back to Devices</a>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,58 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Devices — Battery Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Devices</h1>
|
||||
|
||||
<div class="card">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Slots</th>
|
||||
<th>Installed</th>
|
||||
<th>Brands</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for d in devices %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('device_detail', device_id=d.id) }}"><strong>{{ d.name }}</strong></a></td>
|
||||
<td>{{ d.battery_slots }}</td>
|
||||
<td>
|
||||
{{ d.installed_count() }} / {{ d.battery_slots }}
|
||||
{% if d.installed_count() >= d.battery_slots %}
|
||||
<span class="badge badge-retired">Full</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% set brands = d.installed_brands() %}
|
||||
{% if brands %}
|
||||
{{ brands|join(', ') }}
|
||||
{% if d.has_mixed_brands() %}
|
||||
<span class="badge badge-warning">⚠ mixed</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="white-space:nowrap;">
|
||||
<a class="btn btn-sm btn-secondary" href="{{ url_for('device_detail', device_id=d.id) }}">View</a>
|
||||
<form class="inline" method="post" action="{{ url_for('device_delete', device_id=d.id) }}"
|
||||
onsubmit="return confirm('Delete {{ d.name }}? All installed batteries will be unassigned.');">
|
||||
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="text-muted" style="text-align:center;padding:1rem;">No devices yet. <a href="{{ url_for('device_add') }}">Add one.</a></td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="btn btn-primary" href="{{ url_for('device_add') }}">+ Add Device</a>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user