252 lines
9.7 KiB
Cheetah
252 lines
9.7 KiB
Cheetah
{{define "asset"}}
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
{{template "head" .}}
|
|
<body>
|
|
{{template "topbar" .}}
|
|
{{template "breadcrumbs" .}}
|
|
|
|
<main class="container">
|
|
<section class="card">
|
|
<div class="button-row" style="justify-content: space-between; margin-bottom: 16px;">
|
|
<h2 style="margin: 0;">Server Card</h2>
|
|
<div class="button-row">
|
|
<button class="button" id="asset-edit-toggle" type="button">Edit</button>
|
|
<button class="button button-secondary" id="asset-edit-cancel" type="button" hidden>Cancel</button>
|
|
</div>
|
|
</div>
|
|
<div class="meta" id="asset-edit-message" style="margin-bottom: 12px;"></div>
|
|
<div class="meta-grid">
|
|
<div>
|
|
<span>Name</span>
|
|
<div class="field-value">{{.Asset.Name}}</div>
|
|
<input class="input field-input" id="asset-name-input" value="{{.Asset.Name}}" hidden />
|
|
</div>
|
|
<div>
|
|
<span>Vendor Serial</span>
|
|
<div class="field-value">{{.Asset.VendorSerial}}</div>
|
|
<input class="input field-input" id="asset-vendor-serial-input" value="{{.Asset.VendorSerial}}" hidden />
|
|
</div>
|
|
<div>
|
|
<span>Vendor</span>
|
|
<div class="field-value">{{if .Asset.Vendor}}{{.Asset.Vendor}}{{else}}—{{end}}</div>
|
|
<input class="input field-input" id="asset-vendor-input" value="{{if .Asset.Vendor}}{{.Asset.Vendor}}{{end}}" hidden />
|
|
</div>
|
|
<div>
|
|
<span>Model</span>
|
|
<div class="field-value">{{if .Asset.Model}}{{.Asset.Model}}{{else}}—{{end}}</div>
|
|
<input class="input field-input" id="asset-model-input" value="{{if .Asset.Model}}{{.Asset.Model}}{{end}}" hidden />
|
|
</div>
|
|
<div>
|
|
<span>Asset Tag</span>
|
|
<div class="field-value">{{if .Asset.MachineTag}}{{.Asset.MachineTag}}{{else}}—{{end}}</div>
|
|
<input class="input field-input" id="asset-tag-input" value="{{if .Asset.MachineTag}}{{.Asset.MachineTag}}{{end}}" hidden />
|
|
</div>
|
|
<div><span>Status</span><span class="badge {{assetStatusClass .AssetStatus}}">{{assetStatusText .AssetStatus}}</span></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>Current Components</h2>
|
|
{{if .HasFirmwareMismatch}}
|
|
<div class="meta"><span class="badge status-yellow">Firmware mismatch</span> Identical devices on this server have different firmware versions.</div>
|
|
{{end}}
|
|
{{if .ComponentGroups}}
|
|
{{range .ComponentGroups}}
|
|
<h3>{{.TypeTitle}}</h3>
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Status</th>
|
|
<th>Location</th>
|
|
<th>Vendor Serial</th>
|
|
<th>Vendor</th>
|
|
<th>Model</th>
|
|
<th>Firmware</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .Rows}}
|
|
<tr class="clickable" onclick="navigateToRow('/ui/components/{{.Component.ID}}')">
|
|
<td><span class="badge {{componentStatusClass (index $.ComponentStatusByID .Component.ID)}}">{{componentStatusText (index $.ComponentStatusByID .Component.ID)}}</span></td>
|
|
<td>{{if .Location}}{{.Location}}{{else}}—{{end}}</td>
|
|
<td>{{.Component.VendorSerial}}</td>
|
|
<td>{{if .Component.Vendor}}{{.Component.Vendor}}{{else}}—{{end}}</td>
|
|
<td>{{if .Component.Model}}{{.Component.Model}}{{else}}—{{end}}</td>
|
|
<td>
|
|
{{if index $.ComponentFirmwareByID .Component.ID}}{{index $.ComponentFirmwareByID .Component.ID}}{{else}}—{{end}}
|
|
{{if index $.ComponentFirmwareMismatchByID .Component.ID}}
|
|
<span class="badge status-yellow">Mismatch</span>
|
|
{{end}}
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
{{end}}
|
|
{{else}}
|
|
<div class="meta">No active components.</div>
|
|
{{end}}
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>Previous Components</h2>
|
|
{{if .PreviousComponentGroups}}
|
|
{{range .PreviousComponentGroups}}
|
|
<h3>{{.TypeTitle}}</h3>
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Location</th>
|
|
<th>Vendor Serial</th>
|
|
<th>Vendor</th>
|
|
<th>Model</th>
|
|
<th>Firmware</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .Rows}}
|
|
<tr class="clickable" onclick="navigateToRow('/ui/components/{{.Component.ID}}')">
|
|
<td>{{if .Location}}{{.Location}}{{else}}—{{end}}</td>
|
|
<td>{{.Component.VendorSerial}}</td>
|
|
<td>{{if .Component.Vendor}}{{.Component.Vendor}}{{else}}—{{end}}</td>
|
|
<td>{{if .Component.Model}}{{.Component.Model}}{{else}}—{{end}}</td>
|
|
<td>{{if index $.PreviousComponentFirmwareByID .Component.ID}}{{index $.PreviousComponentFirmwareByID .Component.ID}}{{else}}—{{end}}</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
{{end}}
|
|
{{else}}
|
|
<div class="meta">No previous components.</div>
|
|
{{end}}
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>Movement & Events</h2>
|
|
{{if .Events}}
|
|
<div class="timeline">
|
|
{{range .Events}}
|
|
<div class="event">
|
|
<div>
|
|
<div class="time" title="{{formatTimeFull .EventTime}}">{{formatTime .EventTime}}</div>
|
|
<div class="pill {{timelineEventClass .EventType}}">{{.EventType}}</div>
|
|
</div>
|
|
<div>
|
|
<div class="detail">Asset {{assetLabel .MachineID $.AssetLabelByID}} · Component {{componentLabel .PartID $.ComponentLabelByID}}</div>
|
|
<div class="meta">
|
|
Type {{eventComponentType .PartID $.EventComponentByID}} ·
|
|
Location {{eventComponentLocation .PartID $.EventComponentByID}} ·
|
|
Model {{eventComponentModel .PartID $.EventComponentByID}} ·
|
|
Firmware {{if .FirmwareVersion}}{{.FirmwareVersion}}{{else}}{{eventComponentFirmware .PartID $.EventComponentByID}}{{end}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{else}}
|
|
<div class="meta">No timeline events.</div>
|
|
{{end}}
|
|
</section>
|
|
</main>
|
|
<script>
|
|
const assetEditToggle = document.getElementById('asset-edit-toggle');
|
|
const assetEditCancel = document.getElementById('asset-edit-cancel');
|
|
const assetEditMessage = document.getElementById('asset-edit-message');
|
|
const assetFieldValues = [...document.querySelectorAll('.field-value')];
|
|
const assetFieldInputs = [...document.querySelectorAll('.field-input')];
|
|
const assetInputs = {
|
|
name: document.getElementById('asset-name-input'),
|
|
vendorSerial: document.getElementById('asset-vendor-serial-input'),
|
|
vendor: document.getElementById('asset-vendor-input'),
|
|
model: document.getElementById('asset-model-input'),
|
|
assetTag: document.getElementById('asset-tag-input')
|
|
};
|
|
const initialAssetForm = {
|
|
name: assetInputs.name ? assetInputs.name.value : '',
|
|
vendorSerial: assetInputs.vendorSerial ? assetInputs.vendorSerial.value : '',
|
|
vendor: assetInputs.vendor ? assetInputs.vendor.value : '',
|
|
model: assetInputs.model ? assetInputs.model.value : '',
|
|
assetTag: assetInputs.assetTag ? assetInputs.assetTag.value : ''
|
|
};
|
|
let assetEditMode = false;
|
|
|
|
function setAssetMessage(message) {
|
|
if (assetEditMessage) {
|
|
assetEditMessage.textContent = message;
|
|
}
|
|
}
|
|
|
|
function resetAssetForm() {
|
|
if (assetInputs.name) assetInputs.name.value = initialAssetForm.name;
|
|
if (assetInputs.vendorSerial) assetInputs.vendorSerial.value = initialAssetForm.vendorSerial;
|
|
if (assetInputs.vendor) assetInputs.vendor.value = initialAssetForm.vendor;
|
|
if (assetInputs.model) assetInputs.model.value = initialAssetForm.model;
|
|
if (assetInputs.assetTag) assetInputs.assetTag.value = initialAssetForm.assetTag;
|
|
}
|
|
|
|
function setAssetEditMode(enabled) {
|
|
assetEditMode = enabled;
|
|
assetFieldValues.forEach((element) => {
|
|
element.hidden = enabled;
|
|
});
|
|
assetFieldInputs.forEach((element) => {
|
|
element.hidden = !enabled;
|
|
});
|
|
if (assetEditToggle) {
|
|
assetEditToggle.textContent = enabled ? 'Save' : 'Edit';
|
|
}
|
|
if (assetEditCancel) {
|
|
assetEditCancel.hidden = !enabled;
|
|
}
|
|
if (!enabled) {
|
|
setAssetMessage('');
|
|
}
|
|
}
|
|
|
|
async function saveAsset() {
|
|
const payload = {
|
|
name: (assetInputs.name ? assetInputs.name.value : '').trim(),
|
|
vendor_serial: (assetInputs.vendorSerial ? assetInputs.vendorSerial.value : '').trim(),
|
|
vendor: (assetInputs.vendor ? assetInputs.vendor.value : '').trim(),
|
|
model: (assetInputs.model ? assetInputs.model.value : '').trim(),
|
|
asset_tag: (assetInputs.assetTag ? assetInputs.assetTag.value : '').trim()
|
|
};
|
|
if (!payload.name || !payload.vendor_serial) {
|
|
setAssetMessage('Name and vendor serial are required.');
|
|
return;
|
|
}
|
|
const response = await fetch('/registry/assets/{{.Asset.ID}}', {
|
|
method: 'PUT',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
if (!response.ok) {
|
|
const body = await response.json().catch(() => ({error: 'Request failed'}));
|
|
setAssetMessage(body.error || 'Update asset failed');
|
|
return;
|
|
}
|
|
window.location.reload();
|
|
}
|
|
|
|
if (assetEditToggle) {
|
|
assetEditToggle.addEventListener('click', async () => {
|
|
if (!assetEditMode) {
|
|
setAssetEditMode(true);
|
|
return;
|
|
}
|
|
await saveAsset();
|
|
});
|
|
}
|
|
|
|
if (assetEditCancel) {
|
|
assetEditCancel.addEventListener('click', () => {
|
|
resetAssetForm();
|
|
setAssetEditMode(false);
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
{{end}}
|