Files
location_managemet/app/templates/dashboard/index.html
2026-02-26 19:24:17 +02:00

211 lines
8.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Dashboard Location Management{% endblock %}
{% block content %}
<div class="d-flex align-items-center justify-content-between mb-4">
<h2 class="fw-bold mb-0"><i class="bi bi-grid-1x2-fill me-2 text-primary"></i>Dashboard</h2>
{% if current_user.is_admin() %}
<a href="{{ url_for('boards.add_board') }}" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i> Add Board
</a>
{% endif %}
</div>
<!-- ── Summary cards ─────────────────────────────────────────────────────── -->
<div class="row g-3 mb-4">
<div class="col-sm-4">
<div class="card stat-card border-0 rounded-4 bg-primary bg-opacity-10">
<div class="card-body d-flex align-items-center gap-3">
<i class="bi bi-motherboard display-5 text-primary"></i>
<div>
<div class="display-6 fw-bold">{{ boards | length }}</div>
<div class="text-secondary small">Total Boards</div>
</div>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="card stat-card border-0 rounded-4 bg-success bg-opacity-10">
<div class="card-body d-flex align-items-center gap-3">
<i class="bi bi-wifi display-5 text-success"></i>
<div>
<div class="display-6 fw-bold">{{ boards | selectattr('is_online') | list | length }}</div>
<div class="text-secondary small">Online</div>
</div>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="card stat-card border-0 rounded-4 bg-warning bg-opacity-10">
<div class="card-body d-flex align-items-center gap-3">
<i class="bi bi-diagram-3 display-5 text-warning"></i>
<div>
<div class="display-6 fw-bold">{{ active_workflows }}</div>
<div class="text-secondary small">Active Workflows</div>
</div>
</div>
</div>
</div>
</div>
<!-- ── Board grid ─────────────────────────────────────────────────────────── -->
{% if boards %}
<div class="row g-3" id="board-grid">
{% for board in boards %}
<div class="col-md-6 col-xl-4" id="board-card-{{ board.id }}">
<div class="card board-card border-0 rounded-4 h-100 {% if board.is_online %}border-start border-3 border-success{% else %}border-start border-3 border-secondary{% endif %}">
<div class="card-header bg-transparent d-flex justify-content-between align-items-center pt-3">
<div>
<h5 class="mb-0 fw-semibold">{{ board.name }}</h5>
<span class="badge text-bg-secondary small">{{ board.board_type }}</span>
<span class="badge {% if board.is_online %}text-bg-success{% else %}text-bg-secondary{% endif %} small ms-1" id="online-badge-{{ board.id }}">
{% if board.is_online %}Online{% else %}Offline{% endif %}
</span>
</div>
<a href="{{ url_for('boards.board_detail', board_id=board.id) }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-box-arrow-up-right"></i>
</a>
</div>
<div class="card-body">
<p class="text-secondary small mb-2"><i class="bi bi-hdd-network me-1"></i>{{ board.host }}:{{ board.port }}</p>
<!-- Quick relay controls -->
<div class="d-flex flex-wrap gap-2">
{% 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) %}
<button type="button"
class="btn btn-sm relay-btn {% if is_on %}btn-{{ e.on_color }}{% else %}btn-outline-secondary{% endif %}"
data-relay="{{ n }}" data-board="{{ board.id }}"
data-toggle-url="{{ url_for('boards.toggle_relay_view', board_id=board.id, relay_num=n) }}"
title="{{ e.name }}"
onclick="dashToggleRelay(this)">
<i class="bi {{ e.icon }}"></i>
{{ e.name }}
</button>
{% endfor %}
</div>
<!-- Input states -->
{% if board.num_inputs > 0 %}
<div class="mt-2 d-flex flex-wrap gap-1">
{% 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) %}
<span class="badge {% if is_active %}text-bg-{{ e.active_color }}{% else %}text-bg-dark{% endif %} input-badge"
data-input="{{ n }}" data-board="{{ board.id }}">
<i class="bi {{ e.icon }}"></i> {{ e.name }}
</span>
{% endfor %}
</div>
{% endif %}
</div>
<div class="card-footer bg-transparent text-secondary small">
{% if board.last_seen %}
Last seen {{ board.last_seen.strftime('%H:%M:%S') }}
{% else %}
Never polled
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5 text-secondary">
<i class="bi bi-motherboard display-2"></i>
<p class="mt-3 fs-5">No boards added yet.</p>
{% if current_user.is_admin() %}
<a href="{{ url_for('boards.add_board') }}" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i> Add your first board
</a>
{% endif %}
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
// ── Entity config for all boards (embedded from server) ─────────────────────
const BOARD_ENTITIES = {
{% for board in boards %}
{{ board.id }}: {
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 }}"},
{% 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 }}"},
{% endfor %}
}
},
{% endfor %}
};
const socket = io();
socket.on("board_update", function(data) {
const bid = data.board_id;
const ent = BOARD_ENTITIES[bid] || {relays:{}, inputs:{}};
// online badge
const onlineBadge = document.getElementById("online-badge-" + bid);
if (onlineBadge) {
onlineBadge.textContent = data.is_online ? "Online" : "Offline";
onlineBadge.className = "badge small ms-1 " + (data.is_online ? "text-bg-success" : "text-bg-secondary");
}
// relay buttons
if (data.relay_states) {
for (const [key, isOn] of Object.entries(data.relay_states)) {
const n = parseInt(key.split("_")[1]);
const e = ent.relays[n] || {};
document.querySelectorAll(`[data-relay="${n}"][data-board="${bid}"]`).forEach(btn => {
btn.className = `btn btn-sm relay-btn ${isOn ? 'btn-' + (e.onColor||'success') : 'btn-outline-secondary'}`;
const icon = btn.querySelector("i");
if (icon && e.icon) icon.className = `bi ${e.icon}`;
});
}
}
// input badges — NC inversion: raw true = resting = idle
if (data.input_states) {
for (const [key, rawState] of Object.entries(data.input_states)) {
const n = parseInt(key.split("_")[1]);
const e = ent.inputs[n] || {};
const isActive = !rawState;
document.querySelectorAll(`[data-input="${n}"][data-board="${bid}"]`).forEach(span => {
span.className = `badge input-badge text-bg-${isActive ? (e.activeColor||'info') : (e.idleColor||'dark')}`;
});
}
}
});
// ── Dashboard relay toggle (AJAX — no page navigation) ───────────────────────
function dashToggleRelay(btn) {
const url = btn.getAttribute("data-toggle-url");
btn.disabled = true;
fetch(url, {
method: "POST",
headers: { "Accept": "application/json", "X-Requested-With": "XMLHttpRequest" },
})
.then(r => r.json())
.then(data => {
// SocketIO will update the button colour; just re-enable it
btn.disabled = false;
if (!data.hw_ok) {
// Brief visual indicator that hardware was unreachable
btn.title = "(board unreachable — local state updated)";
setTimeout(() => btn.title = btn.getAttribute("data-label") || "", 3000);
}
})
.catch(() => { btn.disabled = false; });
}
</script>
{% endblock %}