Files
2026-10-06 21:08:12 +03:00

900 lines
16 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Vendors report page.
Страница управления базой OUI.
Бизнес-логика находится в VendorService.
Этот модуль отвечает только за HTML-представление.
"""
from html import escape
from .statistics import (
top_vendors,
)
from .utils import (
format_datetime as format_report_datetime,
)
from .template import (
page_begin,
page_end,
card,
)
# ============================================================
# Helpers
# ============================================================
def format_size(size):
if not size:
return "-"
size = float(size)
units = (
"B",
"KB",
"MB",
"GB",
)
for unit in units:
if size < 1024:
return f"{size:.1f} {unit}"
size /= 1024
return f"{size:.1f} TB"
def format_value(value):
if value is None or value == "":
return "-"
return escape(
str(value)
)
# ============================================================
# Database status
# ============================================================
def database_status_block(status):
if not status["exists"]:
health = "Not found"
elif status["healthy"]:
health = "Healthy"
else:
health = "Error"
html = f"""
<table>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
<tr>
<td>Database</td>
<td>{format_value(status["database"])}</td>
</tr>
<tr>
<td>Status</td>
<td>{health}</td>
</tr>
<tr>
<td>Total entries</td>
<td>{status["total"]}</td>
</tr>
<tr>
<td>MA-L</td>
<td>{status["ma_l"]}</td>
</tr>
<tr>
<td>MA-M</td>
<td>{status["ma_m"]}</td>
</tr>
<tr>
<td>MA-S</td>
<td>{status["ma_s"]}</td>
</tr>
<tr>
<td>Schema version</td>
<td>{format_value(status["schema_version"])}</td>
</tr>
<tr>
<td>Last import</td>
<td>{format_report_datetime(status["last_import"])}</td>
</tr>
<tr>
<td>Import status</td>
<td>{format_value(status["import_status"])}</td>
</tr>
<tr>
<td>Size</td>
<td>{format_size(status["size"])}</td>
</tr>
</table>
"""
return card(
"🏭 OUI Database",
html,
)
# ============================================================
# Top Vendors
# ============================================================
def top_vendors_block(conn):
rows = top_vendors(
conn,
limit=15
)
html = """
<table>
<tr>
<th>Vendor</th>
<th>Devices</th>
</tr>
"""
if not rows:
html += """
<tr>
<td colspan="2">No vendor data.</td>
</tr>
"""
else:
for vendor, total in rows:
if not vendor:
vendor = "-"
html += f"""
<tr>
<td>{escape(str(vendor))}</td>
<td>{total}</td>
</tr>
"""
html += """
</table>
"""
return card(
"🏭 Top Vendors",
html,
)
# ============================================================
# Vendors page
# ============================================================
def update_block(
service,
preview=None,
token=None,
job=None
):
html = """
"""
sources = service.sources
mirrors = service.mirrors
source_html = """
<table>
<tr>
<th>Source</th>
<th>URL</th>
<th>Mirror</th>
</tr>
"""
for name, url in sources.items():
mirror = mirrors.get(
name
)
source_html += f"""
<tr>
<td>{escape(str(name))}</td>
<td>{escape(str(url))}</td>
<td>{escape(str(mirror)) if mirror else "—"}</td>
</tr>
"""
source_html += """
</table>
"""
html += """
<div class="report-grid">
"""
html += card(
"🌐 Configured Sources",
source_html,
)
if preview is None:
if job is not None:
stage_labels = {
"starting":
"Запуск обновления",
"downloading":
"Загрузка источника",
"validating":
"Проверка источника",
"fallback":
"Переход на резервное зеркало",
"building":
"Построение базы OUI",
"analyzing":
"Анализ изменений",
"ready":
"Обновление готово",
"error":
"Ошибка обновления"
}
stage = job.get(
"stage"
)
stage_label = stage_labels.get(
stage,
stage or "Неизвестный этап"
)
source = job.get(
"source"
)
message = job.get(
"message"
)
error = job.get(
"error"
)
html += f"""
<div class="card">
<h2>Обновление базы OUI</h2>
<p>
<strong>Этап:</strong>
<span id="vendor-update-stage">
{escape(stage_label)}
</span>
</p>
<p>
<strong>Источник:</strong>
<span id="vendor-update-source">
{escape(str(source)) if source else "—"}
</span>
</p>
<p>
<span id="vendor-update-message">
{escape(str(message)) if message else ""}
</span>
</p>
<p id="vendor-update-error">
{escape(str(error)) if error else ""}
</p>
<script>
(function(){{
const jobId = {job["id"]!r};
const stageLabels = {{
"starting": "Запуск обновления",
"downloading": "Загрузка источника",
"validating": "Проверка источника",
"fallback": "Переход на резервное зеркало",
"building": "Построение базы OUI",
"analyzing": "Анализ изменений",
"ready": "Обновление готово",
"error": "Ошибка обновления"
}};
async function pollVendorUpdate(){{
try {{
const response = await fetch(
"/reports/vendors/update/status/"
+ encodeURIComponent(jobId),
{{
cache: "no-store"
}}
);
if (!response.ok) {{
return;
}}
const data = await response.json();
const stage =
document.getElementById(
"vendor-update-stage"
);
const source =
document.getElementById(
"vendor-update-source"
);
const message =
document.getElementById(
"vendor-update-message"
);
const error =
document.getElementById(
"vendor-update-error"
);
if (stage) {{
stage.textContent =
stageLabels[data.stage]
|| data.stage
|| "Неизвестный этап";
}}
if (source) {{
source.textContent =
data.source || "—";
}}
if (message) {{
message.textContent =
data.message || "";
}}
if (error) {{
error.textContent =
data.error || "";
}}
if (
data.status === "ready"
&& data.preview_ready
) {{
window.location.href =
"/reports/vendors/?job_id="
+ encodeURIComponent(jobId);
return;
}}
if (
data.status === "error"
|| data.status === "missing"
) {{
return;
}}
}} catch (error) {{
console.error(
"Vendor update polling failed:",
error
);
}}
window.setTimeout(
pollVendorUpdate,
1000
);
}}
pollVendorUpdate();
}})();
</script>
"""
if job.get(
"status"
) == "running":
html += """
<p>
Обновление выполняется. Страница автоматически
обновит результат после завершения.
</p>
"""
elif job.get(
"status"
) == "error":
html += """
<form
method="post"
action="/reports/vendors/update"
>
<div class="vendor-actions">
<button type="submit" class="button">
Повторить обновление
</button>
</div>
</form>
"""
html += """
</div>
"""
else:
html += """
<div class="card">
<h2>Обновление базы OUI</h2>
<form
method="post"
action="/reports/vendors/update"
>
<div class="vendor-actions">
<button type="submit" class="button">
Проверить обновления
</button>
</div>
</form>
</div>
"""
html += """
</div>
"""
return html
oui = preview["oui"]
build = preview["build"]
ap = preview["ap"]
ap_total = (
ap["unknown_to_vendor"]
+ ap["vendor_to_vendor"]
+ ap["vendor_to_unknown"]
)
if oui["identical"]:
status_html = """
<p>
База OUI не изменилась. Обновление не требуется.
</p>
"""
elif ap_total == 0:
status_html = """
<p>
База OUI содержит изменения, но существующие записи
AP не требуют изменения vendor.
</p>
<p>
Обновление базы OUI можно применить отдельно.
Оно обновит справочник для будущих определений vendor.
</p>
"""
else:
status_html = """
<p>
База OUI содержит изменения.
Сначала проверьте изменения в справочнике, затем при
необходимости выберите синхронизацию vendor у существующих AP.
</p>
"""
change_html = f"""
<table>
<tr>
<th>Параметр</th>
<th>Значение</th>
</tr>
<tr>
<td>Текущих записей</td>
<td>{oui["current"]}</td>
</tr>
<tr>
<td>Записей в новой базе</td>
<td>{oui["candidate"]}</td>
</tr>
<tr>
<td>Добавлено</td>
<td>{oui["added"]}</td>
</tr>
<tr>
<td>Изменено</td>
<td>{oui["changed"]}</td>
</tr>
<tr>
<td>Удалено</td>
<td>{oui["removed"]}</td>
</tr>
<tr>
<td>Без изменений</td>
<td>{oui["unchanged"]}</td>
</tr>
<tr>
<td>Импортировано</td>
<td>{build["imported"]}</td>
</tr>
<tr>
<td>Пропущено</td>
<td>{build["skipped"]}</td>
</tr>
</table>
"""
html += card(
"🔄 Предварительный просмотр обновления",
status_html + change_html,
)
html += """
</div>
"""
if oui["identical"]:
pass
else:
html += f"""
<form
method="post"
action="/reports/vendors/apply"
onsubmit="return confirm('Применить обновление базы OUI?');"
>
<input
type="hidden"
name="token"
value="{escape(token)}"
>
"""
if ap_total > 0:
html += """
<div class="vendor-sync">
<hr>
<h2>
Синхронизация существующих AP
</h2>
<p>
Следующие параметры не изменяют саму базу OUI.
Они определяют, какие изменения vendor применить
к уже существующим записям WiFi AP.
</p>
<table>
<tr>
<th>Изменение vendor</th>
<th>Количество</th>
<th>Применить</th>
</tr>
<tr>
<td>
<strong>Unknown → Vendor</strong>
<details>
<summary>Что это означает?</summary>
<p>
Заполнить поле vendor у существующих AP, для которых
vendor сейчас неизвестен. Новое значение определяется
по обновлённой базе OUI.
</p>
</details>
</td>
<td>""" + str(ap["unknown_to_vendor"]) + """</td>
<td>
<input
type="checkbox"
name="unknown_to_vendor"
value="1"
checked
onchange="updateVendorApplyCount()"
>
</td>
</tr>
<tr>
<td>
<strong>Vendor → другой Vendor</strong>
<details>
<summary>Что это означает?</summary>
<p>
Заменить текущее значение vendor, если обновлённая
база OUI определяет тот же AP как принадлежащий
другому vendor.
</p>
</details>
</td>
<td>""" + str(ap["vendor_to_vendor"]) + """</td>
<td>
<input
type="checkbox"
name="vendor_to_vendor"
value="1"
onchange="updateVendorApplyCount()"
>
</td>
</tr>
<tr>
<td>
<strong>Vendor → Unknown</strong>
<details>
<summary>Что это означает?</summary>
<p>
Удалить текущее значение vendor, если обновлённая
база OUI больше не позволяет определить vendor
для данного AP.
</p>
</details>
</td>
<td>""" + str(ap["vendor_to_unknown"]) + """</td>
<td>
<input
type="checkbox"
name="vendor_to_unknown"
value="1"
onchange="updateVendorApplyCount()"
>
</td>
</tr>
</table>
<p>
Выбрано изменений AP:
<strong id="vendor-apply-count">""" + str(
ap["unknown_to_vendor"]
) + """</strong>
</p>
<p>
Синхронизация AP необязательна. Снятые категории
не будут изменять существующие записи AP.
</p>
</div>
"""
else:
html += """
<div class="vendor-sync vendor-sync-empty">
<hr>
<h2>
Синхронизация существующих AP
</h2>
<p>
Существующие записи AP не требуют изменения vendor.
Для этого обновления синхронизация AP недоступна.
</p>
</div>
"""
html += """
<div class="vendor-actions">
<button type="submit" class="button">
Применить обновление базы OUI
</button>
</div>
</form>
"""
if ap_total > 0:
html += f"""
<script>
function updateVendorApplyCount() {{
let total = 0;
const fields = [
[
"unknown_to_vendor",
{ap["unknown_to_vendor"]}
],
[
"vendor_to_vendor",
{ap["vendor_to_vendor"]}
],
[
"vendor_to_unknown",
{ap["vendor_to_unknown"]}
]
];
for (const field of fields) {{
const checkbox = document.querySelector(
'input[name="' + field[0] + '"]'
);
if (checkbox && checkbox.checked) {{
total += field[1];
}}
}}
document.getElementById(
"vendor-apply-count"
).textContent = total;
}}
</script>
"""
return html
def render_vendors_page(
conn,
service,
preview=None,
token=None,
message=None,
context=None,
job=None
):
status = service.status()
html = page_begin(
title="Vendors",
description=(
"Vendor database and OUI management"
),
active="vendors",
context=context,
)
if message:
html += f"""
<div class="card">
<p>
{escape(message["text"])}
</p>
</div>
"""
html += """
<div class="report-grid">
"""
html += database_status_block(
status
)
html += top_vendors_block(
conn
)
html += """
</div>
"""
html += update_block(
service,
preview,
token,
job
)
html += page_end()
return html