Initial commit: Location Management Flask app

This commit is contained in:
ske087
2026-02-26 19:24:17 +02:00
commit 7a22575dab
52 changed files with 3481 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
{% 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 %}