725 lines
32 KiB
HTML
725 lines
32 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}{{ device.name }} — Battery Tracker{% endblock %}
|
||
|
||
{% block content %}
|
||
{% if device.parent %}
|
||
<div style="margin-bottom:0.5rem;font-size:0.9rem;">
|
||
<a href="{{ url_for('device_detail', device_id=device.parent.id) }}">← {{ device.parent.name }}</a>
|
||
<span class="text-muted"> / {{ device.name }}</span>
|
||
</div>
|
||
{% endif %}
|
||
<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;">{% if device.has_children() %}{% set ns = namespace(inst=0, slots=0) %}{% for c in device.children %}{% set ns.inst = ns.inst + c.installed_count() %}{% set ns.slots = ns.slots + c.battery_slots %}{% endfor %}{{ ns.inst }} / {{ ns.slots }} (across sub-components){% else %}{{ device.installed_count() }} / {{ device.battery_slots }} used{% endif %}</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.battery_size %}
|
||
<tr>
|
||
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Battery Size</td>
|
||
<td style="border:none;">{{ device.battery_size }}</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.is_subcomponent() and device.parent %}
|
||
{% if device.parent.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.parent.device_type }} <span class="text-muted" style="font-size:0.85em;">(inherited)</span></td>
|
||
</tr>
|
||
{% endif %}
|
||
{% if device.parent.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.parent.location }} <span class="text-muted" style="font-size:0.85em;">(inherited)</span></td>
|
||
</tr>
|
||
{% endif %}
|
||
{% 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 device.parent %}
|
||
<tr>
|
||
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Part of</td>
|
||
<td style="border:none;"><a href="{{ url_for('device_detail', device_id=device.parent.id) }}">{{ device.parent.name }}</a></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>
|
||
|
||
{% if device.children or not device.is_subcomponent() %}
|
||
<div class="card">
|
||
{% if device.children %}
|
||
<h2>Sub-components</h2>
|
||
<div style="display:flex;flex-direction:column;gap:0.5rem;margin-bottom:0.75rem;">
|
||
{% for child in device.children %}
|
||
{% set child_free = child.battery_slots - child.installed_count() %}
|
||
{% set child_batteries = children_avail.get(child.id, []) %}
|
||
<details style="border:1px solid var(--border);border-radius:6px;background:var(--bg-card);">
|
||
<summary style="padding:0.6rem 0.9rem;cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between;">
|
||
<span>
|
||
<strong>{{ child.name }}</strong>
|
||
<small class="text-muted" style="margin-left:0.5rem;">{{ child.installed_count() }}/{{ child.battery_slots }} slots · {{ child.battery_size }}{% if child.ha_entity_id %} · HA{% endif %}</small>
|
||
</span>
|
||
<span style="display:flex;gap:0.5rem;align-items:center;">
|
||
{% if child_free > 0 and child_batteries %}<small class="text-muted">{{ child_batteries|length }} available</small>{% endif %}
|
||
<a href="{{ url_for('device_detail', device_id=child.id) }}" onclick="event.stopPropagation();" style="font-size:0.8rem;">View</a>
|
||
</span>
|
||
</summary>
|
||
<div style="padding:0.6rem 0.9rem;border-top:1px solid var(--border);">
|
||
{% if child_free <= 0 %}
|
||
<p class="text-muted" style="margin:0;">No free slots.</p>
|
||
{% elif not child_batteries %}
|
||
<p class="text-muted" style="margin:0;">No compatible batteries available.</p>
|
||
{% else %}
|
||
<form method="post" action="{{ url_for('device_install_batch', device_id=child.id) }}"
|
||
data-confirm="Install the selected batteries into {{ child.name }}?"
|
||
data-confirm-ok="Install" data-confirm-class="btn-primary">
|
||
<div style="margin-bottom:0.4rem;">
|
||
<label style="font-size:0.85rem;cursor:pointer;">
|
||
<input type="checkbox" id="select-all-child-{{ child.id }}" onchange="toggleAllChild{{ child.id }}(this)"> Select all
|
||
</label>
|
||
</div>
|
||
<div style="max-height:180px;overflow-y:auto;border:1px solid #cbd5e1;border-radius:4px;padding:0.4rem 0.6rem;">
|
||
{% for b in child_batteries %}
|
||
<div>
|
||
<label style="font-size:0.9rem;cursor:pointer;">
|
||
<input type="checkbox" class="child-bat-{{ child.id }}" name="battery_ids" value="{{ b.id }}">
|
||
{{ b.label }} — {{ b.brand }}{% if b.battery_percentage is not none %} ({{ b.battery_percentage }}%){% endif %}
|
||
</label>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
<button class="btn btn-sm btn-primary" type="submit" style="margin-top:0.5rem;">Install Selected</button>
|
||
</form>
|
||
<script>
|
||
(function() {
|
||
var FREE_{{ child.id }} = {{ child_free }};
|
||
var queue_{{ child.id }} = [];
|
||
document.querySelectorAll('.child-bat-{{ child.id }}').forEach(function(cb) {
|
||
cb.addEventListener('change', function() {
|
||
if (cb.checked) {
|
||
queue_{{ child.id }}.push(cb);
|
||
if (queue_{{ child.id }}.length > FREE_{{ child.id }}) {
|
||
queue_{{ child.id }}.shift().checked = false;
|
||
}
|
||
} else {
|
||
queue_{{ child.id }} = queue_{{ child.id }}.filter(function(c) { return c !== cb; });
|
||
}
|
||
document.getElementById('select-all-child-{{ child.id }}').checked = false;
|
||
});
|
||
});
|
||
window['toggleAllChild{{ child.id }}'] = function(masterCb) {
|
||
var all = Array.from(document.querySelectorAll('.child-bat-{{ child.id }}'));
|
||
if (masterCb.checked) {
|
||
queue_{{ child.id }} = [];
|
||
all.forEach(function(c) { c.checked = false; });
|
||
all.slice(0, FREE_{{ child.id }}).forEach(function(c) { c.checked = true; queue_{{ child.id }}.push(c); });
|
||
if (all.length > FREE_{{ child.id }}) masterCb.checked = false;
|
||
} else {
|
||
all.forEach(function(c) { c.checked = false; });
|
||
queue_{{ child.id }} = [];
|
||
}
|
||
};
|
||
})();
|
||
</script>
|
||
{% endif %}
|
||
</div>
|
||
</details>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<h2>Sub-components</h2>
|
||
<p class="text-muted" style="margin-bottom:0.75rem;">No sub-components. Batteries are tracked at the device level.</p>
|
||
{% endif %}
|
||
<a class="btn btn-sm btn-secondary" href="{{ url_for('device_add') }}?parent_id={{ device.id }}">+ Add Sub-component</a>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if device.has_children() %}
|
||
<div class="card" id="installed">
|
||
<h2>Installed Batteries</h2>
|
||
{% if flat_installed %}
|
||
<div class="table-wrap">
|
||
<table class="responsive-table">
|
||
<thead><tr>
|
||
<th>Component</th><th>Label</th><th>Brand</th>
|
||
{% if ha_enabled %}<th>Bat %</th>{% endif %}
|
||
<th>Last Charged</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
{% for b, comp_name, comp_id in flat_installed %}
|
||
<tr>
|
||
<td data-label="Component"><a href="{{ url_for('device_detail', device_id=comp_id) }}">{{ comp_name }}</a></td>
|
||
<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">⚠ {{ b.battery_percentage }}%</span>
|
||
{% else %}{{ b.battery_percentage }}%{% endif %}
|
||
{% else %}—{% endif %}
|
||
</td>
|
||
{% endif %}
|
||
<td data-label="Last Charged" class="text-muted">{{ b.charge_logs[-1].charged_date if b.charge_logs else '—' }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<p class="text-muted">No batteries installed across sub-components.</p>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if device.has_children() and flat_installed %}
|
||
<div class="card">
|
||
<h2>Charge All Installed Batteries</h2>
|
||
<form method="post" action="{{ url_for('device_charge_all', device_id=device.id) }}"
|
||
data-confirm="Log a charge for all installed batteries in {{ device.name }}?"
|
||
data-confirm-ok="Log Charge" data-confirm-class="btn-primary"
|
||
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. overnight charge">
|
||
</div>
|
||
<div style="padding-bottom:1rem;">
|
||
<button class="btn btn-primary" type="submit">Log Charge for All</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if not device.has_children() %}
|
||
<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{% if device.battery_size %} — showing {{ device.battery_size }} batteries only{% endif %}</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 editSizeSelectChanged(sel) {
|
||
var input = document.getElementById('edit-battery-size');
|
||
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>
|
||
{% endif %}
|
||
|
||
{% if not device.has_children() %}
|
||
<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>Last Charged</th><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="Last Charged" class="text-muted">{{ b.charge_logs[-1].charged_date if b.charge_logs else '—' }}</td>
|
||
<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 %}
|
||
{% if installed %}
|
||
<form class="inline" method="post"
|
||
action="{{ url_for('device_unassign_all', device_id=device.id) }}"
|
||
data-confirm="Unassign all batteries from {{ device.name }}?"
|
||
data-confirm-ok="Unassign" data-confirm-class="btn-warning"
|
||
style="margin-top:0.5rem;">
|
||
<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 All</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% if installed %}
|
||
<div class="card">
|
||
<h2>Charge All Installed Batteries</h2>
|
||
<form method="post" action="{{ url_for('device_charge_all', device_id=device.id) }}"
|
||
data-confirm="Log a charge for all installed batteries in {{ device.name }}?"
|
||
data-confirm-ok="Log Charge" data-confirm-class="btn-primary"
|
||
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. overnight charge">
|
||
</div>
|
||
<div style="padding-bottom:1rem;">
|
||
<button class="btn btn-primary" type="submit">Log Charge for All</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="card">
|
||
<h2>Install Specific Batteries{% if device.battery_size %} <small class="text-muted" style="font-weight:normal;font-size:0.8rem;">({{ device.battery_size }} only)</small>{% endif %}</h2>
|
||
{% if available_batteries %}
|
||
<form method="post" action="{{ url_for('device_install_batch', device_id=device.id) }}"
|
||
data-confirm="Install the selected batteries into {{ device.name }}?"
|
||
data-confirm-ok="Install" data-confirm-class="btn-primary">
|
||
<div style="margin-bottom:0.5rem;">
|
||
<label style="font-size:0.85rem;cursor:pointer;">
|
||
<input type="checkbox" id="select-all-avail" onchange="toggleAllAvail(this)"> Select all
|
||
</label>
|
||
</div>
|
||
<div style="max-height:200px;overflow-y:auto;border:1px solid #cbd5e1;border-radius:4px;padding:0.4rem 0.6rem;">
|
||
{% for b in available_batteries %}
|
||
<div>
|
||
<label style="font-size:0.9rem;cursor:pointer;">
|
||
<input type="checkbox" name="battery_ids" value="{{ b.id }}">
|
||
{{ b.label }} — {{ b.brand }}{% if b.battery_percentage is not none %} ({{ b.battery_percentage }}%){% endif %}
|
||
</label>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
<button class="btn btn-sm btn-primary" type="submit" style="margin-top:0.5rem;">Install Selected</button>
|
||
</form>
|
||
<script>
|
||
var FREE_SLOTS = {{ device.battery_slots - device.installed_count() }};
|
||
var checkedQueue = [];
|
||
|
||
document.querySelectorAll('input[name="battery_ids"]').forEach(function(cb) {
|
||
cb.addEventListener('change', function() {
|
||
if (cb.checked) {
|
||
checkedQueue.push(cb);
|
||
if (checkedQueue.length > FREE_SLOTS) {
|
||
var oldest = checkedQueue.shift();
|
||
oldest.checked = false;
|
||
}
|
||
} else {
|
||
checkedQueue = checkedQueue.filter(function(c) { return c !== cb; });
|
||
}
|
||
document.getElementById('select-all-avail').checked = false;
|
||
});
|
||
});
|
||
|
||
function toggleAllAvail(masterCb) {
|
||
var all = Array.from(document.querySelectorAll('input[name="battery_ids"]'));
|
||
if (masterCb.checked) {
|
||
checkedQueue = [];
|
||
all.forEach(function(c) { c.checked = false; });
|
||
all.slice(0, FREE_SLOTS).forEach(function(c) { c.checked = true; checkedQueue.push(c); });
|
||
if (all.length > FREE_SLOTS) masterCb.checked = false;
|
||
} else {
|
||
all.forEach(function(c) { c.checked = false; });
|
||
checkedQueue = [];
|
||
}
|
||
}
|
||
</script>
|
||
{% else %}
|
||
<p class="text-muted">No compatible batteries available.</p>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="card">
|
||
<h2>Logbook</h2>
|
||
{% if logbook_entries %}
|
||
<div style="display:flex;flex-direction:column;gap:0.6rem;margin-bottom:1rem;">
|
||
{% for entry in logbook_entries %}
|
||
<div style="border-left:3px solid var(--border);padding:0.35rem 0.75rem;
|
||
display:flex;justify-content:space-between;align-items:flex-start;gap:0.5rem;">
|
||
<div>
|
||
<div class="text-muted" style="font-size:0.78rem;margin-bottom:0.15rem;">{{ entry.recorded_at }}</div>
|
||
<div style="white-space:pre-wrap;">{{ entry.body }}</div>
|
||
</div>
|
||
<form method="post"
|
||
action="{{ url_for('device_logbook_delete', device_id=device.id, entry_id=entry.id) }}"
|
||
data-confirm="Delete this logbook entry?" data-confirm-ok="Delete">
|
||
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||
</form>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<p class="text-muted" style="margin-bottom:0.75rem;">No logbook entries yet.</p>
|
||
{% endif %}
|
||
<h3 style="font-size:1rem;margin:0.75rem 0 0.5rem;color:var(--text-h2);">Add Entry</h3>
|
||
<form method="post" action="{{ url_for('device_logbook_add', device_id=device.id) }}">
|
||
<div class="form-group" style="margin-bottom:0.5rem;">
|
||
<textarea name="body" placeholder="Write a note…" rows="2" required style="min-height:60px;"></textarea>
|
||
</div>
|
||
<button class="btn btn-primary" type="submit">Add Entry</button>
|
||
</form>
|
||
</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>
|
||
{% if not device.has_children() %}
|
||
<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>
|
||
{% endif %}
|
||
{% if not device.is_subcomponent() %}
|
||
<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>
|
||
{% endif %}
|
||
{% if not device.has_children() %}
|
||
<div class="form-group">
|
||
<label>Battery Size</label>
|
||
{% set _preset_sizes = ['AA','AAA','C','D','9V','CR2032','CR2025','CR2016','18650','14500','16340','26650','LR44/AG13'] %}
|
||
<select id="edit-battery-size-select" onchange="editSizeSelectChanged(this)">
|
||
<option value="">— none —</option>
|
||
{% for s in _preset_sizes %}
|
||
<option value="{{ s }}" {% if device.battery_size == s %}selected{% endif %}>{{ s }}</option>
|
||
{% endfor %}
|
||
{% for s in device_battery_sizes|default([]) %}
|
||
{% if s not in _preset_sizes %}
|
||
<option value="{{ s }}" {% if device.battery_size == s %}selected{% endif %}>{{ s }}</option>
|
||
{% endif %}
|
||
{% endfor %}
|
||
{% if device.battery_size and device.battery_size not in _preset_sizes and device.battery_size not in device_battery_sizes|default([]) %}
|
||
<option value="{{ device.battery_size }}" selected>{{ device.battery_size }}</option>
|
||
{% endif %}
|
||
<option value="__new__">Other…</option>
|
||
</select>
|
||
<input type="text" id="edit-battery-size" name="battery_size"
|
||
value="{{ device.battery_size or '' }}"
|
||
placeholder="e.g. CR123A"
|
||
style="display:{% if device.battery_size and device.battery_size not in _preset_sizes %}''{% else %}none{% endif %};margin-top:0.4rem;">
|
||
</div>
|
||
{% endif %}
|
||
{% if not device.is_subcomponent() %}
|
||
<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>
|
||
{% endif %}
|
||
|
||
<div class="form-group">
|
||
<label for="edit-notes">Notes</label>
|
||
<textarea id="edit-notes" name="notes">{{ device.notes or '' }}</textarea>
|
||
</div>
|
||
{% if not device.has_children() %}
|
||
<div class="form-group">
|
||
<label for="edit-parent">Part of Device (optional)</label>
|
||
<select id="edit-parent" name="parent_id">
|
||
<option value="">— none (top-level device) —</option>
|
||
{% for d in device_list_all|default([]) %}
|
||
{% if d.id != device.id and not d.is_subcomponent() and d.battery_slots == 0 and d.battery_size is none %}
|
||
<option value="{{ d.id }}" {% if device.parent_id == d.id %}selected{% endif %}>{{ d.name }}</option>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
{% endif %}
|
||
{% 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.
|
||
{% if device.has_children() %}
|
||
Sub-components ({{ device.children|map(attribute='name')|join(', ') }}) will become independent devices.
|
||
{% endif %}
|
||
</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 %}
|