526 lines
25 KiB
Cheetah
526 lines
25 KiB
Cheetah
{{define "failures"}}
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
{{template "head" .}}
|
|
<body>
|
|
{{template "topbar" .}}
|
|
{{template "breadcrumbs" .}}
|
|
<style>
|
|
.failures-table td {
|
|
vertical-align: top;
|
|
overflow-wrap: anywhere;
|
|
word-break: break-word;
|
|
white-space: normal;
|
|
}
|
|
.failures-details-cell a {
|
|
display: block;
|
|
max-height: 7.2em;
|
|
overflow: auto;
|
|
line-height: 1.2;
|
|
}
|
|
</style>
|
|
|
|
<main class="container">
|
|
<section class="card">
|
|
<div class="button-row" style="justify-content: space-between; margin-bottom: 12px;">
|
|
<h2 style="margin:0;">Active Failures</h2>
|
|
<button class="button" type="button" id="open-manual-failure-modal">Register Failure</button>
|
|
</div>
|
|
<div class="meta" id="manual-failure-page-message" style="margin-bottom:10px;"></div>
|
|
{{if .ActiveFailures}}
|
|
<table class="table failures-table" data-disable-auto-filters="true" style="table-layout:fixed; width:100%;">
|
|
<colgroup>
|
|
<col style="width: 22%;" />
|
|
<col style="width: 24%;" />
|
|
<col style="width: 18%;" />
|
|
<col style="width: 14%;" />
|
|
<col style="width: 22%;" />
|
|
</colgroup>
|
|
<thead>
|
|
<tr>
|
|
<th>Failure Time</th>
|
|
<th>Details</th>
|
|
<th>Server</th>
|
|
<th>Slot</th>
|
|
<th>Component</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .ActiveFailures}}
|
|
<tr>
|
|
<td>
|
|
<a href="/ui/failure/{{.FailureID}}" class="js-local-failure-time" data-utc="{{formatTimeRFC3339 .FailureTime}}" title="{{formatTimeFull .FailureTime}}">
|
|
{{formatTime .FailureTime}}
|
|
</a><br />
|
|
<span class="meta">{{.FailureType}}</span>
|
|
</td>
|
|
<td class="failures-details-cell">
|
|
<a href="/ui/failure/{{.FailureID}}">{{if .Description}}{{.Description}}{{else}}—{{end}}</a>
|
|
</td>
|
|
<td>
|
|
<div><span class="badge {{assetStatusClass .AssetStatus}}">{{assetStatusText .AssetStatus}}</span></div>
|
|
<div style="margin-top:6px;">{{if .AssetID}}{{if .AssetURL}}<a href="{{.AssetURL}}">{{.AssetLabel}}</a>{{else}}{{.AssetLabel}}{{end}}{{else}}—{{end}}</div>
|
|
<div class="meta">{{if .AssetSerial}}{{.AssetSerial}}{{else}}—{{end}}</div>
|
|
</td>
|
|
<td>{{if .Slot}}{{.Slot}}{{else}}—{{end}}</td>
|
|
<td>
|
|
<div><span class="badge {{componentStatusClass .ComponentStatus}}">{{componentStatusText .ComponentStatus}}</span></div>
|
|
<div style="margin-top:6px;">{{if .ComponentURL}}<a href="{{.ComponentURL}}">{{.ComponentLabel}}</a>{{else}}{{.ComponentLabel}}{{end}}</div>
|
|
<div class="meta">{{if .ComponentSerial}}{{.ComponentSerial}}{{else}}—{{end}}</div>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
{{template "pagination" .ActivePager}}
|
|
{{else}}
|
|
<div class="meta">No active failures.</div>
|
|
{{end}}
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2 style="margin-top:0;">Failure Chronology</h2>
|
|
<div class="meta" style="margin-bottom:10px;">Includes failure registrations and repairs by replacing a component in the same slot on the same server.</div>
|
|
{{if .FailureChronologyGroups}}
|
|
{{range .FailureChronologyGroups}}
|
|
<div class="timeline-day">
|
|
<div class="timeline-day-title">{{.Day}}</div>
|
|
{{range .Items}}
|
|
<div class="timeline-card {{if eq .Kind "failure"}}timeline-card-accent-status-failed{{else}}timeline-card-accent-install{{end}}" style="cursor: default; margin-bottom:10px;">
|
|
<div class="timeline-card-header">
|
|
<div class="timeline-card-title">
|
|
{{if eq .Kind "failure"}}Failure{{else}}Replacement repair{{end}}
|
|
</div>
|
|
<div class="timeline-card-time">{{formatTimeFull .EventTime}}</div>
|
|
</div>
|
|
<div class="timeline-card-badges">
|
|
{{if eq .Kind "failure"}}
|
|
<span class="timeline-card-badge timeline-card-badge-status-failed">{{if .FailureType}}{{.FailureType}}{{else}}component_failed{{end}}</span>
|
|
{{else}}
|
|
<span class="timeline-card-badge timeline-card-badge-install">component_installed</span>
|
|
{{end}}
|
|
</div>
|
|
<div class="timeline-card-context" style="margin-top:8px;">
|
|
Server {{if .AssetURL}}<a href="{{.AssetURL}}">{{.AssetLabel}}</a>{{else}}{{.AssetLabel}}{{end}}{{if .AssetSerial}} ({{.AssetSerial}}){{end}} · Slot {{if .Slot}}{{.Slot}}{{else}}—{{end}}
|
|
</div>
|
|
{{if eq .Kind "failure"}}
|
|
<div class="timeline-card-subline" style="margin-top:6px;">
|
|
Component {{if .ComponentURL}}<a href="{{.ComponentURL}}">{{.ComponentLabel}}</a>{{else}}{{.ComponentLabel}}{{end}}{{if .ComponentSerial}} ({{.ComponentSerial}}){{end}}
|
|
{{if .Description}} · {{.Description}}{{end}}
|
|
</div>
|
|
{{else}}
|
|
<div class="timeline-card-subline" style="margin-top:6px;">
|
|
Replaced failed {{if .ComponentURL}}<a href="{{.ComponentURL}}">{{.ComponentLabel}}</a>{{else}}{{.ComponentLabel}}{{end}}
|
|
with {{if .ReplacementComponentURL}}<a href="{{.ReplacementComponentURL}}">{{.ReplacementComponentLabel}}</a>{{else}}{{.ReplacementComponentLabel}}{{end}}
|
|
{{if .ReplacementComponentSerial}} ({{.ReplacementComponentSerial}}){{end}}
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
{{else}}
|
|
<div class="meta">No failure chronology yet.</div>
|
|
{{end}}
|
|
{{template "pagination" .ChronologyPager}}
|
|
</section>
|
|
</main>
|
|
|
|
<datalist id="manual-failure-component-serials">
|
|
{{range .ManualFailureForm.ComponentOptions}}<option value="{{.Serial}}" label="{{if .Model}}{{.Serial}} - {{.Model}}{{else}}{{.Serial}}{{end}}"></option>{{end}}
|
|
</datalist>
|
|
<datalist id="manual-failure-server-serials">
|
|
{{range .ManualFailureForm.ServerSerials}}<option value="{{.}}"></option>{{end}}
|
|
</datalist>
|
|
<datalist id="manual-failure-locations">
|
|
{{range .ManualFailureForm.LocationSuggestions}}<option value="{{.}}"></option>{{end}}
|
|
</datalist>
|
|
<datalist id="manual-failure-dates">
|
|
{{range .ManualFailureForm.DateSuggestions}}<option value="{{.}}"></option>{{end}}
|
|
</datalist>
|
|
<datalist id="manual-failure-descriptions">
|
|
{{range .ManualFailureForm.DescriptionHints}}<option value="{{.}}"></option>{{end}}
|
|
</datalist>
|
|
|
|
<div class="modal" id="manual-failure-modal">
|
|
<div class="modal-card" style="width:min(720px,100%);">
|
|
<div class="button-row" style="justify-content:space-between; align-items:center;">
|
|
<h3 class="modal-header" style="margin:0;">Register Failure</h3>
|
|
<button class="button button-secondary" type="button" data-close-modal="manual-failure-modal">Close</button>
|
|
</div>
|
|
<div class="meta" id="manual-failure-modal-message" style="margin-bottom:10px;"></div>
|
|
<div class="form">
|
|
<div class="field">
|
|
<label for="manual-failure-server-serial">Server Serial</label>
|
|
<input class="input" id="manual-failure-server-serial" list="manual-failure-server-serials" />
|
|
</div>
|
|
<div class="field">
|
|
<label for="manual-failure-location">Location</label>
|
|
<input class="input" id="manual-failure-location" list="manual-failure-locations" placeholder="AOC#1" />
|
|
</div>
|
|
<div class="field">
|
|
<label for="manual-failure-component-serial">Component Serial</label>
|
|
<input class="input" id="manual-failure-component-serial" list="manual-failure-component-serials" />
|
|
</div>
|
|
<div class="field">
|
|
<label for="manual-failure-date">Failure Date</label>
|
|
<input class="input" id="manual-failure-date" list="manual-failure-dates" value="{{.ManualFailureForm.Today}}" />
|
|
</div>
|
|
<div class="field">
|
|
<label for="manual-failure-description">Description</label>
|
|
<input class="input" id="manual-failure-description" list="manual-failure-descriptions" />
|
|
</div>
|
|
<div id="manual-failure-location-candidates"></div>
|
|
<div class="meta" id="manual-failure-resolved-meta"></div>
|
|
<div class="button-row" style="margin-top:10px;">
|
|
<button class="button" type="button" id="manual-failure-review-btn" disabled>Review</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal" id="manual-failure-confirm-modal">
|
|
<div class="modal-card" style="width:min(720px,100%);">
|
|
<div class="button-row" style="justify-content:space-between; align-items:center;">
|
|
<h3 class="modal-header" style="margin:0;">Confirm Failure Registration</h3>
|
|
<button class="button button-secondary" type="button" data-close-modal="manual-failure-confirm-modal">Close</button>
|
|
</div>
|
|
<div class="meta" id="manual-failure-confirm-message" style="margin-bottom:10px;"></div>
|
|
<div class="card" style="padding:12px;">
|
|
<div id="manual-failure-confirm-summary" class="meta"></div>
|
|
</div>
|
|
<div class="button-row" style="margin-top:12px;">
|
|
<button class="button button-secondary" type="button" id="manual-failure-confirm-back">Back</button>
|
|
<button class="button" type="button" id="manual-failure-submit-btn">Confirm & Register</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(() => {
|
|
const lookupByComponent = {{.ManualFailureForm.LookupByComponent}};
|
|
const state = { resolved: null, payload: null };
|
|
const pageMsg = document.getElementById('manual-failure-page-message');
|
|
const modalMsg = document.getElementById('manual-failure-modal-message');
|
|
const resolvedMeta = document.getElementById('manual-failure-resolved-meta');
|
|
const locationCandidates = document.getElementById('manual-failure-location-candidates');
|
|
const confirmMsg = document.getElementById('manual-failure-confirm-message');
|
|
const confirmSummary = document.getElementById('manual-failure-confirm-summary');
|
|
const reviewBtn = document.getElementById('manual-failure-review-btn');
|
|
const submitBtn = document.getElementById('manual-failure-submit-btn');
|
|
const componentInput = document.getElementById('manual-failure-component-serial');
|
|
const serverInput = document.getElementById('manual-failure-server-serial');
|
|
const locationInput = document.getElementById('manual-failure-location');
|
|
const locationDatalist = document.getElementById('manual-failure-locations');
|
|
const dateInput = document.getElementById('manual-failure-date');
|
|
const descInput = document.getElementById('manual-failure-description');
|
|
const allLookupEntries = Object.values(lookupByComponent || {});
|
|
|
|
function setText(el, txt) { if (el) el.textContent = txt || ''; }
|
|
function esc(v) {
|
|
return String(v ?? '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
function openModal(id) { const m = document.getElementById(id); if (m) m.classList.add('open'); }
|
|
function closeModal(id) { const m = document.getElementById(id); if (m) m.classList.remove('open'); }
|
|
document.querySelectorAll('[data-close-modal]').forEach((btn) => {
|
|
btn.addEventListener('click', () => closeModal(btn.getAttribute('data-close-modal')));
|
|
});
|
|
document.querySelectorAll('.modal').forEach((m) => {
|
|
m.addEventListener('click', (e) => { if (e.target === m) m.classList.remove('open'); });
|
|
});
|
|
|
|
function clearModalState() {
|
|
state.resolved = null;
|
|
state.payload = null;
|
|
setText(modalMsg, '');
|
|
setText(confirmMsg, '');
|
|
if (locationCandidates) locationCandidates.innerHTML = '';
|
|
if (resolvedMeta) resolvedMeta.innerHTML = '';
|
|
if (confirmSummary) confirmSummary.innerHTML = '';
|
|
if (reviewBtn) reviewBtn.disabled = true;
|
|
}
|
|
|
|
function renderLocationCandidates(items, message) {
|
|
if (!locationCandidates) return;
|
|
if (!items || !items.length) {
|
|
locationCandidates.innerHTML = '';
|
|
return;
|
|
}
|
|
const rows = items.map((it, idx) => {
|
|
const component = `${it.component_label || it.component_serial || it.component_id || '—'}${it.component_model ? ' · ' + it.component_model : ''}`;
|
|
const server = `${it.server_label || '—'}${it.server_serial ? ' (' + it.server_serial + ')' : ''}`;
|
|
return `
|
|
<button type="button" class="button button-secondary js-manual-failure-location-candidate" data-idx="${idx}" style="justify-content:flex-start; text-align:left; width:100%; margin-top:6px;">
|
|
${esc(component)} · ${esc(it.slot || '—')} · ${esc(server)}
|
|
</button>
|
|
`;
|
|
}).join('');
|
|
locationCandidates.innerHTML = `
|
|
<div class="meta" style="margin-top:8px;">${esc(message || 'Select component for location')}</div>
|
|
<div style="margin-top:4px;">${rows}</div>
|
|
`;
|
|
locationCandidates.querySelectorAll('.js-manual-failure-location-candidate').forEach((btn) => {
|
|
btn.addEventListener('click', () => {
|
|
const idx = Number(btn.getAttribute('data-idx'));
|
|
const picked = items[idx];
|
|
if (!picked) return;
|
|
applyResolvedEntry(picked, 'location');
|
|
if (locationInput && picked.slot) locationInput.value = picked.slot;
|
|
renderResolvedMeta();
|
|
});
|
|
});
|
|
}
|
|
|
|
function refreshLocationDatalist() {
|
|
if (!locationDatalist) return;
|
|
const serverSerial = (serverInput?.value || '').trim().toLowerCase();
|
|
const componentSerial = (componentInput?.value || '').trim().toLowerCase();
|
|
let pool = allLookupEntries.slice();
|
|
if (serverSerial) {
|
|
pool = pool.filter((it) => String(it.server_serial || '').toLowerCase() === serverSerial);
|
|
}
|
|
if (componentSerial) {
|
|
const exact = pool.find((it) => String(it.component_serial || '').toLowerCase() === componentSerial);
|
|
if (exact && exact.slot) {
|
|
pool = [exact];
|
|
}
|
|
}
|
|
const seen = new Set();
|
|
const values = [];
|
|
pool.forEach((it) => {
|
|
const v = String(it.slot || '').trim();
|
|
if (!v) return;
|
|
const k = v.toLowerCase();
|
|
if (seen.has(k)) return;
|
|
seen.add(k);
|
|
values.push(v);
|
|
});
|
|
values.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
|
|
locationDatalist.innerHTML = values.map((v) => `<option value="${esc(v)}"></option>`).join('');
|
|
}
|
|
|
|
function applyResolvedEntry(resolved, origin) {
|
|
clearModalState();
|
|
if (!resolved) return;
|
|
state.resolved = resolved;
|
|
if (origin !== 'serial' && componentInput && resolved.component_serial) componentInput.value = resolved.component_serial;
|
|
if (origin !== 'server' && serverInput && (!serverInput.value || !serverInput.value.trim()) && resolved.server_serial) {
|
|
serverInput.value = resolved.server_serial;
|
|
}
|
|
if (origin !== 'location' && locationInput && (!locationInput.value || !locationInput.value.trim()) && resolved.slot) {
|
|
locationInput.value = resolved.slot;
|
|
}
|
|
if (origin === 'serial' && locationInput && resolved.slot) locationInput.value = resolved.slot;
|
|
if (origin === 'location' && componentInput && resolved.component_serial) componentInput.value = resolved.component_serial;
|
|
}
|
|
|
|
function renderResolvedMeta() {
|
|
const resolved = state.resolved;
|
|
const serverSerial = (serverInput?.value || '').trim();
|
|
const location = (locationInput?.value || '').trim();
|
|
if (!resolved) {
|
|
if (!(componentInput?.value || '').trim()) {
|
|
setText(resolvedMeta, 'Component serial or location is required.');
|
|
}
|
|
renderLocationCandidates([]);
|
|
if (reviewBtn) reviewBtn.disabled = true;
|
|
return;
|
|
}
|
|
const parts = [
|
|
`Component: ${resolved.component_label || resolved.component_serial || resolved.component_id}${resolved.component_model ? ' · ' + resolved.component_model : ''}`,
|
|
resolved.server_label || resolved.server_serial ? `Server: ${(resolved.server_label || '—')}${resolved.server_serial ? ' (' + resolved.server_serial + ')' : ''}` : 'Server: —',
|
|
`Location: ${resolved.slot || '—'}`
|
|
];
|
|
if (serverSerial && resolved.server_serial && serverSerial !== resolved.server_serial) {
|
|
parts.push('Warning: entered server serial differs from recognized current server');
|
|
}
|
|
if (location && resolved.slot && location !== resolved.slot) {
|
|
parts.push('Warning: entered location differs from recognized location');
|
|
}
|
|
if (resolvedMeta) resolvedMeta.innerHTML = parts.map((p) => `<div>${esc(p)}</div>`).join('');
|
|
renderLocationCandidates([]);
|
|
if (reviewBtn) reviewBtn.disabled = false;
|
|
}
|
|
|
|
function resolveBySerial() {
|
|
const componentSerial = (componentInput?.value || '').trim();
|
|
if (!componentSerial) {
|
|
clearModalState();
|
|
renderResolvedMeta();
|
|
return;
|
|
}
|
|
const resolved = lookupByComponent[String(componentSerial).toLowerCase()];
|
|
if (!resolved) {
|
|
clearModalState();
|
|
setText(resolvedMeta, 'Component serial not recognized. Select an existing component serial.');
|
|
return;
|
|
}
|
|
applyResolvedEntry(resolved, 'serial');
|
|
refreshLocationDatalist();
|
|
renderResolvedMeta();
|
|
}
|
|
|
|
function resolveByLocation() {
|
|
const location = (locationInput?.value || '').trim();
|
|
if (!location) {
|
|
if ((componentInput?.value || '').trim()) {
|
|
resolveBySerial();
|
|
} else {
|
|
clearModalState();
|
|
refreshLocationDatalist();
|
|
renderResolvedMeta();
|
|
}
|
|
return;
|
|
}
|
|
const serverSerial = (serverInput?.value || '').trim().toLowerCase();
|
|
const locNeedle = location.toLowerCase();
|
|
let candidates = allLookupEntries.filter((it) => String(it.slot || '').toLowerCase() === locNeedle);
|
|
if (serverSerial) {
|
|
const byServer = candidates.filter((it) => String(it.server_serial || '').toLowerCase() === serverSerial);
|
|
if (byServer.length === 1) {
|
|
applyResolvedEntry(byServer[0], 'location');
|
|
refreshLocationDatalist();
|
|
renderResolvedMeta();
|
|
return;
|
|
}
|
|
if (byServer.length > 1) {
|
|
clearModalState();
|
|
setText(resolvedMeta, 'Multiple components found for this location on the selected server. Select one below.');
|
|
renderLocationCandidates(byServer, 'Select component for this location/server');
|
|
refreshLocationDatalist();
|
|
return;
|
|
}
|
|
}
|
|
if (candidates.length === 1) {
|
|
applyResolvedEntry(candidates[0], 'location');
|
|
refreshLocationDatalist();
|
|
renderResolvedMeta();
|
|
return;
|
|
}
|
|
clearModalState();
|
|
if (candidates.length === 0) {
|
|
setText(resolvedMeta, 'Location not recognized. Enter component serial or server serial + location.');
|
|
} else {
|
|
setText(resolvedMeta, 'Location is ambiguous. Select a component below or enter server serial / component serial.');
|
|
renderLocationCandidates(candidates, 'Multiple components use this location');
|
|
}
|
|
refreshLocationDatalist();
|
|
}
|
|
|
|
function openRegisterModal() {
|
|
clearModalState();
|
|
if (dateInput && !String(dateInput.value || '').trim()) {
|
|
dateInput.value = '{{.ManualFailureForm.Today}}';
|
|
}
|
|
refreshLocationDatalist();
|
|
openModal('manual-failure-modal');
|
|
componentInput?.focus();
|
|
}
|
|
|
|
function buildPayload() {
|
|
const componentSerial = (componentInput?.value || '').trim();
|
|
const serverSerial = (serverInput?.value || '').trim();
|
|
const location = (locationInput?.value || '').trim();
|
|
const failureDate = (dateInput?.value || '').trim();
|
|
const description = (descInput?.value || '').trim();
|
|
if (!componentSerial) return { error: 'Component serial is required.' };
|
|
if (!state.resolved) return { error: 'Component serial must be recognized from suggestions.' };
|
|
if (!failureDate) return { error: 'Failure date is required.' };
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(failureDate)) return { error: 'Failure date must be YYYY-MM-DD.' };
|
|
return {
|
|
payload: {
|
|
component_serial: componentSerial,
|
|
server_serial: serverSerial || null,
|
|
failure_date: failureDate,
|
|
description: description || null
|
|
}
|
|
};
|
|
}
|
|
|
|
function openConfirm() {
|
|
setText(modalMsg, '');
|
|
setText(confirmMsg, '');
|
|
const built = buildPayload();
|
|
if (built.error) {
|
|
setText(modalMsg, built.error);
|
|
return;
|
|
}
|
|
state.payload = built.payload;
|
|
const r = state.resolved || {};
|
|
const enteredLocation = (locationInput?.value || '').trim();
|
|
const lines = [
|
|
`<div><strong>Entered component serial:</strong> ${esc(built.payload.component_serial)}</div>`,
|
|
`<div><strong>Entered server serial:</strong> ${esc(built.payload.server_serial || '—')}</div>`,
|
|
`<div><strong>Entered location:</strong> ${esc(enteredLocation || '—')}</div>`,
|
|
`<div><strong>Failure date:</strong> ${esc(built.payload.failure_date)}</div>`,
|
|
`<div><strong>Description:</strong> ${esc(built.payload.description || '—')}</div>`,
|
|
`<div style="margin-top:8px;"><strong>Recognized component:</strong> ${esc(r.component_label || r.component_serial || r.component_id || '—')}${r.component_model ? ' · ' + esc(r.component_model) : ''}</div>`,
|
|
`<div><strong>Recognized server:</strong> ${esc(r.server_label || '—')}${r.server_serial ? ' (' + esc(r.server_serial) + ')' : ''}</div>`,
|
|
`<div><strong>Recognized location:</strong> ${esc(r.slot || '—')}</div>`
|
|
];
|
|
if (confirmSummary) confirmSummary.innerHTML = lines.join('');
|
|
openModal('manual-failure-confirm-modal');
|
|
}
|
|
|
|
async function submitFailure() {
|
|
if (!state.payload) {
|
|
setText(confirmMsg, 'Nothing to submit.');
|
|
return;
|
|
}
|
|
setText(confirmMsg, '');
|
|
if (submitBtn) submitBtn.disabled = true;
|
|
try {
|
|
const resp = await fetch('/failures', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(state.payload)
|
|
});
|
|
const body = await resp.json().catch(() => ({}));
|
|
if (!resp.ok) {
|
|
setText(confirmMsg, body.error || `Registration failed (${resp.status})`);
|
|
return;
|
|
}
|
|
closeModal('manual-failure-confirm-modal');
|
|
closeModal('manual-failure-modal');
|
|
setText(pageMsg, 'Failure registered.');
|
|
window.location.reload();
|
|
} catch (err) {
|
|
setText(confirmMsg, err?.message || 'Registration failed');
|
|
} finally {
|
|
if (submitBtn) submitBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
document.getElementById('open-manual-failure-modal')?.addEventListener('click', openRegisterModal);
|
|
componentInput?.addEventListener('input', resolveBySerial);
|
|
componentInput?.addEventListener('change', resolveBySerial);
|
|
locationInput?.addEventListener('input', resolveByLocation);
|
|
locationInput?.addEventListener('change', resolveByLocation);
|
|
serverInput?.addEventListener('input', refreshLocationDatalist);
|
|
serverInput?.addEventListener('change', () => {
|
|
if ((locationInput?.value || '').trim()) resolveByLocation();
|
|
else if ((componentInput?.value || '').trim()) resolveBySerial();
|
|
else refreshLocationDatalist();
|
|
});
|
|
document.getElementById('manual-failure-review-btn')?.addEventListener('click', openConfirm);
|
|
document.getElementById('manual-failure-confirm-back')?.addEventListener('click', () => closeModal('manual-failure-confirm-modal'));
|
|
document.getElementById('manual-failure-submit-btn')?.addEventListener('click', () => { submitFailure(); });
|
|
refreshLocationDatalist();
|
|
document.querySelectorAll('.js-local-failure-time').forEach((el) => {
|
|
const raw = el.getAttribute('data-utc');
|
|
if (!raw) return;
|
|
const dt = new Date(raw);
|
|
if (Number.isNaN(dt.getTime())) return;
|
|
el.textContent = dt.toLocaleDateString() + ' ' + dt.toLocaleTimeString();
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
{{end}}
|