Feature: отдельная страница data.html, общие css и upload-core.js

Made-with: Cursor
This commit is contained in:
cursor-agent
2026-04-06 09:33:33 +00:00
parent 6f84a178d4
commit f742f52f83
6 changed files with 1338 additions and 1057 deletions

210
data.html Normal file
View File

@@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Источник данных — ServiceDesk Monitor</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@400;500;600;700;800&family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<link rel="stylesheet" href="css/dashboard.css" />
</head>
<body>
<div class="page">
<header class="topbar">
<div class="topbar-brand">
<div class="brand-hex" aria-hidden="true"></div>
<div>
<div class="brand-text-main">ServiceDesk Monitor</div>
<div class="brand-text-sub">OMC · Ситуационный центр</div>
</div>
</div>
<nav class="topbar-nav" aria-label="Разделы">
<a href="index.html" class="nav-link">Мониторинг</a>
<a href="data.html" class="nav-link active">Источник данных</a>
</nav>
<div class="topbar-seg">
<span class="seg-label">Статус</span>
<span class="seg-value"><span class="status-pip pip-green" aria-hidden="true"></span> АКТИВЕН</span>
</div>
<div class="topbar-seg">
<span class="seg-label">В базе</span>
<span class="seg-value" id="statusbar-count"></span>
</div>
<div class="topbar-seg">
<span class="seg-label">Дата системы</span>
<span class="seg-value" id="current-date-display"></span>
</div>
</header>
<div class="section-head">
<span class="section-head-label">Подсистема</span>
<span class="section-head-title">Источник данных</span>
<span class="section-head-rule"></span>
<span class="section-head-info">CSV · JSON · Excel → PostgreSQL, ключ «Название»</span>
</div>
<main class="main">
<div class="data-stats-row">
<div class="data-stat-card">
<div class="ds-val" id="db-count"></div>
<div class="ds-lbl">Объектов в базе</div>
<div class="ds-sub" id="db-count-sub">GET /api/incidents</div>
</div>
<div class="data-stat-card">
<div class="ds-val" id="last-applied"></div>
<div class="ds-lbl">Сохранено при последней загрузке</div>
<div class="ds-sub" id="last-upload-sub"></div>
</div>
<div class="data-stat-card">
<div class="ds-val ds-warn" id="last-skipped"></div>
<div class="ds-lbl">Пропущено (нет «Название»)</div>
<div class="ds-sub" id="last-total">Строк в файле: —</div>
</div>
</div>
<div class="upload-panel">
<span class="lbl">Загрузка файла</span>
<button type="button" class="btn-upload" id="btn-upload">↑ Выбрать файл</button>
<span class="upload-hint" id="upload-hint">CSV, JSON или Excel — те же колонки, что в экспорте ServiceDesk</span>
<input type="file" id="file-input" accept=".csv,.json,.xlsx,.xls,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel" hidden />
</div>
<div id="success" class="msg msg-ok"></div>
<div id="loading" class="msg msg-load"></div>
<div id="error" class="msg msg-err"></div>
<div class="data-info-panel">
<strong>Справка</strong>
После разбора файл отправляется в API; для каждой строки ключ — целое число из поля «Название»: существующая запись обновляется, новая добавляется.
Откройте <a href="index.html" style="color:var(--sky);font-weight:600;">мониторинг</a> для графиков и таблицы.
</div>
</main>
</div>
<script src="js/upload-core.js"></script>
<script>
(function () {
var el = function (id) { return document.getElementById(id); };
function pad(n) { return n < 10 ? "0" + n : String(n); }
function formatSystemDate() {
var d = new Date();
return pad(d.getDate()) + "." + pad(d.getMonth() + 1) + "." + d.getFullYear() + " " + pad(d.getHours()) + ":" + pad(d.getMinutes());
}
function refreshDbCount() {
if (!window.OmcUpload) return;
OmcUpload.fetchIncidents()
.then(function (j) {
if (!j || !j.rows) {
el("db-count").textContent = "—";
el("statusbar-count").textContent = "—";
return;
}
var n = j.rows.length;
el("db-count").textContent = n;
el("statusbar-count").textContent = String(n);
})
.catch(function () {
el("db-count").textContent = "—";
el("statusbar-count").textContent = "—";
});
}
function readLastUpload() {
try {
var s = sessionStorage.getItem("omc_last_upload");
if (!s) return null;
return JSON.parse(s);
} catch (e) {
return null;
}
}
function showLastUpload() {
var x = readLastUpload();
if (!x) {
el("last-applied").textContent = "—";
el("last-skipped").textContent = "—";
el("last-total").textContent = "Строк в файле: —";
el("last-upload-sub").textContent = "";
return;
}
el("last-applied").textContent = x.applied != null ? x.applied : "—";
el("last-skipped").textContent = x.skipped != null ? x.skipped : "0";
el("last-total").textContent = "Строк в файле: " + (x.total != null ? x.total : "—");
if (x.at) {
var d = new Date(x.at);
el("last-upload-sub").textContent =
"Время: " + pad(d.getDate()) + "." + pad(d.getMonth() + 1) + "." + d.getFullYear() + " " + pad(d.getHours()) + ":" + pad(d.getMinutes());
}
}
function handleFile(file) {
if (!file) return;
el("upload-hint").textContent = file.name;
el("success").classList.remove("show");
el("error").classList.remove("show");
el("loading").classList.add("show");
el("loading").textContent = "Разбор файла…";
OmcUpload.parseFileToRows(file)
.then(function (rows) {
if (!rows.length) throw new Error("Нет строк данных");
el("loading").textContent = "Сохранение в базу данных…";
return OmcUpload.syncRowsToServer(rows).then(function (j) {
return { j: j, rows: rows };
});
})
.then(function (pack) {
var j = pack.j;
var rows = pack.rows;
sessionStorage.setItem(
"omc_last_upload",
JSON.stringify({
applied: j.applied,
skipped: j.skipped,
total: rows.length,
at: new Date().toISOString()
})
);
refreshDbCount();
showLastUpload();
el("loading").classList.remove("show");
el("error").classList.remove("show");
el("success").classList.add("show");
el("success").textContent =
"✓ В базе сохранено: " + j.applied + " из " + rows.length + " строк" +
(j.skipped ? " (пропущено без «Название»: " + j.skipped + ")" : "");
})
.catch(function (err) {
el("loading").classList.remove("show");
el("success").classList.remove("show");
el("error").classList.add("show");
el("error").textContent = "✗ " + (err && err.message ? err.message : String(err));
});
}
el("btn-upload").addEventListener("click", function () {
el("file-input").click();
});
el("file-input").addEventListener("change", function () {
var f = el("file-input").files[0];
if (f) handleFile(f);
});
function tickClock() {
el("current-date-display").textContent = formatSystemDate();
}
tickClock();
setInterval(tickClock, 30000);
refreshDbCount();
showLastUpload();
})();
</script>
</body>
</html>