Files
wifi-gps-mapper-public/gui/templates/index.html
T
2026-10-06 21:08:12 +03:00

2889 lines
83 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
>
<title>WiFi GPS Mapper</title>
<link
rel="stylesheet"
href="{{ url_for('static', filename='style.css') }}"
>
</head>
<body>
<header class="app-header">
<div class="header-inner">
<div>
<h1>
WiFi GPS Mapper
</h1>
<div class="subtitle">
Wireless network scanner
</div>
</div>
<div class="header-actions">
<a
class="header-link"
href="{{ url_for('reports_dashboard') }}"
>
REPORTS
</a>
<div class="header-scanner-controls">
<button
type="submit"
form="interface-form"
class="header-button start"
{% if status.running or gps_test.running %}disabled{% endif %}
>
START
</button>
<form
method="post"
action="{{ url_for('scanner_stop') }}"
>
<button
type="submit"
class="header-button stop"
{% if not status.running %}disabled{% endif %}
>
STOP
</button>
</form>
</div>
<div
id="header-scanner-status"
class="header-status {{ status.health|lower }}"
>
<span
class="status-indicator {{ status.health|lower }}"
></span>
<span class="status-text">
{% if status.health == "ACTIVE" %}
SCANNER ACTIVE
{% elif status.health == "WARNING" %}
WARNING
{% elif status.health == "STOPPED" %}
SCANNER STOPPED
{% else %}
SCANNER READY
{% endif %}
</span>
</div>
</div>
</header>
<main>
<section class="panel scanner-panel">
<div class="panel-heading">
<div>
<div class="panel-kicker">
CONTROL
</div>
<h2>
Scanner
</h2>
</div>
<div class="scanner-state">
{% if status.running %}
<strong class="running">
RUNNING
</strong>
<span>
PID {{ status.pid }}
</span>
{% else %}
<strong class="stopped">
STOPPED
</strong>
{% endif %}
</div>
</div>
<div class="scanner-controls">
<form
id="interface-form"
class="interface-form"
method="post"
action="{{ url_for('scanner_start') }}"
>
<div class="field">
<label for="interface">
WiFi interface
</label>
<select
id="interface"
name="interface"
required
{% if status.running or gps_test.running %}disabled{% endif %}
>
<option value="">
Select interface
</option>
{% for interface in interfaces %}
<option value="{{ interface }}">
{{ interface }}
</option>
{% endfor %}
</select>
</div>
{% if interface_error %}
<div class="error">
{{ interface_error }}
</div>
{% endif %}
<button
type="submit"
class="button start"
{% if status.running or gps_test.running %}disabled{% endif %}
>
START SCAN
</button>
</form>
<form
method="post"
action="{{ url_for('scanner_stop') }}"
>
<button
type="submit"
class="button stop"
{% if not status.running %}disabled{% endif %}
>
STOP SCAN
</button>
</form>
</div>
</section>
<section class="panel configuration-panel">
<div class="panel-heading">
<div>
<div class="panel-kicker">
SYSTEM
</div>
<h2>
Configuration
</h2>
</div>
<span class="panel-hint">
config.yaml
</span>
</div>
{% if config._error %}
<div class="error">
{{ config._error }}
</div>
{% else %}
<form
id="configuration-form"
class="configuration-form"
>
<div class="config-grid">
<div class="config-column config-column-wifi">
<div class="field field-wide wifi-configuration">
<div class="wifi-title">
WiFi
</div>
<div class="wifi-interface-status">
<span>
Interface:
</span>
<strong id="wifi-diagnostics-interface">
not selected
</strong>
<span
id="wifi-diagnostics-status"
>
Select interface to detect channels
</span>
</div>
<div class="wifi-band">
<div class="wifi-band-header">
<label class="wifi-band-toggle">
<input
id="wifi-enable-2-4ghz"
type="checkbox"
{% if config.wifi.enable_2_4ghz %}
checked
{% endif %}
>
<span>
2.4 GHz
</span>
</label>
<div class="wifi-band-actions">
<button
type="button"
class="channel-action"
data-band="2.4"
>
Select all
</button>
<button
type="button"
class="channel-action"
data-band="2.4"
data-action="clear"
>
Clear all
</button>
</div>
</div>
<div
id="wifi-channels-2-4"
class="wifi-channel-grid"
></div>
</div>
<div class="wifi-band">
<div class="wifi-band-header">
<label class="wifi-band-toggle">
<input
id="wifi-enable-5ghz"
type="checkbox"
{% if config.wifi.enable_5ghz %}
checked
{% endif %}
>
<span>
5 GHz
</span>
</label>
<div class="wifi-band-actions">
<button
type="button"
class="channel-action"
data-band="5"
>
Select all
</button>
<button
type="button"
class="channel-action"
data-band="5"
data-action="clear"
>
Clear all
</button>
</div>
</div>
<div
id="wifi-channels-5"
class="wifi-channel-grid"
></div>
</div>
<div class="wifi-band">
<div class="wifi-band-header">
<label class="wifi-band-toggle">
<input
id="wifi-enable-6ghz"
type="checkbox"
{% if config.wifi.enable_6ghz %}
checked
{% endif %}
>
<span>
6 GHz
</span>
</label>
<div class="wifi-band-actions">
<button
type="button"
class="channel-action"
data-band="6"
>
Select all
</button>
<button
type="button"
class="channel-action"
data-band="6"
data-action="clear"
>
Clear all
</button>
</div>
</div>
<div
id="wifi-channels-6"
class="wifi-channel-grid"
></div>
</div>
<div
id="wifi-channel-error"
class="error"
hidden
></div>
<input
id="wifi-channels"
type="hidden"
value=""
>
</div>
</div>
<div class="config-column config-column-main">
<div class="field">
<label for="gps-source">
GPS source
</label>
<select id="gps-source">
<option
value="gpsrelay"
{% if config.gps.source == "gpsrelay" %}
selected
{% endif %}
>
GPSRelay
</option>
<option
value="gnss"
{% if config.gps.source == "gnss" %}
selected
{% endif %}
>
GNSS
</option>
</select>
</div>
<div
id="gpsrelay-settings"
{% if config.gps.source != "gpsrelay" %}
hidden
{% endif %}
>
<div class="field">
<label for="gps-host">
GPS host
</label>
<input
id="gps-host"
type="text"
value="{{ config.gps.gpsrelay.host }}"
inputmode="decimal"
placeholder="xxx.xxx.xxx.xxx"
maxlength="15"
pattern="^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$"
title="Введите IPv4-адрес в формате xxx.xxx.xxx.xxx, например 192.168.1.10"
required
>
</div>
<div class="field">
<label for="gps-port">
GPS port
</label>
<input
id="gps-port"
type="number"
value="{{ config.gps.gpsrelay.port }}"
min="1"
max="65535"
required
>
</div>
</div>
<div
id="gnss-settings"
{% if config.gps.source != "gnss" %}
hidden
{% endif %}
>
<div class="field">
<label for="gnss-handler">
GNSS handler
</label>
<select id="gnss-handler">
<option
value="direct"
{% if config.gps.gnss.handler == "direct" %}
selected
{% endif %}
>
Direct serial
</option>
<option
value="gpsd"
{% if config.gps.gnss.handler == "gpsd" %}
selected
{% endif %}
>
gpsd
</option>
<option
value="modem"
{% if config.gps.gnss.handler == "modem" %}
selected
{% endif %}
>
Modem
</option>
</select>
</div>
<div
class="field"
id="gnss-direct-device-field"
{% if config.gps.gnss.handler != "direct" %}
hidden
{% endif %}
>
<label for="gnss-direct-device">
GNSS device
</label>
<input
id="gnss-direct-device"
type="text"
value="{{ config.gps.gnss.direct.device }}"
{% if config.gps.gnss.handler == "direct" %}
required
{% endif %}
>
</div>
<div
class="field"
id="gnss-modem-device-field"
{% if config.gps.gnss.handler != "modem" %}
hidden
{% endif %}
>
<label for="gnss-modem-device">
GNSS modem device
</label>
<input
id="gnss-modem-device"
type="text"
value="{{ config.gps.gnss.modem.device }}"
{% if config.gps.gnss.handler == "modem" %}
required
{% endif %}
>
</div>
<div
class="field"
id="gnss-at-device-field"
{% if config.gps.gnss.handler != "modem" %}
hidden
{% endif %}
>
<label for="gnss-at-device">
GNSS AT device
</label>
<input
id="gnss-at-device"
type="text"
value="{{ config.gps.gnss.modem.at_device }}"
{% if config.gps.gnss.handler == "modem" %}
required
{% endif %}
>
</div>
</div>
<div class="field config-info">
<label>
Database
</label>
<div class="config-info-value">
{{ config.database.file }}
</div>
</div>
<div class="field config-info">
<label>
Vendor database
</label>
<div class="config-info-value">
{{ config.vendor.database }}
</div>
</div>
<div class="field config-info">
<label>
Captures directory
</label>
<div class="config-info-value">
{{ config.captures.directory }}
</div>
</div>
</div>
<div class="config-column config-column-extra">
<div class="field">
<label for="wifi-extra-args">
Extra args
</label>
<input
id="wifi-extra-args"
type="text"
value="{{ config.wifi.extra_args }}"
>
<div class="config-help">
Дополнительные параметры запуска hcxdumptool.
Если параметры не нужны, оставьте поле пустым.
Полную справку по параметрам используемой версии смотрите командой:
./bin/hcxdumptool --help
</div>
</div>
<div class="field">
<label for="timezone">
Timezone
</label>
<input
id="timezone"
type="text"
value="{{ config.timezone }}"
placeholder="Europe/Moscow"
required
>
<div class="config-help">
IANA часовой пояс для отображения времени в UI и отчетах.
В базе данных и обменных файлах время всегда хранится в UTC.
</div>
</div>
<div class="field">
<label for="logging-level">
Logging level
</label>
<select id="logging-level">
<option
value="DEBUG"
{% if config.logging.level == "DEBUG" %}
selected
{% endif %}
>
DEBUG
</option>
<option
value="INFO"
{% if config.logging.level == "INFO" %}
selected
{% endif %}
>
INFO
</option>
<option
value="WARNING"
{% if config.logging.level == "WARNING" %}
selected
{% endif %}
>
WARNING
</option>
<option
value="ERROR"
{% if config.logging.level == "ERROR" %}
selected
{% endif %}
>
ERROR
</option>
</select>
</div>
<div class="field">
<label for="status-interval">
Status interval
</label>
<input
id="status-interval"
type="number"
value="{{ config.status.interval }}"
min="1"
required
>
<div class="config-help">
Интервал вывода контрольной информации о работе программы, секунд.
Каждые N секунд отображаются длительность текущей сессии,
количество полученных GPS сообщений, последние координаты
и текущая статистика захвата.
Сохранение GPS-потока в gps_track.nmea от этого параметра не зависит.
</div>
</div>
</div>
</div>
<div class="configuration-actions">
<button
type="submit"
class="button start"
>
SAVE CONFIGURATION
</button>
<div
id="configuration-save-status"
hidden
></div>
</div>
</form>
{% endif %}
</section>
<section class="panel gps-diagnostics-panel">
<div class="panel-heading">
<div>
<div class="panel-kicker">
GPS
</div>
<h2>
GPS Diagnostics
</h2>
</div>
<span
id="gps-test-status"
class="panel-hint"
>
GPS READY
</span>
</div>
<div class="gps-diagnostics">
<div class="gps-diagnostics-state">
<span
id="gps-test-indicator"
class="gps-state-indicator ready"
>
●
</span>
<div>
<strong
id="gps-test-state"
>
Готов к тесту
</strong>
<span
id="gps-test-message"
>
GPS test is ready.
</span>
</div>
</div>
<div class="gps-diagnostics-grid">
<div class="gps-diagnostic">
<span>
Source
</span>
<strong id="gps-test-source">
{{ config.gps.source|upper }}
</strong>
</div>
<div class="gps-diagnostic">
<span>
Handler
</span>
<strong id="gps-test-handler">
{% if config.gps.source == "gnss" %}
{{ config.gps.gnss.handler }}
{% else %}
GPSRelay
{% endif %}
</strong>
</div>
<div class="gps-diagnostic">
<span>
NMEA
</span>
<strong id="gps-test-nmea">
{{ gps_test.nmea_count }}
</strong>
</div>
<div class="gps-diagnostic">
<span>
FIX
</span>
<strong id="gps-test-fix">
{{ gps_test.fix_count }}
</strong>
</div>
<div class="gps-diagnostic">
<span>
Latitude
</span>
<strong id="gps-test-latitude">
{% if gps_test.latitude is not none %}
{{ "%.6f"|format(gps_test.latitude) }}
{% else %}
—
{% endif %}
</strong>
</div>
<div class="gps-diagnostic">
<span>
Longitude
</span>
<strong id="gps-test-longitude">
{% if gps_test.longitude is not none %}
{{ "%.6f"|format(gps_test.longitude) }}
{% else %}
—
{% endif %}
</strong>
</div>
</div>
<div class="gps-diagnostics-actions">
<button
id="gps-test-button"
type="button"
class="button start"
{% if status.running %}disabled{% endif %}
>
TEST GPS
</button>
</div>
</div>
</section>
<section class="panel">
<div class="panel-heading">
<div>
<div class="panel-kicker">
RUNTIME
</div>
<h2>
Scanner Output
</h2>
</div>
<span class="panel-hint">
Live output
</span>
</div>
<pre id="scanner-output">{% for line in output %}{{ line }}
{% endfor %}</pre>
</section>
</main>
<script>
let scannerWasRunning = false;
function updateScannerHeader(
health
)
{
const status =
document.getElementById(
"header-scanner-status"
);
const indicator =
status.querySelector(
".status-indicator"
);
const text =
status.querySelector(
".status-text"
);
status.className =
"header-status " +
health.toLowerCase();
indicator.className =
"status-indicator " +
health.toLowerCase();
if (health === "ACTIVE")
{
text.textContent =
"SCANNER ACTIVE";
}
else if (health === "WARNING")
{
text.textContent =
"WARNING";
}
else if (health === "STOPPING")
{
text.textContent =
"SCANNER STOPPING";
}
else if (health === "STOPPED")
{
text.textContent =
"SCANNER STOPPED";
}
else
{
text.textContent =
"SCANNER READY";
}
}
function updateGpsDiagnostics(
gpsTest,
scannerRunning
)
{
const status =
document.getElementById(
"gps-test-status"
);
const state =
document.getElementById(
"gps-test-state"
);
const indicator =
document.getElementById(
"gps-test-indicator"
);
const message =
document.getElementById(
"gps-test-message"
);
const nmea =
document.getElementById(
"gps-test-nmea"
);
const fix =
document.getElementById(
"gps-test-fix"
);
const latitude =
document.getElementById(
"gps-test-latitude"
);
const longitude =
document.getElementById(
"gps-test-longitude"
);
const button =
document.getElementById(
"gps-test-button"
);
if (!gpsTest)
{
return;
}
if (button)
{
button.textContent =
gpsTest.running
? "STOP GPS TEST"
: "TEST GPS";
button.disabled =
!gpsTest.running
&&
scannerRunning;
}
nmea.textContent =
gpsTest.nmea_count;
fix.textContent =
gpsTest.fix_count;
latitude.textContent =
gpsTest.latitude !== null
? Number(gpsTest.latitude).toFixed(6)
: "—";
longitude.textContent =
gpsTest.longitude !== null
? Number(gpsTest.longitude).toFixed(6)
: "—";
if (gpsTest.state === "STARTING")
{
status.textContent =
"GPS CONNECTING";
indicator.className =
"gps-state-indicator connecting";
indicator.textContent =
"●";
state.textContent =
"Подключение к GPS…";
message.textContent =
"Connecting to GPS source.";
}
else if (
gpsTest.state === "CONNECTED"
)
{
status.textContent =
"GPS CONNECTED";
indicator.className =
"gps-state-indicator connecting";
indicator.textContent =
"●";
state.textContent =
"Подключение к GPS…";
message.textContent =
"GPS source connected. Waiting for NMEA.";
}
else if (
gpsTest.state === "NMEA"
)
{
status.textContent =
"WAITING FOR FIX";
indicator.className =
"gps-state-indicator nmea";
indicator.textContent =
"●";
state.textContent =
"NMEA получен, FIX ещё нет";
message.textContent =
"NMEA data received, but no valid coordinates yet.";
}
else if (
gpsTest.state === "FIX"
)
{
status.textContent =
"GPS READY";
indicator.className =
"gps-state-indicator fix";
indicator.textContent =
"●";
state.textContent =
"GPS готов — FIX получен";
message.textContent =
"Valid GPS coordinates received.";
}
else if (
gpsTest.state === "ERROR"
)
{
status.textContent =
"GPS ERROR";
indicator.className =
"gps-state-indicator error";
indicator.textContent =
"●";
state.textContent =
"Ошибка подключения / источника";
message.textContent =
gpsTest.error ||
"Unable to connect to GPS source.";
}
else if (
gpsTest.state === "STOPPED"
)
{
status.textContent =
"GPS STOPPED";
indicator.className =
"gps-state-indicator stopped";
indicator.textContent =
"●";
state.textContent =
"Тест остановлен";
message.textContent =
"GPS test stopped.";
}
else
{
status.textContent =
"GPS READY";
indicator.className =
"gps-state-indicator ready";
indicator.textContent =
"●";
state.textContent =
"Готов к тесту";
message.textContent =
"GPS test is ready.";
}
}
async function updateStatus()
{
try
{
const response =
await fetch(
"{{ url_for('api_status') }}"
);
const data =
await response.json();
updateGpsDiagnostics(
data.gps_test,
data.status.running
);
updateScannerHeader(
data.status.health
);
const scannerState =
document.querySelector(
".scanner-state"
);
if (data.status.running)
{
scannerState.innerHTML =
"<strong class=\"running\">" +
"RUNNING" +
"</strong>" +
"<span>PID " +
data.status.pid +
"</span>";
}
else
{
scannerState.innerHTML =
"<strong class=\"stopped\">" +
"STOPPED" +
"</strong>";
}
const output =
document.getElementById(
"scanner-output"
);
const scannerStopping =
data.status.health === "STOPPING";
const interfaceSelect =
document.getElementById(
"interface"
);
const startButtons =
document.querySelectorAll(
'button.start[form="interface-form"], ' +
'#interface-form button.start'
);
const stopButtons =
document.querySelectorAll(
'form[action="{{ url_for("scanner_stop") }}"] button.stop'
);
interfaceSelect.disabled =
data.status.running ||
data.gps_test.running;
startButtons.forEach(
function(button)
{
button.disabled =
data.status.running ||
data.gps_test.running;
}
);
stopButtons.forEach(
function(button)
{
button.disabled =
!data.status.running ||
scannerStopping;
}
);
output.textContent =
data.output.join("\n");
if (data.status.running)
{
scannerWasRunning = true;
output.scrollTop =
output.scrollHeight;
}
else if (scannerWasRunning)
{
scannerWasRunning = false;
}
}
catch (error)
{
console.error(error);
}
}
setInterval(
updateStatus,
1000
);
const WIFI_BANDS = [
"2.4",
"5",
"6"
];
const wifiChannelState = {
"2.4": new Set(),
"5": new Set(),
"6": new Set()
};
function getWifiChannelSuffix(band)
{
if (band === "2.4")
{
return "a";
}
if (band === "5")
{
return "b";
}
return "c";
}
function getWifiChannelContainer(band)
{
return document.getElementById(
band === "2.4"
? "wifi-channels-2-4"
: `wifi-channels-${band}`
);
}
function renderWifiChannels(
band,
availableChannels
)
{
const container =
getWifiChannelContainer(band);
if (!container)
{
return;
}
container.innerHTML = "";
const channels = [
...new Set(
availableChannels.map(
item => item.channel
)
)
].sort(
(a, b) => a - b
);
const available = new Set(
availableChannels
.filter(
item => item.available === true
)
.map(
item => item.channel
)
);
for (const channel of channels)
{
const availableNow =
available.has(channel);
const suffix =
getWifiChannelSuffix(band);
const value =
`${channel}${suffix}`;
if (!availableNow)
{
wifiChannelState[band].delete(
value
);
}
const label =
document.createElement("label");
label.className =
"wifi-channel";
if (!availableNow)
{
label.classList.add(
"unavailable"
);
}
const checkbox =
document.createElement("input");
checkbox.type = "checkbox";
checkbox.value = value;
checkbox.dataset.band = band;
checkbox.disabled =
!availableNow;
checkbox.checked =
availableNow &&
wifiChannelState[band].has(
value
);
checkbox.addEventListener(
"change",
function()
{
const toggle =
document.getElementById(
band === "2.4"
? "wifi-enable-2-4ghz"
: band === "5"
? "wifi-enable-5ghz"
: "wifi-enable-6ghz"
);
if (this.checked)
{
wifiChannelState[
band
].add(
value
);
if (
toggle &&
!toggle.disabled
)
{
toggle.checked =
true;
}
}
else
{
wifiChannelState[
band
].delete(
value
);
if (
toggle &&
wifiChannelState[
band
].size === 0
)
{
toggle.checked =
false;
}
}
updateWifiChannelsValue();
}
);
const text =
document.createElement("span");
text.textContent =
availableNow
? channel
: `${channel} (недоступен)`;
label.appendChild(
checkbox
);
label.appendChild(
text
);
container.appendChild(
label
);
}
updateWifiBandAvailability(
band,
availableChannels.some(
item => item.available === true
)
);
}
function updateWifiBandAvailability(
band,
available
)
{
const buttons =
document.querySelectorAll(
`.channel-action[data-band="${band}"]`
);
buttons.forEach(
function(button)
{
button.disabled =
!available;
}
);
const container =
getWifiChannelContainer(
band
);
if (container)
{
container.classList.toggle(
"band-unavailable",
!available
);
}
const toggle =
document.getElementById(
band === "2.4"
? "wifi-enable-2-4ghz"
: band === "5"
? "wifi-enable-5ghz"
: "wifi-enable-6ghz"
);
if (!toggle)
{
return;
}
toggle.disabled =
!available;
if (!available)
{
toggle.checked =
false;
setWifiBandSelection(
band,
false
);
return;
}
if (wifiChannelState[band].size > 0)
{
toggle.checked =
true;
}
}
function updateWifiChannelsValue()
{
const values = [];
[
"2.4",
"5",
"6"
].forEach(
function(band)
{
for (
const value
of wifiChannelState[band]
)
{
values.push(value);
}
}
);
const order = {
"a": 0,
"b": 1,
"c": 2
};
values.sort(
function(a, b)
{
const channelA =
parseInt(a, 10);
const channelB =
parseInt(b, 10);
if (
channelA !==
channelB
)
{
return (
channelA -
channelB
);
}
return (
order[
a.slice(-1)
] -
order[
b.slice(-1)
]
);
}
);
document.getElementById(
"wifi-channels"
).value =
values.join(",");
}
function restoreWifiChannelState(
configuredChannels
)
{
[
"2.4",
"5",
"6"
].forEach(
function(band)
{
wifiChannelState[band].clear();
}
);
const configuredValues = [
configuredChannels["2.4"] || "",
configuredChannels["5"] || "",
configuredChannels["6"] || ""
];
for (
const value
of configuredValues
.join(",")
.split(",")
.map(
item => item.trim()
)
.filter(Boolean)
)
{
if (value.endsWith("a"))
{
wifiChannelState[
"2.4"
].add(value);
continue;
}
if (value.endsWith("b"))
{
wifiChannelState[
"5"
].add(value);
continue;
}
if (value.endsWith("c"))
{
wifiChannelState[
"6"
].add(value);
continue;
}
const channel =
parseInt(value, 10);
if (
channel >= 1 &&
channel <= 13
)
{
wifiChannelState[
"2.4"
].add(
`${channel}a`
);
}
}
}
async function detectWifiChannels(
interfaceName
)
{
const status =
document.getElementById(
"wifi-diagnostics-status"
);
const interfaceLabel =
document.getElementById(
"wifi-diagnostics-interface"
);
const error =
document.getElementById(
"wifi-channel-error"
);
interfaceLabel.textContent =
interfaceName || "not selected";
error.hidden = true;
if (!interfaceName)
{
status.textContent =
"Select interface to detect channels";
[
"2.4",
"5",
"6"
].forEach(
function(band)
{
renderWifiChannels(
band,
[]
);
}
);
return;
}
try
{
const response =
await fetch(
"{{ url_for('api_wifi_channels') }}"
+
"?interface="
+
encodeURIComponent(
interfaceName
)
);
const data =
await response.json();
if (!response.ok)
{
throw new Error(
data.error
||
"Channel detection failed."
);
}
renderWifiChannels(
"2.4",
data["2.4"] || []
);
renderWifiChannels(
"5",
data["5"] || []
);
renderWifiChannels(
"6",
data["6"] || []
);
const count =
(data["2.4"] || []).filter(
item => item.available === true
).length
+
(data["5"] || []).filter(
item => item.available === true
).length
+
(data["6"] || []).filter(
item => item.available === true
).length;
status.textContent =
`${count} available channels detected`;
if (data.error)
{
throw new Error(
data.error
);
}
updateWifiChannelsValue();
}
catch (errorValue)
{
status.textContent =
"Channel detection failed.";
error.textContent =
errorValue.message;
error.hidden = false;
}
}
function setWifiBandSelection(
band,
selected
)
{
const container =
getWifiChannelContainer(
band
);
if (!container)
{
return;
}
const toggle =
document.getElementById(
band === "2.4"
? "wifi-enable-2-4ghz"
: band === "5"
? "wifi-enable-5ghz"
: "wifi-enable-6ghz"
);
if (
toggle &&
!toggle.disabled
)
{
toggle.checked =
selected;
}
container
.querySelectorAll(
"input[type='checkbox']"
)
.forEach(
function(checkbox)
{
if (
checkbox.disabled
)
{
return;
}
checkbox.checked =
selected;
if (selected)
{
wifiChannelState[
band
].add(
checkbox.value
);
}
else
{
wifiChannelState[
band
].delete(
checkbox.value
);
}
}
);
updateWifiChannelsValue();
}
document
.querySelectorAll(
".channel-action"
)
.forEach(
function(button)
{
button.addEventListener(
"click",
function()
{
const band =
this.dataset.band;
const clear =
this.dataset.action
===
"clear";
setWifiBandSelection(
band,
!clear
);
}
);
}
);
const interfaceSelect =
document.getElementById(
"interface"
);
if (interfaceSelect)
{
interfaceSelect.addEventListener(
"change",
function()
{
detectWifiChannels(
this.value
);
}
);
}
const scannerStartForm =
document.querySelector(
".interface-form"
);
if (scannerStartForm)
{
scannerStartForm.addEventListener(
"submit",
function(event)
{
const hasSelectedChannel =
WIFI_BANDS.some(
function(band)
{
return (
wifiChannelState[
band
].size > 0
);
}
);
if (hasSelectedChannel)
{
return;
}
event.preventDefault();
let error =
document.querySelector(
".interface-form .error"
);
if (!error)
{
error =
document.createElement(
"div"
);
error.className =
"error";
scannerStartForm
.querySelector(
".field"
)
.after(error);
}
error.textContent =
"Select at least one available WiFi channel before starting the scan.";
error.hidden =
false;
}
);
}
[
[
"2.4",
"wifi-enable-2-4ghz",
{{ config.wifi.enable_2_4ghz | tojson }}
],
[
"5",
"wifi-enable-5ghz",
{{ config.wifi.enable_5ghz | tojson }}
],
[
"6",
"wifi-enable-6ghz",
{{ config.wifi.enable_6ghz | tojson }}
]
].forEach(
function(entry)
{
const band =
entry[0];
const toggle =
document.getElementById(
entry[1]
);
if (!toggle)
{
return;
}
toggle.checked =
entry[2] === true ||
wifiChannelState[band].size > 0;
toggle.addEventListener(
"change",
function()
{
if (!this.checked)
{
setWifiBandSelection(
band,
false
);
}
}
);
}
);
restoreWifiChannelState(
{
"2.4":
{{ config.wifi.channels_2_4ghz | tojson }},
"5":
{{ config.wifi.channels_5ghz | tojson }},
"6":
{{ config.wifi.channels_6ghz | tojson }}
}
);
if (interfaceSelect)
{
detectWifiChannels(
interfaceSelect.value
);
}
const configurationForm =
document.getElementById(
"configuration-form"
);
const gpsSource =
document.getElementById(
"gps-source"
);
const gpsrelaySettings =
document.getElementById(
"gpsrelay-settings"
);
const gnssSettings =
document.getElementById(
"gnss-settings"
);
const gnssDirectDeviceField =
document.getElementById(
"gnss-direct-device-field"
);
const gnssDirectDevice =
document.getElementById(
"gnss-direct-device"
);
const gnssModemDeviceField =
document.getElementById(
"gnss-modem-device-field"
);
const gnssModemDevice =
document.getElementById(
"gnss-modem-device"
);
const gnssAtDeviceField =
document.getElementById(
"gnss-at-device-field"
);
const gnssAtDevice =
document.getElementById(
"gnss-at-device"
);
const gnssHandler =
document.getElementById(
"gnss-handler"
);
if (gpsSource)
{
gpsSource.addEventListener(
"change",
function()
{
const isGpsRelay =
gpsSource.value === "gpsrelay";
if (gpsrelaySettings)
{
gpsrelaySettings.hidden =
!isGpsRelay;
}
if (gnssSettings)
{
gnssSettings.hidden =
isGpsRelay;
}
}
);
}
function updateGnssHandlerSettings()
{
if (!gnssHandler)
{
return;
}
const isDirect =
gnssHandler.value === "direct";
const isModem =
gnssHandler.value === "modem";
if (gnssDirectDeviceField)
{
gnssDirectDeviceField.hidden =
!isDirect;
}
if (gnssModemDeviceField)
{
gnssModemDeviceField.hidden =
!isModem;
}
if (gnssAtDeviceField)
{
gnssAtDeviceField.hidden =
!isModem;
}
if (gnssDirectDevice)
{
gnssDirectDevice.required =
isDirect;
}
if (gnssModemDevice)
{
gnssModemDevice.required =
isModem;
}
if (gnssAtDevice)
{
gnssAtDevice.required =
isModem;
}
}
if (gnssHandler)
{
gnssHandler.addEventListener(
"change",
updateGnssHandlerSettings
);
updateGnssHandlerSettings();
}
if (configurationForm)
{
configurationForm.addEventListener(
"submit",
async function(event)
{
event.preventDefault();
const configuration = {
gps: {
source:
document.getElementById(
"gps-source"
).value,
gpsrelay: {
host:
document.getElementById(
"gps-host"
).value.trim(),
port:
Number(
document.getElementById(
"gps-port"
).value
)
},
gnss: {
handler:
document.getElementById(
"gnss-handler"
).value,
direct: {
device:
document.getElementById(
"gnss-direct-device"
).value.trim()
},
gpsd: {},
modem: {
device:
document.getElementById(
"gnss-modem-device"
).value.trim(),
at_device:
document.getElementById(
"gnss-at-device"
).value.trim()
}
}
},
timezone:
document.getElementById(
"timezone"
).value.trim(),
database: {
file:
{{ config.database.file | tojson }}
},
vendor: {
database:
{{ config.vendor.database | tojson }},
sources: {
oui:
{{ config.vendor.sources.oui | tojson }},
mam:
{{ config.vendor.sources.mam | tojson }},
oui36:
{{ config.vendor.sources.oui36 | tojson }}
}
},
wifi: {
enable_2_4ghz:
document.getElementById(
"wifi-enable-2-4ghz"
).checked,
channels_2_4ghz:
Array.from(
wifiChannelState["2.4"]
).join(","),
enable_5ghz:
document.getElementById(
"wifi-enable-5ghz"
).checked,
channels_5ghz:
Array.from(
wifiChannelState["5"]
).join(","),
enable_6ghz:
document.getElementById(
"wifi-enable-6ghz"
).checked,
channels_6ghz:
Array.from(
wifiChannelState["6"]
).join(","),
filtermode:
Number(
{{ config.wifi.filtermode | tojson }}
),
extra_args:
document.getElementById(
"wifi-extra-args"
).value.trim()
},
captures: {
directory:
{{ config.captures.directory | tojson }}
},
reports: {
recent_sessions_limit:
Number(
{{ config.reports.recent_sessions_limit | tojson }}
)
},
logging: {
level:
document.getElementById(
"logging-level"
).value
},
status: {
interval:
Number(
document.getElementById(
"status-interval"
).value
)
}
};
const saveStatus =
document.getElementById(
"configuration-save-status"
);
const saveButton =
configurationForm.querySelector(
'button[type="submit"]'
);
try
{
if (saveStatus)
{
saveStatus.hidden = true;
}
if (saveButton)
{
saveButton.disabled = true;
saveButton.textContent = "SAVING...";
}
const response =
await fetch(
"{{ url_for('config_save') }}",
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body:
JSON.stringify(
configuration
)
}
);
if (response.ok)
{
const result =
await response.json();
if (saveStatus)
{
saveStatus.textContent =
"✓ CONFIGURATION SAVED";
saveStatus.hidden = false;
}
setTimeout(
function()
{
window.location.reload();
},
1500
);
return;
}
let errorMessage =
"Configuration save failed.";
try
{
const result =
await response.json();
if (result.error)
{
errorMessage =
result.error;
}
}
catch (error)
{
// Keep default error message.
}
if (saveStatus)
{
saveStatus.textContent =
"✗ " + errorMessage;
saveStatus.hidden = false;
}
if (saveButton)
{
saveButton.disabled = false;
saveButton.textContent =
"SAVE CONFIGURATION";
}
}
catch (error)
{
console.error(
"Configuration save error:",
error
);
if (saveStatus)
{
saveStatus.textContent =
"✗ " + error.message;
saveStatus.hidden = false;
}
if (saveButton)
{
saveButton.disabled = false;
saveButton.textContent =
"SAVE CONFIGURATION";
}
}
}
);
}
const gpsTestButton =
document.getElementById(
"gps-test-button"
);
if (gpsTestButton)
{
gpsTestButton.addEventListener(
"click",
async function()
{
if (gpsTestButton.disabled)
{
return;
}
gpsTestButton.disabled = true;
try
{
const gpsTest =
await fetch(
"{{ url_for('api_gps_test') }}"
);
const gpsTestData =
await gpsTest.json();
const endpoint =
gpsTestData.running
? "{{ url_for('gps_test_stop') }}"
: "{{ url_for('gps_test_start') }}";
const response =
await fetch(
endpoint,
{
method: "POST"
}
);
const data =
await response.json();
if (!response.ok)
{
throw new Error(
data.error
||
"GPS test request failed."
);
}
updateStatus();
}
catch (error)
{
console.error(
"GPS test error:",
error
);
}
}
);
}
</script>
</body>
</html>