Files
location_managemet/app/templates/boards/add.html
scheianu fbf5802c69 Fix board registration blocked by HMAC auth
- /register endpoint no longer requires verifyAPIRequest() - it is
  the bootstrap handshake and api_secret cannot be set before it runs
- Add api_secret field to the Add Board form (hardware boards only)
  with a cryptographic Generate button, same as the Edit form
- Save api_secret from the add-board POST so the driver can sign
  requests immediately after registration
2026-03-15 16:12:18 +02:00

363 lines
14 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 %}Add Board Location Management{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('boards.list_boards') }}">Boards</a></li>
<li class="breadcrumb-item active">Add Board</li>
</ol>
</nav>
{# ── Step indicator ──────────────────────────────────────────────────────── #}
<div class="d-flex align-items-center mb-4" id="step-indicator" style="max-width:420px">
<div class="step-bubble active" id="bubble-1">1</div>
<span class="small fw-semibold ms-2 me-3" id="lbl-1">Choose Type</span>
<div class="step-line" id="step-line"></div>
<div class="step-bubble ms-3 me-2" id="bubble-2">2</div>
<span class="small text-muted" id="lbl-2">Configure</span>
</div>
{# ══════════════════════════════════════════════════════════
STEP 1 pick a board / integration type
══════════════════════════════════════════════════════════ #}
<div id="step1">
<h5 class="fw-bold mb-1">What are you adding?</h5>
<p class="text-muted small mb-4">Choose the hardware board or cloud service you want to connect.</p>
<p class="text-uppercase text-muted fw-semibold small mb-2" style="letter-spacing:.07em">
<i class="bi bi-cloud me-1"></i>Cloud Gateways
</p>
<div class="row g-3 mb-4">
<div class="col-12 col-sm-6">
<button type="button" class="type-card w-100 text-start" data-type="sonoff_ewelink">
<div class="d-flex align-items-start gap-3">
<div class="type-icon" style="background:rgba(255,210,0,.12);color:#ffd200">
<i class="bi bi-cloud-lightning-fill"></i>
</div>
<div>
<div class="fw-semibold">Sonoff eWeLink</div>
<div class="small text-muted mt-1">Control Sonoff devices via the eWeLink cloud.</div>
</div>
</div>
</button>
</div>
<div class="col-12 col-sm-6">
<button type="button" class="type-card w-100 text-start" data-type="tuya_cloud">
<div class="d-flex align-items-start gap-3">
<div class="type-icon" style="background:rgba(32,201,151,.12);color:#20c997">
<i class="bi bi-cloud-fill"></i>
</div>
<div>
<div class="fw-semibold">Tuya Cloud</div>
<div class="small text-muted mt-1">Control Tuya / Smart Life devices via QR login.</div>
</div>
</div>
</button>
</div>
</div>
<p class="text-uppercase text-muted fw-semibold small mb-2" style="letter-spacing:.07em">
<i class="bi bi-cpu me-1"></i>Hardware Boards
</p>
<div class="row g-3">
{% for d in drivers %}
{% if d.DRIVER_ID not in ("sonoff_ewelink", "tuya_cloud") %}
<div class="col-12 col-sm-6 col-lg-4">
<button type="button" class="type-card w-100 text-start" data-type="{{ d.DRIVER_ID }}">
<div class="d-flex align-items-start gap-3">
<div class="type-icon" style="background:rgba(88,166,255,.12);color:#58a6ff">
<i class="bi bi-cpu-fill"></i>
</div>
<div>
<div class="fw-semibold">{{ d.DRIVER_ID | replace('_',' ') | title }}</div>
<div class="small text-muted mt-1">{{ d.DESCRIPTION }}</div>
</div>
</div>
</button>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{# ══════════════════════════════════════════════════════════
STEP 2 configuration form
══════════════════════════════════════════════════════════ #}
<div id="step2" class="d-none" style="max-width:560px">
<button type="button" class="btn btn-link btn-sm text-muted ps-0 mb-3" id="btn-back">
<i class="bi bi-arrow-left me-1"></i>Back
</button>
{# Selected type badge #}
<div class="d-flex align-items-center gap-3 mb-4">
<div class="type-icon-sm" id="s2-icon-wrap"></div>
<div>
<div class="fw-bold fs-6" id="s2-type-label"></div>
<div class="small text-muted" id="s2-type-desc"></div>
</div>
</div>
<form method="POST" id="add-form">
<input type="hidden" name="board_type" id="f-board-type">
{# Name always shown #}
<div class="mb-4">
<label class="form-label fw-semibold">Board Name</label>
<input type="text" name="name" id="f-name" class="form-control"
placeholder="e.g. Living Room Gateway" required autofocus>
<div class="form-text text-muted">A friendly name to identify this board.</div>
</div>
{# ── Gateway info box (Sonoff / Tuya) ── #}
<div id="f-gw-section" class="d-none mb-4">
<div class="alert py-3 d-flex gap-3 align-items-start" id="f-gw-alert">
<span id="f-gw-icon" class="fs-4 mt-1"></span>
<div id="f-gw-text"></div>
</div>
</div>
{# ── Hardware-only fields ── #}
<div id="f-hw-section" class="d-none">
<div class="row g-3 mb-3">
<div class="col-8">
<label class="form-label fw-semibold">IP Address / Hostname</label>
<input type="text" name="host" id="f-host" class="form-control font-monospace"
placeholder="192.168.1.100">
</div>
<div class="col-4">
<label class="form-label fw-semibold">Port</label>
<input type="number" name="port" class="form-control" value="80" min="1" max="65535">
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-6">
<label class="form-label fw-semibold">Relay Outputs</label>
<input type="number" name="num_relays" id="f-relays" class="form-control"
value="4" min="0" max="32">
</div>
<div class="col-6">
<label class="form-label fw-semibold">Digital Inputs</label>
<input type="number" name="num_inputs" id="f-inputs" class="form-control"
value="4" min="0" max="32">
</div>
</div>
</div>
<div id="f-secret-section" class="d-none mb-4">
<label class="form-label fw-semibold">API Secret <span class="text-secondary fw-normal small">(HMAC-SHA256 shared secret)</span></label>
<div class="input-group">
<input type="text" name="api_secret" id="f-api-secret" class="form-control font-monospace"
placeholder="Leave empty to disable API authentication">
<button class="btn btn-outline-secondary" type="button" onclick="genSecret()">
<i class="bi bi-shuffle me-1"></i>Generate
</button>
</div>
<div class="form-text">Must match <code>API_SECRET</code> in the board's <code>secrets.h</code>. You can also set this later in the board's Edit page.</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary px-4">
<i class="bi bi-check-lg me-1"></i>Add Board
</button>
<a href="{{ url_for('boards.list_boards') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
<style>
.type-card {
background: #161b22;
border: 1px solid #30363d;
border-radius: .75rem;
padding: 1rem 1.1rem;
cursor: pointer;
transition: border-color .15s, background .15s, box-shadow .15s;
color: inherit;
}
.type-card:hover, .type-card:focus {
border-color: #58a6ff;
background: rgba(88,166,255,.06);
box-shadow: 0 0 0 3px rgba(88,166,255,.12);
outline: none;
}
.type-icon {
width: 44px; height: 44px; border-radius: .55rem;
display: flex; align-items: center; justify-content: center;
font-size: 1.4rem; flex-shrink: 0;
}
.type-icon-sm {
width: 38px; height: 38px; border-radius: .45rem;
display: flex; align-items: center; justify-content: center;
font-size: 1.25rem; flex-shrink: 0;
}
.step-bubble {
width: 28px; height: 28px; border-radius: 50%;
background: #30363d; color: #8b949e;
display: flex; align-items: center; justify-content: center;
font-size: .8rem; font-weight: 700; flex-shrink: 0;
transition: background .2s, color .2s;
}
.step-bubble.active { background: #1f6feb; color: #fff; }
.step-bubble.done { background: #238636; color: #fff; }
.step-line {
flex: 1; height: 2px; background: #30363d;
min-width: 32px; max-width: 60px;
transition: background .2s;
}
.step-line.done { background: #238636; }
</style>
{% endblock %}
{% block scripts %}
<script>
(function () {
const DRIVERS = {
{% for d in drivers %}
"{{ d.DRIVER_ID }}": {
relays: {{ d.DEFAULT_NUM_RELAYS }},
inputs: {{ d.DEFAULT_NUM_INPUTS }},
desc: "{{ d.DESCRIPTION | replace('"', '\\"') }}",
},
{% endfor %}
};
const GW_META = {
sonoff_ewelink: {
label: "Sonoff eWeLink",
iconHtml: '<i class="bi bi-cloud-lightning-fill"></i>',
iconBg: "rgba(255,210,0,.15)",
iconColor: "#ffd200",
alertCls: "alert-warning",
alertIcon: '<i class="bi bi-cloud-lightning-fill text-warning"></i>',
alertBody: '<strong>Sonoff eWeLink Gateway</strong><br>'
+ '<span class="small">After adding, you\'ll enter your eWeLink '
+ 'account credentials to discover and control all your Sonoff devices '
+ 'automatically.</span>',
},
tuya_cloud: {
label: "Tuya Cloud",
iconHtml: '<i class="bi bi-cloud-fill"></i>',
iconBg: "rgba(32,201,151,.15)",
iconColor: "#20c997",
alertCls: "alert-info",
alertIcon: '<i class="bi bi-qr-code text-info"></i>',
alertBody: '<strong>Tuya / Smart Life Cloud Gateway</strong><br>'
+ '<span class="small">After adding, you\'ll scan a QR code with '
+ 'the <strong>Smart Life</strong> or <strong>Tuya Smart</strong> app '
+ 'to link your account and discover all your Tuya devices automatically.</span>',
},
};
const GATEWAY_TYPES = Object.keys(GW_META);
// DOM
const step1 = document.getElementById("step1");
const step2 = document.getElementById("step2");
const btnBack = document.getElementById("btn-back");
const bubble1 = document.getElementById("bubble-1");
const bubble2 = document.getElementById("bubble-2");
const stepLine = document.getElementById("step-line");
const lbl1 = document.getElementById("lbl-1");
const lbl2 = document.getElementById("lbl-2");
const fType = document.getElementById("f-board-type");
const fName = document.getElementById("f-name");
const fHost = document.getElementById("f-host");
const fRelays = document.getElementById("f-relays");
const fInputs = document.getElementById("f-inputs");
const hwSec = document.getElementById("f-hw-section");
const gwSec = document.getElementById("f-gw-section");
const gwAlert = document.getElementById("f-gw-alert");
const gwIcon = document.getElementById("f-gw-icon");
const gwText = document.getElementById("f-gw-text");
const s2Icon = document.getElementById("s2-icon-wrap");
const s2Label = document.getElementById("s2-type-label");
const s2Desc = document.getElementById("s2-type-desc");
// Card clicks
document.querySelectorAll(".type-card").forEach(function (card) {
card.addEventListener("click", function () { goStep2(card.dataset.type); });
});
// Back
btnBack.addEventListener("click", function () {
step2.classList.add("d-none");
step1.classList.remove("d-none");
bubble1.classList.replace("done", "active");
bubble2.classList.remove("active");
stepLine.classList.remove("done");
lbl1.classList.remove("text-success");
lbl2.classList.add("text-muted");
lbl2.classList.remove("fw-semibold");
});
function goStep2(type) {
const drv = DRIVERS[type] || { relays: 4, inputs: 4, desc: "" };
const meta = GW_META[type] || null;
const isGw = GATEWAY_TYPES.includes(type);
fType.value = type;
fRelays.value = drv.relays;
fInputs.value = drv.inputs;
fName.value = "";
// Header icon + label
if (meta) {
s2Icon.innerHTML = meta.iconHtml;
s2Icon.style.background = meta.iconBg;
s2Icon.style.color = meta.iconColor;
s2Label.textContent = meta.label;
} else {
s2Icon.innerHTML = '<i class="bi bi-cpu-fill"></i>';
s2Icon.style.background = "rgba(88,166,255,.15)";
s2Icon.style.color = "#58a6ff";
s2Label.textContent = type.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
}
s2Desc.textContent = drv.desc;
// Section visibility
if (isGw) {
hwSec.classList.add("d-none");
fHost.required = false;
gwSec.classList.remove("d-none");
gwAlert.className = "alert py-3 d-flex gap-3 align-items-start " + meta.alertCls;
gwIcon.innerHTML = meta.alertIcon;
gwText.innerHTML = meta.alertBody;
document.getElementById("f-secret-section").classList.add("d-none");
} else {
hwSec.classList.remove("d-none");
fHost.required = true;
gwSec.classList.add("d-none");
document.getElementById("f-secret-section").classList.remove("d-none");
}
// Step indicator
step1.classList.add("d-none");
step2.classList.remove("d-none");
bubble1.classList.remove("active");
bubble1.classList.add("done");
bubble2.classList.add("active");
stepLine.classList.add("done");
lbl1.classList.add("text-success");
lbl2.classList.remove("text-muted");
lbl2.classList.add("fw-semibold");
fName.focus();
}
window.genSecret = function () {
const buf = new Uint8Array(32);
crypto.getRandomValues(buf);
document.getElementById('f-api-secret').value =
Array.from(buf).map(b => b.toString(16).padStart(2, '0')).join('');
};
}());
</script>
{% endblock %}