206 lines
9.5 KiB
HTML
206 lines
9.5 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}{{ board.name }} – Location Management{% endblock %}
|
||
|
||
{% block content %}
|
||
<nav aria-label="breadcrumb" class="mb-3">
|
||
<ol class="breadcrumb">
|
||
<li class="breadcrumb-item"><a href="{{ url_for('boards.list_boards') }}">Boards</a></li>
|
||
<li class="breadcrumb-item active">{{ board.name }}</li>
|
||
</ol>
|
||
</nav>
|
||
|
||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||
<div>
|
||
<h2 class="fw-bold mb-0">{{ board.name }}</h2>
|
||
<span class="badge text-bg-secondary">{{ board.board_type }}</span>
|
||
<span class="badge {% if board.is_online %}text-bg-success{% else %}text-bg-secondary{% endif %} ms-1">
|
||
{% if board.is_online %}Online{% else %}Offline{% endif %}
|
||
</span>
|
||
<span class="text-secondary small ms-2 font-monospace">{{ board.host }}:{{ board.port }}</span>
|
||
</div>
|
||
{% if current_user.is_admin() %}
|
||
<div class="d-flex gap-2">
|
||
<a href="{{ url_for('boards.edit_entities', board_id=board.id) }}" class="btn btn-outline-info">
|
||
<i class="bi bi-palette me-1"></i> Configure Entities
|
||
</a>
|
||
<a href="{{ url_for('boards.edit_board', board_id=board.id) }}" class="btn btn-outline-secondary">
|
||
<i class="bi bi-pencil me-1"></i> Edit
|
||
</a>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="row g-4">
|
||
<!-- ── Relay controls ────────────────────────────────────────────────────── -->
|
||
<div class="col-lg-6">
|
||
<div class="card border-0 rounded-4 h-100">
|
||
<div class="card-header bg-transparent fw-semibold pt-3">
|
||
<i class="bi bi-lightning-charge me-1 text-warning"></i> Relay Controls
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row g-3">
|
||
{% for n in range(1, board.num_relays + 1) %}
|
||
{% set relay_key = "relay_" ~ n %}
|
||
{% set is_on = board.relay_states.get(relay_key, false) %}
|
||
{% set e = board.get_relay_entity(n) %}
|
||
<div class="col-6">
|
||
<div id="relay-card-{{ n }}" class="card rounded-3 border-{{ e.on_color if is_on else e.off_color }} h-100">
|
||
<div class="card-body text-center py-3">
|
||
|
||
<i id="relay-icon-{{ n }}" class="bi {{ e.icon }} mb-2 text-{{ e.on_color if is_on else e.off_color }}"
|
||
style="font-size:2rem"></i>
|
||
<div class="fs-6 fw-semibold mb-1">{{ e.name }}</div>
|
||
|
||
<div class="mb-3">
|
||
<span id="relay-badge-{{ n }}" class="badge text-bg-{{ e.on_color if is_on else e.off_color }}">
|
||
{{ e.on_label if is_on else e.off_label }}
|
||
</span>
|
||
</div>
|
||
<div class="d-flex justify-content-center gap-2">
|
||
<form method="POST" action="{{ url_for('boards.set_relay_view', board_id=board.id, relay_num=n) }}">
|
||
<button id="relay-on-btn-{{ n }}" class="btn btn-sm btn-{{ e.on_color }}" {% if is_on %}disabled{% endif %}>{{ e.on_label }}</button>
|
||
<input type="hidden" name="state" value="on" />
|
||
</form>
|
||
<form method="POST" action="{{ url_for('boards.toggle_relay_view', board_id=board.id, relay_num=n) }}">
|
||
<button class="btn btn-sm btn-outline-primary">Toggle</button>
|
||
</form>
|
||
<form method="POST" action="{{ url_for('boards.set_relay_view', board_id=board.id, relay_num=n) }}">
|
||
<button id="relay-off-btn-{{ n }}" class="btn btn-sm btn-{{ e.off_color }}" {% if not is_on %}disabled{% endif %}>{{ e.off_label }}</button>
|
||
<input type="hidden" name="state" value="off" />
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Input states ──────────────────────────────────────────────────────── -->
|
||
<div class="col-lg-6">
|
||
<div class="card border-0 rounded-4 h-100">
|
||
<div class="card-header bg-transparent fw-semibold pt-3">
|
||
<i class="bi bi-activity me-1 text-info"></i> Input States
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row g-3">
|
||
{% for n in range(1, board.num_inputs + 1) %}
|
||
{% set input_key = "input_" ~ n %}
|
||
{% set raw_state = board.input_states.get(input_key, true) %}
|
||
{% set is_active = not raw_state %}
|
||
{% set e = board.get_input_entity(n) %}
|
||
<div class="col-6">
|
||
<div id="input-card-{{ n }}" class="card rounded-3 border-{{ e.active_color if is_active else e.idle_color }}">
|
||
<div class="card-body text-center py-3">
|
||
|
||
<i id="input-icon-{{ n }}" class="bi {{ e.icon }} mb-2 text-{{ e.active_color if is_active else e.idle_color }}"
|
||
style="font-size:2rem"></i>
|
||
<div class="fs-6 fw-semibold mb-1">{{ e.name }}</div>
|
||
|
||
<span id="input-badge-{{ n }}" class="badge text-bg-{{ e.active_color if is_active else e.idle_color }}">
|
||
{{ e.active_label if is_active else e.idle_label }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Board info ────────────────────────────────────────────────────────── -->
|
||
<div class="col-12">
|
||
<div class="card border-0 rounded-4">
|
||
<div class="card-header bg-transparent fw-semibold pt-3">
|
||
<i class="bi bi-info-circle me-1"></i> Board Information
|
||
</div>
|
||
<div class="card-body">
|
||
<dl class="row mb-0">
|
||
<dt class="col-sm-3">Board ID</dt><dd class="col-sm-9 font-monospace">{{ board.id }}</dd>
|
||
<dt class="col-sm-3">Type</dt><dd class="col-sm-9">{{ board.board_type }}</dd>
|
||
<dt class="col-sm-3">Host</dt><dd class="col-sm-9 font-monospace">{{ board.host }}:{{ board.port }}</dd>
|
||
<dt class="col-sm-3">Firmware</dt><dd class="col-sm-9">{{ board.firmware_version or '—' }}</dd>
|
||
<dt class="col-sm-3">Added</dt><dd class="col-sm-9">{{ board.created_at.strftime('%Y-%m-%d %H:%M') }}</dd>
|
||
<dt class="col-sm-3">Last Seen</dt>
|
||
<dd class="col-sm-9">{% if board.last_seen %}{{ board.last_seen.strftime('%Y-%m-%d %H:%M:%S') }}{% else %}Never{% endif %}</dd>
|
||
</dl>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
const socket = io();
|
||
const boardId = {{ board.id }};
|
||
|
||
// ── Entity config embedded from server ──────────────────────────────────────
|
||
const ENTITY_CONFIG = {
|
||
relays: {
|
||
{% for n in range(1, board.num_relays + 1) %}{% set e = board.get_relay_entity(n) %}
|
||
{{ n }}: {icon:"{{ e.icon }}",onColor:"{{ e.on_color }}",offColor:"{{ e.off_color }}",onLabel:"{{ e.on_label }}",offLabel:"{{ e.off_label }}"},
|
||
{% endfor %}
|
||
},
|
||
inputs: {
|
||
{% for n in range(1, board.num_inputs + 1) %}{% set e = board.get_input_entity(n) %}
|
||
{{ n }}: {icon:"{{ e.icon }}",activeColor:"{{ e.active_color }}",idleColor:"{{ e.idle_color }}",activeLabel:"{{ e.active_label }}",idleLabel:"{{ e.idle_label }}"},
|
||
{% endfor %}
|
||
}
|
||
};
|
||
|
||
function swapBorderColor(el, color) {
|
||
el.classList.forEach(c => {
|
||
if (c.startsWith('border-') && c !== 'border-0' && c !== 'border-3') el.classList.remove(c);
|
||
});
|
||
el.classList.add('border-' + color);
|
||
}
|
||
|
||
function applyRelayState(n, isOn) {
|
||
const e = ENTITY_CONFIG.relays[n]; if (!e) return;
|
||
const color = isOn ? e.onColor : e.offColor;
|
||
const label = isOn ? e.onLabel : e.offLabel;
|
||
const card = document.getElementById('relay-card-' + n);
|
||
const icon = document.getElementById('relay-icon-' + n);
|
||
const badge = document.getElementById('relay-badge-' + n);
|
||
const onBtn = document.getElementById('relay-on-btn-' + n);
|
||
const offBtn= document.getElementById('relay-off-btn-' + n);
|
||
if (card) swapBorderColor(card, color);
|
||
if (icon) { icon.className = `bi ${e.icon} mb-2 text-${color}`; icon.style.fontSize = '2rem'; }
|
||
if (badge) { badge.className = 'badge text-bg-' + color; badge.textContent = label; }
|
||
if (onBtn) onBtn.disabled = isOn;
|
||
if (offBtn) offBtn.disabled = !isOn;
|
||
}
|
||
|
||
function applyInputState(n, rawState) {
|
||
const e = ENTITY_CONFIG.inputs[n]; if (!e) return;
|
||
const isActive = !rawState; // NC contact: raw true = resting = idle
|
||
const color = isActive ? e.activeColor : e.idleColor;
|
||
const label = isActive ? e.activeLabel : e.idleLabel;
|
||
const card = document.getElementById('input-card-' + n);
|
||
const icon = document.getElementById('input-icon-' + n);
|
||
const badge = document.getElementById('input-badge-' + n);
|
||
if (card) swapBorderColor(card, color);
|
||
if (icon) { icon.className = `bi ${e.icon} mb-2 text-${color}`; icon.style.fontSize = '2rem'; }
|
||
if (badge) { badge.className = 'badge text-bg-' + color; badge.textContent = label; }
|
||
}
|
||
|
||
socket.on("board_update", function(data) {
|
||
if (data.board_id !== boardId) return;
|
||
if (data.relay_states) {
|
||
for (const [key, isOn] of Object.entries(data.relay_states)) {
|
||
applyRelayState(parseInt(key.split('_')[1]), isOn);
|
||
}
|
||
}
|
||
if (data.input_states) {
|
||
for (const [key, rawState] of Object.entries(data.input_states)) {
|
||
applyInputState(parseInt(key.split('_')[1]), rawState);
|
||
}
|
||
}
|
||
});
|
||
</script>
|
||
{% endblock %}
|