372 lines
16 KiB
HTML
372 lines
16 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}{{ device.name }} — Battery Tracker{% endblock %}
|
||
|
||
{% block content %}
|
||
<h1>{{ device.name }}</h1>
|
||
|
||
<div class="card" style="position:relative;">
|
||
{% if ha_enabled and device.ha_entity_id and ha_live_pct is not none %}
|
||
{% set _pct = ha_live_pct %}
|
||
{% set _fill = ((_pct / 100) * 48) | int %}
|
||
{% if _pct < 20 %}{% set _color = '#ef4444' %}
|
||
{% elif _pct < 60 %}{% set _color = '#f59e0b' %}
|
||
{% else %}{% set _color = '#22c55e' %}{% endif %}
|
||
<div title="HA battery: {{ _pct }}%"
|
||
style="position:absolute;top:1rem;right:1rem;display:flex;flex-direction:column;align-items:center;gap:0.2rem;opacity:0.9;">
|
||
<svg width="54" height="26" viewBox="0 0 60 28" xmlns="http://www.w3.org/2000/svg">
|
||
<rect x="1" y="4" width="52" height="20" rx="3" ry="3"
|
||
fill="none" stroke="var(--text-muted)" stroke-width="2"/>
|
||
<rect x="54" y="10" width="5" height="8" rx="1.5"
|
||
fill="var(--text-muted)"/>
|
||
{% if _fill > 0 %}
|
||
<rect x="3" y="6" width="{{ _fill }}" height="16" rx="2"
|
||
fill="{{ _color }}"/>
|
||
{% endif %}
|
||
<text x="27" y="19" text-anchor="middle"
|
||
font-size="10" font-weight="600" fill="#fff"
|
||
style="text-shadow:0 0 3px rgba(0,0,0,0.6);">{{ _pct }}%</text>
|
||
</svg>
|
||
</div>
|
||
{% endif %}
|
||
<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.device_type %}
|
||
<tr>
|
||
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Type</td>
|
||
<td style="border:none;">{{ device.device_type }}</td>
|
||
</tr>
|
||
{% endif %}
|
||
{% 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.location %}
|
||
<tr>
|
||
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Location</td>
|
||
<td style="border:none;">{{ device.location }}</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 %}
|
||
{% if ha_enabled and device.ha_entity_id %}
|
||
<tr>
|
||
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">HA Live %</td>
|
||
<td style="border:none;">
|
||
{% if ha_live_pct is not none %}
|
||
{% if ha_live_pct < 20 %}
|
||
<span class="badge badge-warning">⚠ {{ ha_live_pct }}%</span>
|
||
{% else %}
|
||
{{ ha_live_pct }}%
|
||
{% endif %}
|
||
{% else %}
|
||
<span class="text-muted">—</span>
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endif %}
|
||
</table>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Install Batteries</h2>
|
||
{% set free_slots = device.battery_slots - device.installed_count() %}
|
||
<p class="text-muted" style="margin-bottom:0.75rem;">{{ free_slots }} slot(s) free</p>
|
||
<form method="post" action="{{ url_for('device_install', device_id=device.id) }}">
|
||
<div id="install-grid" style="display:grid;grid-template-columns:1fr auto;gap:0.4rem;max-width:400px;align-items:start;margin-bottom:0.5rem;">
|
||
<span style="font-weight:600;font-size:0.85rem;color:#64748b;">Brand</span>
|
||
<span style="font-weight:600;font-size:0.85rem;color:#64748b;">Qty</span>
|
||
<div class="install-row-pair" style="display:contents;">
|
||
<div>
|
||
<select onchange="brandSelectChanged(this)">
|
||
<option value="">— select —</option>
|
||
{% for b in brands|default([]) %}
|
||
<option value="{{ b }}">{{ b }}</option>
|
||
{% endfor %}
|
||
<option value="__new__">➕ New brand…</option>
|
||
</select>
|
||
<input type="text" name="brand[]" value=""
|
||
placeholder="Type brand name"
|
||
style="display:none;margin-top:0.3rem;padding:0.3rem 0.5rem;">
|
||
</div>
|
||
<input type="number" name="qty[]" value="0" min="0"
|
||
style="padding:0.3rem 0.5rem;width:4rem;text-align:center;">
|
||
</div>
|
||
</div>
|
||
<button type="button" class="btn btn-sm btn-secondary" onclick="addInstallRow()" style="margin-bottom:0.75rem;">+ Add brand</button>
|
||
<br>
|
||
<button class="btn btn-primary" type="submit">Install</button>
|
||
</form>
|
||
|
||
<script>
|
||
function editTypeSelectChanged(sel) {
|
||
var input = document.getElementById('edit-device-type');
|
||
if (sel.value === '__new__') {
|
||
input.style.display = ''; input.value = ''; input.focus();
|
||
} else {
|
||
input.style.display = 'none'; input.value = sel.value;
|
||
}
|
||
}
|
||
function editLocationSelectChanged(sel) {
|
||
var input = document.getElementById('edit-location');
|
||
if (sel.value === '__new__') {
|
||
input.style.display = ''; input.value = ''; input.focus();
|
||
} else {
|
||
input.style.display = 'none'; input.value = sel.value;
|
||
}
|
||
}
|
||
function brandSelectChanged(sel) {
|
||
var input = sel.nextElementSibling;
|
||
if (sel.value === '__new__') {
|
||
input.style.display = ''; input.value = ''; input.focus();
|
||
} else {
|
||
input.style.display = 'none';
|
||
input.value = (sel.value === '') ? '' : sel.value;
|
||
}
|
||
}
|
||
function addInstallRow() {
|
||
var grid = document.getElementById('install-grid');
|
||
var tmpl = grid.querySelector('.install-row-pair');
|
||
var clone = tmpl.cloneNode(true);
|
||
clone.querySelector('select').value = '';
|
||
clone.querySelector('input[type=text]').style.display = 'none';
|
||
clone.querySelector('input[type=text]').value = '';
|
||
clone.querySelector('input[type=number]').value = '0';
|
||
grid.appendChild(clone);
|
||
}
|
||
</script>
|
||
</div>
|
||
|
||
<div class="card" id="installed">
|
||
<h2>Installed Batteries</h2>
|
||
{% set installed = device.batteries | selectattr('status', 'eq', 'installed') | list %}
|
||
{% if installed %}
|
||
<div class="table-wrap">
|
||
<table class="responsive-table">
|
||
<thead>
|
||
<tr><th>Label</th><th>Brand</th>{% if ha_enabled %}<th>Bat %</th>{% endif %}<th>Notes</th><th>Actions</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for b in installed %}
|
||
<tr>
|
||
<td data-label="Label"><a href="{{ url_for('battery_detail', battery_id=b.id) }}">{{ b.label }}</a></td>
|
||
<td data-label="Brand">{{ b.brand }}</td>
|
||
{% if ha_enabled %}
|
||
<td data-label="Bat %">
|
||
{% if b.battery_percentage is not none %}
|
||
{% if b.battery_percentage < 20 %}
|
||
<span class="badge badge-warning" title="Low — consider replacing">⚠ {{ b.battery_percentage }}%</span>
|
||
{% else %}{{ b.battery_percentage }}%{% endif %}
|
||
{% else %}—{% endif %}
|
||
</td>
|
||
{% endif %}
|
||
<td data-label="Notes" class="text-muted">{{ b.notes or '—' }}</td>
|
||
<td data-label="Actions">
|
||
<form class="inline" method="post" action="{{ url_for('battery_unassign', battery_id=b.id) }}">
|
||
<input type="hidden" name="next" value="{{ url_for('device_detail', device_id=device.id) }}#installed">
|
||
<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>Install Specific Battery</h2>
|
||
{% if available_batteries %}
|
||
<form method="post" action="{{ url_for('device_install_one', device_id=device.id) }}">
|
||
<div class="form-group">
|
||
<label for="battery_id">Battery</label>
|
||
<select name="battery_id" id="battery_id">
|
||
<option value="">— select —</option>
|
||
{% for b in available_batteries %}
|
||
<option value="{{ b.id }}">{{ b.label }} — {{ b.brand }}
|
||
{%- if b.size %} {{ b.size }}{% endif %}
|
||
{%- if b.notes %} ({{ b.notes }}){% endif %}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<button class="btn btn-primary" type="submit">Install</button>
|
||
</form>
|
||
{% else %}
|
||
<p class="text-muted">No available batteries.</p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Edit Device</h2>
|
||
<form method="post" action="{{ url_for('device_edit', device_id=device.id) }}">
|
||
<div class="form-group">
|
||
<label for="edit-name">Name</label>
|
||
<input type="text" id="edit-name" name="name" value="{{ device.name }}" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-slots">Battery Slots</label>
|
||
<input type="number" id="edit-slots" name="battery_slots" value="{{ device.battery_slots }}" min="1" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Type</label>
|
||
{% set _preset_types = ['Remote Control','Game Controller','Flashlight','Lock','Sensor','Toy','Clock','Smoke Detector'] %}
|
||
<select id="edit-device-type-select" onchange="editTypeSelectChanged(this)">
|
||
<option value="">— none —</option>
|
||
{% for opt in _preset_types %}
|
||
<option value="{{ opt }}" {% if device.device_type == opt %}selected{% endif %}>{{ opt }}</option>
|
||
{% endfor %}
|
||
{% for opt in device_types|default([]) %}
|
||
{% if opt not in _preset_types %}
|
||
<option value="{{ opt }}" {% if device.device_type == opt %}selected{% endif %}>{{ opt }}</option>
|
||
{% endif %}
|
||
{% endfor %}
|
||
<option value="__new__" {% if device.device_type and device.device_type not in _preset_types and device.device_type not in device_types|default([]) %}selected{% endif %}>Other…</option>
|
||
</select>
|
||
<input type="text" id="edit-device-type" name="device_type"
|
||
value="{{ device.device_type or '' }}"
|
||
placeholder="Enter device type"
|
||
style="display:{% if device.device_type and device.device_type not in _preset_types %}''{% else %}none{% endif %};margin-top:0.4rem;">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Location</label>
|
||
<select id="edit-location-select" onchange="editLocationSelectChanged(this)">
|
||
<option value="">— none —</option>
|
||
{% for loc in device_locations|default([]) %}
|
||
<option value="{{ loc }}" {% if device.location == loc %}selected{% endif %}>{{ loc }}</option>
|
||
{% endfor %}
|
||
{% if device.location and device.location not in device_locations|default([]) %}
|
||
<option value="{{ device.location }}" selected>{{ device.location }}</option>
|
||
{% endif %}
|
||
<option value="__new__">➕ New location…</option>
|
||
</select>
|
||
<input type="text" id="edit-location" name="location"
|
||
value="{{ device.location or '' }}"
|
||
placeholder="e.g. Living Room, Bedroom"
|
||
style="display:{% if device.location and device.location not in device_locations|default([]) %}''{% else %}none{% endif %};margin-top:0.4rem;">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="edit-notes">Notes</label>
|
||
<textarea id="edit-notes" name="notes">{{ device.notes or '' }}</textarea>
|
||
</div>
|
||
{% if ha_enabled %}
|
||
<div class="form-group">
|
||
<label for="edit-ha-entity">Home Assistant Entity ID</label>
|
||
<div style="position:relative;">
|
||
<input type="text" id="edit-ha-entity" name="ha_entity_id"
|
||
value="{{ device.ha_entity_id or '' }}"
|
||
placeholder="e.g. sensor.tv_remote_battery"
|
||
autocomplete="off">
|
||
<div id="ha-entity-dropdown"
|
||
style="display:none;position:absolute;left:0;right:0;top:100%;z-index:200;
|
||
background:var(--bg-card);border:1px solid var(--border);border-radius:4px;
|
||
max-height:220px;overflow-y:auto;box-shadow:0 4px 12px rgba(0,0,0,0.15);"></div>
|
||
</div>
|
||
<small class="text-muted" id="ha-entities-status"
|
||
style="display:block;margin-top:0.25rem;font-size:0.8rem;"></small>
|
||
</div>
|
||
{% endif %}
|
||
<button class="btn btn-primary" type="submit">Save Changes</button>
|
||
</form>
|
||
</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>
|
||
|
||
{% if ha_enabled %}
|
||
<script>
|
||
(function() {
|
||
var input = document.getElementById('edit-ha-entity');
|
||
var dropdown = document.getElementById('ha-entity-dropdown');
|
||
var status = document.getElementById('ha-entities-status');
|
||
if (!input || !dropdown) return;
|
||
|
||
var allEntities = [];
|
||
|
||
function renderDropdown(query) {
|
||
var q = query.toLowerCase();
|
||
var matches = q
|
||
? allEntities.filter(function(e) {
|
||
return e.entity_id.toLowerCase().indexOf(q) !== -1
|
||
|| (e.friendly_name && e.friendly_name.toLowerCase().indexOf(q) !== -1);
|
||
})
|
||
: allEntities;
|
||
if (!matches.length) { dropdown.style.display = 'none'; return; }
|
||
dropdown.innerHTML = '';
|
||
matches.slice(0, 60).forEach(function(e) {
|
||
var item = document.createElement('div');
|
||
item.style.cssText = 'padding:0.5rem 0.75rem;cursor:pointer;border-bottom:1px solid var(--border);';
|
||
var label = document.createElement('div');
|
||
label.style.cssText = 'font-size:0.875rem;word-break:break-all;';
|
||
label.textContent = e.entity_id;
|
||
item.appendChild(label);
|
||
if (e.friendly_name && e.friendly_name !== e.entity_id) {
|
||
var sub = document.createElement('div');
|
||
sub.style.cssText = 'font-size:0.75rem;color:var(--text-muted);';
|
||
sub.textContent = e.friendly_name;
|
||
item.appendChild(sub);
|
||
}
|
||
item.addEventListener('mousedown', function(ev) { ev.preventDefault(); });
|
||
item.addEventListener('click', function() {
|
||
input.value = e.entity_id;
|
||
dropdown.style.display = 'none';
|
||
input.focus();
|
||
});
|
||
item.addEventListener('mouseover', function() { this.style.background = 'var(--bg-hover)'; });
|
||
item.addEventListener('mouseout', function() { this.style.background = ''; });
|
||
dropdown.appendChild(item);
|
||
});
|
||
dropdown.style.display = 'block';
|
||
}
|
||
|
||
input.addEventListener('input', function() { renderDropdown(this.value); });
|
||
input.addEventListener('focus', function() { if (allEntities.length) renderDropdown(this.value); });
|
||
input.addEventListener('blur', function() { setTimeout(function() { dropdown.style.display = 'none'; }, 150); });
|
||
input.addEventListener('keydown', function(e) { if (e.key === 'Escape') dropdown.style.display = 'none'; });
|
||
document.addEventListener('click', function(e) {
|
||
if (!input.contains(e.target) && !dropdown.contains(e.target)) dropdown.style.display = 'none';
|
||
});
|
||
|
||
function onEntities(entities) {
|
||
allEntities = entities;
|
||
if (status) status.textContent = entities.length ? entities.length + ' battery entities available' : '';
|
||
}
|
||
|
||
try {
|
||
var cached = sessionStorage.getItem('ha_battery_entities');
|
||
if (cached) { onEntities(JSON.parse(cached)); return; }
|
||
} catch(e) {}
|
||
|
||
if (status) status.textContent = 'Loading HA entities\u2026';
|
||
fetch('/ha/entities')
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(entities) {
|
||
try { sessionStorage.setItem('ha_battery_entities', JSON.stringify(entities)); } catch(e) {}
|
||
onEntities(entities);
|
||
})
|
||
.catch(function() { if (status) status.textContent = ''; });
|
||
}());
|
||
</script>
|
||
{% endif %}
|
||
{% endblock %}
|