Files

320 lines
15 KiB
HTML
Raw Permalink 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 %}NFC Access Control {{ board.name }}{% 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"><a href="{{ url_for('boards.board_detail', board_id=board.id) }}">{{ board.name }}</a></li>
<li class="breadcrumb-item active">NFC Access Control</li>
</ol>
</nav>
<div class="d-flex align-items-center justify-content-between mb-4">
<div>
<h2 class="fw-bold mb-0"><i class="bi bi-credit-card-2-front me-2 text-primary"></i>NFC Access Control</h2>
<span class="text-secondary small font-monospace">{{ board.name }} — {{ board.host }}:{{ board.port }}</span>
</div>
<button class="btn btn-outline-secondary" id="refresh-btn" onclick="loadStatus()">
<i class="bi bi-arrow-clockwise me-1"></i>Refresh
</button>
</div>
<div class="row g-4">
<!-- ── Live Reader Status ──────────────────────────────────────────────── -->
<div class="col-lg-5">
<div class="card border-0 rounded-4 h-100">
<div class="card-header bg-transparent fw-semibold pt-3">
<i class="bi bi-broadcast me-1 text-info"></i> Reader Status
</div>
<div class="card-body">
<!-- Hardware status -->
<div class="mb-3 d-flex align-items-center gap-3">
<span id="hw-badge" class="badge fs-6
{% if nfc.get('initialized') %}text-bg-success{% else %}text-bg-danger{% endif %}">
{% if nfc.get('initialized') %}
<i class="bi bi-check-circle me-1"></i>PN532 Ready
{% else %}
<i class="bi bi-x-circle me-1"></i>PN532 Not Detected
{% endif %}
</span>
</div>
<!-- Access state indicator -->
<div class="mb-4">
<div class="small text-secondary mb-1">Access state</div>
<div id="access-state-badge" class="badge fs-5
{% set as = nfc.get('access_state','idle') %}
{% if as == 'granted' %}text-bg-success
{% elif as == 'denied' %}text-bg-danger
{% else %}text-bg-secondary{% endif %}">
{% set as = nfc.get('access_state','idle') %}
{% if as == 'granted' %}<i class="bi bi-unlock me-1"></i>ACCESS GRANTED
{% elif as == 'denied' %}<i class="bi bi-lock me-1"></i>ACCESS DENIED
{% else %}<i class="bi bi-hourglass-split me-1"></i>Idle — waiting for card
{% endif %}
</div>
</div>
<!-- Last detected UID -->
<div class="mb-3">
<div class="small text-secondary mb-1">Last detected card UID</div>
<div class="d-flex align-items-center gap-2">
<code id="last-uid" class="fs-5 fw-bold text-primary">
{{ nfc.get('last_uid') or '—' }}
</code>
{% if current_user.is_admin() %}
<button id="use-uid-btn" class="btn btn-sm btn-outline-primary"
onclick="useLastUID()"
{% if not nfc.get('last_uid') %}disabled{% endif %}>
<i class="bi bi-arrow-down-circle me-1"></i>Use as authorized
</button>
{% endif %}
</div>
</div>
<!-- Current authorized UID -->
<div class="mb-3">
<div class="small text-secondary mb-1">Authorized card UID</div>
{% if nfc.get('auth_uid') %}
<code id="auth-uid-display" class="fs-6 fw-bold text-success">{{ nfc.get('auth_uid') }}</code>
{% else %}
<span id="auth-uid-display" class="text-danger small"><i class="bi bi-exclamation-triangle me-1"></i>None — no card enrolled yet</span>
{% endif %}
</div>
<!-- Current relay & timeout -->
<dl class="row mb-0 small">
<dt class="col-5 text-secondary">Module state</dt>
<dd class="col-7" id="module-state-dd">
{% if nfc.get('nfc_enabled') %}
<span class="badge text-bg-success"><i class="bi bi-toggle-on me-1"></i>Enabled</span>
{% else %}
<span class="badge text-bg-secondary"><i class="bi bi-toggle-off me-1"></i>Disabled</span>
{% endif %}
</dd>
<dt class="col-5 text-secondary">Trigger relay</dt>
<dd class="col-7 fw-semibold" id="relay-display">Relay {{ nfc.get('relay_num', 1) }}</dd>
<dt class="col-5 text-secondary">Absence timeout</dt>
<dd class="col-7 fw-semibold" id="pulse-display">{{ nfc.get('pulse_ms', 3000) }} ms</dd>
</dl>
</div>
</div>
</div>
<!-- ── Right column: enroll + config ──────────────────────────────────── -->
<div class="col-lg-7 d-flex flex-column gap-4">
{% if current_user.is_admin() %}
<!-- ── Module Enable / Disable ─────────────────────────────────────── -->
<div class="card border-0 rounded-4">
<div class="card-header bg-transparent fw-semibold pt-3">
<i class="bi bi-toggle-on me-1 text-primary"></i> Mifare / NFC Module
</div>
<div class="card-body d-flex align-items-center justify-content-between gap-3">
<div>
<div class="fw-semibold mb-1">Access control module</div>
<div class="text-secondary small">When disabled the board stops polling the PN532 and will not open any relay on card presentation. The setting persists across power cycles.</div>
</div>
<form method="POST" action="{{ url_for('boards.nfc_enable', board_id=board.id) }}" class="flex-shrink-0">
{% if nfc.get('nfc_enabled') %}
<input type="hidden" name="enabled" value="0">
<button type="submit" id="nfc-toggle-btn"
class="btn btn-success d-flex align-items-center gap-2" style="min-width:140px">
<i class="bi bi-toggle-on fs-5"></i><span>Enabled</span>
</button>
{% else %}
<input type="hidden" name="enabled" value="1">
<button type="submit" id="nfc-toggle-btn"
class="btn btn-secondary d-flex align-items-center gap-2" style="min-width:140px">
<i class="bi bi-toggle-off fs-5"></i><span>Disabled</span>
</button>
{% endif %}
</form>
</div>
</div>
<!-- ── Quick Enroll ──────────────────────────────────────────────────── -->
<div class="card border-0 rounded-4">
<div class="card-header bg-transparent fw-semibold pt-3">
<i class="bi bi-person-badge me-1 text-success"></i> Quick Enroll
</div>
<div class="card-body">
<p class="text-secondary small mb-3">
Present a card to the reader, click <strong>Refresh</strong> until the UID appears, then click <strong>Enroll card</strong>.
The UID will be set as the authorized card with the relay and timeout below.
</p>
<form method="POST" action="{{ url_for('boards.nfc_enroll', board_id=board.id) }}">
<div class="row g-3 align-items-end">
<div class="col-sm-4">
<label class="form-label small text-secondary mb-1">Trigger relay</label>
<select name="relay_num" id="enroll-relay" class="form-select">
{% for r in range(1, 5) %}
<option value="{{ r }}" {% if nfc.get('relay_num', 1) == r %}selected{% endif %}>Relay {{ r }}</option>
{% endfor %}
</select>
</div>
<div class="col-sm-4">
<label class="form-label small text-secondary mb-1">Absence timeout (ms)</label>
<input type="number" name="pulse_ms" id="enroll-pulse"
class="form-control" min="100" max="60000"
value="{{ nfc.get('pulse_ms', 3000) }}">
</div>
<div class="col-sm-4">
<button type="submit" class="btn btn-success w-100"
{% if not nfc.get('last_uid') %}disabled{% endif %}
id="enroll-btn">
<i class="bi bi-person-plus me-1"></i>Enroll card
</button>
</div>
</div>
{% if nfc.get('last_uid') %}
<div class="mt-2 small text-secondary">
Will enroll UID: <code class="text-primary">{{ nfc.get('last_uid') }}</code>
</div>
{% else %}
<div class="mt-2 small text-warning">
<i class="bi bi-exclamation-triangle me-1"></i>No card detected yet — present a card to the reader and refresh.
</div>
{% endif %}
</form>
</div>
</div>
<!-- ── Manual Config ────────────────────────────────────────────────── -->
<div class="card border-0 rounded-4">
<div class="card-header bg-transparent fw-semibold pt-3">
<i class="bi bi-sliders me-1 text-warning"></i> Manual Settings
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('boards.nfc_config_save', board_id=board.id) }}">
<div class="row g-3">
<div class="col-12">
<label class="form-label small text-secondary mb-1">Authorized card UID <span class="text-muted">(leave empty to clear authorization)</span></label>
<input type="text" name="auth_uid" id="manual-uid"
class="form-control font-monospace text-uppercase"
placeholder="e.g. 04:AB:CD:EF"
value="{{ nfc.get('auth_uid', '') }}"
maxlength="31"
oninput="this.value=this.value.toUpperCase()">
</div>
<div class="col-sm-6">
<label class="form-label small text-secondary mb-1">Trigger relay</label>
<select name="relay_num" class="form-select">
{% for r in range(1, 5) %}
<option value="{{ r }}" {% if nfc.get('relay_num', 1) == r %}selected{% endif %}>Relay {{ r }}</option>
{% endfor %}
</select>
</div>
<div class="col-sm-6">
<label class="form-label small text-secondary mb-1">Absence timeout (ms)</label>
<input type="number" name="pulse_ms" class="form-control"
min="100" max="60000" value="{{ nfc.get('pulse_ms', 3000) }}">
<div class="form-text">Relay closes this many ms after the card is removed (100 60 000).</div>
</div>
<div class="col-12 d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-floppy me-1"></i>Save config
</button>
{% if nfc.get('auth_uid') %}
<button type="submit" class="btn btn-outline-danger"
onclick="document.getElementById('manual-uid').value=''">
<i class="bi bi-x-circle me-1"></i>Clear authorization
</button>
{% endif %}
</div>
</div>
</form>
</div>
</div>
{% endif %}{# is_admin #}
</div>{# right col #}
</div>{# row #}
{% endblock %}
{% block scripts %}
<script>
const STATUS_URL = "{{ url_for('boards.nfc_status_json', board_id=board.id) }}";
function loadStatus() {
const btn = document.getElementById('refresh-btn');
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Loading…'; }
fetch(STATUS_URL)
.then(r => r.json())
.then(d => {
// hardware badge
const hwBadge = document.getElementById('hw-badge');
if (hwBadge) {
hwBadge.className = 'badge fs-6 ' + (d.initialized ? 'text-bg-success' : 'text-bg-danger');
hwBadge.innerHTML = d.initialized
? '<i class="bi bi-check-circle me-1"></i>PN532 Ready'
: '<i class="bi bi-x-circle me-1"></i>PN532 Not Detected';
}
// access state
const asBadge = document.getElementById('access-state-badge');
if (asBadge) {
const as = d.access_state || 'idle';
const cls = as === 'granted' ? 'text-bg-success' : as === 'denied' ? 'text-bg-danger' : 'text-bg-secondary';
const icon = as === 'granted' ? 'bi-unlock' : as === 'denied' ? 'bi-lock' : 'bi-hourglass-split';
const txt = as === 'granted' ? 'ACCESS GRANTED' : as === 'denied' ? 'ACCESS DENIED' : 'Idle — waiting for card';
asBadge.className = 'badge fs-5 ' + cls;
asBadge.innerHTML = `<i class="bi ${icon} me-1"></i>${txt}`;
}
// last UID
const uidEl = document.getElementById('last-uid');
if (uidEl) uidEl.textContent = d.last_uid || '—';
const useBtn = document.getElementById('use-uid-btn');
if (useBtn) useBtn.disabled = !d.last_uid;
const enrollBtn = document.getElementById('enroll-btn');
if (enrollBtn) enrollBtn.disabled = !d.last_uid;
// authorized UID display
const authEl = document.getElementById('auth-uid-display');
if (authEl) {
if (d.auth_uid) {
authEl.className = 'fs-6 fw-bold text-success';
authEl.textContent = d.auth_uid;
} else {
authEl.className = 'text-danger small';
authEl.innerHTML = '<i class="bi bi-exclamation-triangle me-1"></i>None — no card enrolled yet';
}
}
// relay & timeout summary
const relayDisp = document.getElementById('relay-display');
if (relayDisp) relayDisp.textContent = 'Relay ' + (d.relay_num || 1);
const pulseDisp = document.getElementById('pulse-display');
if (pulseDisp) pulseDisp.textContent = (d.pulse_ms || 3000) + ' ms';
// module enabled/disabled summary in left card
const modDd = document.getElementById('module-state-dd');
if (modDd) {
modDd.innerHTML = d.nfc_enabled
? '<span class="badge text-bg-success"><i class="bi bi-toggle-on me-1"></i>Enabled</span>'
: '<span class="badge text-bg-secondary"><i class="bi bi-toggle-off me-1"></i>Disabled</span>';
}
})
.catch(() => {})
.finally(() => {
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="bi bi-arrow-clockwise me-1"></i>Refresh'; }
});
}
function useLastUID() {
const uid = document.getElementById('last-uid').textContent.trim();
if (uid && uid !== '—') {
const manualUid = document.getElementById('manual-uid');
if (manualUid) {
manualUid.value = uid;
manualUid.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}
// Auto-refresh every 3 s while the page is open
setInterval(loadStatus, 3000);
</script>
{% endblock %}