Files
core/internal/api/ui_ingest.tmpl

194 lines
8.2 KiB
Cheetah
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.
{{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}}