194 lines
8.2 KiB
Cheetah
194 lines
8.2 KiB
Cheetah
{{define "ingest"}}
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
{{template "head" .}}
|
||
<body>
|
||
{{template "topbar" .}}
|
||
{{template "breadcrumbs" .}}
|
||
|
||
<main class="container">
|
||
<section class="card">
|
||
<h2>Manual CSV Import</h2>
|
||
<div class="meta" style="margin-bottom: 12px;">
|
||
Required columns: <code>дата_осмотра</code>, <code>серийный_номер_сервера</code>, <code>вендор</code>, <code>p/n_устройства</code>, <code>s/n_устройства</code>.
|
||
</div>
|
||
<div class="meta" style="margin-bottom: 12px;">
|
||
Optional columns: <code>локейшн_в_сервере</code>, <code>версия_прошивки</code>, <code>состояние_оборудования</code>
|
||
(values: <code>Рабочий</code>, <code>Не рабочий</code>, <code>Не проверял</code>).
|
||
</div>
|
||
<div class="meta" style="margin-bottom: 16px;">
|
||
<a href="/ui/ingest/manual-template.csv">Download CSV template</a>
|
||
</div>
|
||
|
||
<form class="form" data-endpoint="/ingest/manual/csv" data-kind="multipart">
|
||
<div class="field">
|
||
<label for="manualCsvFile">CSV File</label>
|
||
<input class="input" id="manualCsvFile" name="file" type="file" accept=".csv,text/csv" />
|
||
</div>
|
||
<button class="button" type="submit">Import CSV</button>
|
||
<div class="meta-grid" data-metrics style="display:none;"></div>
|
||
<pre class="meta" data-response></pre>
|
||
</form>
|
||
</section>
|
||
|
||
<section class="card">
|
||
<h2>Hardware Ingest</h2>
|
||
<form class="form" data-endpoint="/ingest/hardware" data-kind="json">
|
||
<div class="field">
|
||
<label for="hardware">Payload (JSON)</label>
|
||
<textarea class="input" id="hardware" name="payload" rows="10">{{.HardwarePayload}}</textarea>
|
||
</div>
|
||
<div class="field">
|
||
<label for="hardwareFile">Upload a JSON snapshot (per INTEGRATION_GUIDE.md)</label>
|
||
<input class="input" id="hardwareFile" type="file" accept="application/json" />
|
||
</div>
|
||
<button class="button" type="submit">Send Hardware Snapshot</button>
|
||
<pre class="meta" data-response></pre>
|
||
</form>
|
||
</section>
|
||
</main>
|
||
|
||
<script>
|
||
const forms = document.querySelectorAll("form[data-endpoint]");
|
||
forms.forEach((form) => {
|
||
form.addEventListener("submit", async (event) => {
|
||
event.preventDefault();
|
||
const endpoint = form.getAttribute("data-endpoint");
|
||
const kind = form.getAttribute("data-kind");
|
||
const output = form.querySelector("[data-response]");
|
||
const metricsContainer = form.querySelector("[data-metrics]");
|
||
if (!endpoint || !output) return;
|
||
|
||
output.textContent = "Sending...";
|
||
if (metricsContainer) {
|
||
metricsContainer.style.display = "none";
|
||
metricsContainer.innerHTML = "";
|
||
}
|
||
try {
|
||
let response;
|
||
if (kind === "multipart") {
|
||
const fileInput = form.querySelector("input[name='file']");
|
||
const file = fileInput?.files?.[0];
|
||
if (!file) {
|
||
output.textContent = "Please select a CSV file.";
|
||
return;
|
||
}
|
||
const formData = new FormData();
|
||
formData.set("file", file);
|
||
response = await fetch(`${endpoint}?async=1`, {
|
||
method: "POST",
|
||
body: formData,
|
||
});
|
||
} else {
|
||
const textarea = form.querySelector("textarea[name='payload']");
|
||
if (!textarea) {
|
||
output.textContent = "Payload input is missing.";
|
||
return;
|
||
}
|
||
response = await fetch(endpoint, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: textarea.value,
|
||
});
|
||
}
|
||
const text = await response.text();
|
||
if (kind === "multipart" && response.status === 202) {
|
||
const accepted = JSON.parse(text);
|
||
const jobID = accepted?.job_id;
|
||
if (!jobID) {
|
||
output.textContent = `${response.status} ${response.statusText}\n${text}`;
|
||
return;
|
||
}
|
||
output.textContent = `202 Accepted\nJob: ${jobID}\nStatus: queued`;
|
||
const result = await pollManualJob(jobID, output);
|
||
output.textContent = `${result.result_code ?? 200} Completed\n${JSON.stringify(result.result ?? {}, null, 2)}`;
|
||
if (metricsContainer) {
|
||
const metrics = result?.result?.metrics;
|
||
if (metrics && typeof metrics === "object") {
|
||
const rowsPerSec = Number(metrics.rows_per_sec || 0);
|
||
const items = [
|
||
["Parse, ms", metrics.parse_ms],
|
||
["Grouping, ms", metrics.grouping_ms],
|
||
["Ingest total, ms", metrics.ingest_ms_total],
|
||
["Total request, ms", metrics.total_ms],
|
||
["Rows/sec", rowsPerSec.toFixed(2)],
|
||
["Groups total", metrics.groups_total],
|
||
["Groups success", metrics.groups_success],
|
||
["Workers used", metrics.workers_used],
|
||
];
|
||
metricsContainer.innerHTML = items.map(([label, value]) => (
|
||
`<div><span>${label}</span><strong>${value ?? "—"}</strong></div>`
|
||
)).join("");
|
||
metricsContainer.style.display = "grid";
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
output.textContent = `${response.status} ${response.statusText}\n${text}`;
|
||
if (metricsContainer) {
|
||
try {
|
||
const parsed = JSON.parse(text);
|
||
const metrics = parsed?.metrics;
|
||
if (metrics && typeof metrics === "object") {
|
||
const rowsPerSec = Number(metrics.rows_per_sec || 0);
|
||
const items = [
|
||
["Parse, ms", metrics.parse_ms],
|
||
["Grouping, ms", metrics.grouping_ms],
|
||
["Ingest total, ms", metrics.ingest_ms_total],
|
||
["Total request, ms", metrics.total_ms],
|
||
["Rows/sec", rowsPerSec.toFixed(2)],
|
||
["Groups total", metrics.groups_total],
|
||
["Groups success", metrics.groups_success],
|
||
["Workers used", metrics.workers_used],
|
||
];
|
||
metricsContainer.innerHTML = items.map(([label, value]) => (
|
||
`<div><span>${label}</span><strong>${value ?? "—"}</strong></div>`
|
||
)).join("");
|
||
metricsContainer.style.display = "grid";
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
} catch (err) {
|
||
output.textContent = `Request failed: ${err}`;
|
||
}
|
||
});
|
||
});
|
||
|
||
const hardwareFileInput = document.getElementById("hardwareFile");
|
||
const hardwareTextarea = document.getElementById("hardware");
|
||
if (hardwareFileInput && hardwareTextarea) {
|
||
hardwareFileInput.addEventListener("change", async () => {
|
||
const file = hardwareFileInput.files?.[0];
|
||
if (!file) {
|
||
return;
|
||
}
|
||
try {
|
||
const text = await file.text();
|
||
hardwareTextarea.value = text;
|
||
} catch (err) {
|
||
alert(`Failed to load file: ${err}`);
|
||
}
|
||
});
|
||
}
|
||
|
||
async function pollManualJob(jobID, outputNode) {
|
||
const startedAt = Date.now();
|
||
while (true) {
|
||
const response = await fetch(`/ingest/manual/csv/jobs/${encodeURIComponent(jobID)}`);
|
||
const payload = await response.json().catch(() => ({}));
|
||
const job = payload?.job;
|
||
const status = job?.status || "unknown";
|
||
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
||
outputNode.textContent = `202 Accepted\nJob: ${jobID}\nStatus: ${status}\nElapsed: ${elapsed}s`;
|
||
if (status === "completed" || status === "failed") {
|
||
return payload;
|
||
}
|
||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|
||
{{end}}
|