feat(*): Add machine notes and MAC address fields

This commit is contained in:
Josh Creek
2026-07-12 20:19:19 +01:00
parent 9cea45945c
commit 336ee5e91b
7 changed files with 98 additions and 7 deletions
+4 -1
View File
@@ -5,10 +5,12 @@
"ipAddress": "192.168.1.10", "ipAddress": "192.168.1.10",
"role": "Hypervisor", "role": "Hypervisor",
"operatingSystem": "Linux", "operatingSystem": "Linux",
"notes": "Optional notes",
"ports": [ "ports": [
{ {
"portName": "eth0", "portName": "eth0",
"speedGbps": 1, "speedGbps": 1,
"macAddress": "aa:bb:cc:dd:ee:01",
"connectedTo": { "connectedTo": {
"device": "Example Switch", "device": "Example Switch",
"port": "1" "port": "1"
@@ -20,7 +22,8 @@
{ {
"name": "Example VM", "name": "Example VM",
"role": "Service", "role": "Service",
"ipAddress": "192.168.1.20" "ipAddress": "192.168.1.20",
"macAddress": "aa:bb:cc:dd:ee:02"
} }
] ]
}, },
+63
View File
@@ -1931,6 +1931,21 @@
)} )}
/> />
</label> </label>
<label>
Notes
<textarea
value={selectedMachine.notes ?? ''}
on:input={(event) =>
mutateDraft(
(draft) => {
draft.machines[selectedTarget.index].notes = (
event.currentTarget as HTMLTextAreaElement
).value;
},
{ autosave: true }
)}
></textarea>
</label>
</section> </section>
<section class="edit-section"> <section class="edit-section">
@@ -2037,6 +2052,15 @@
on:input={() => mutateDraft(() => undefined, { autosave: true })} on:input={() => mutateDraft(() => undefined, { autosave: true })}
/> />
</label> </label>
<label>
MAC Address
<input
type="text"
placeholder="aa:bb:cc:dd:ee:ff"
bind:value={vm.macAddress}
on:input={() => mutateDraft(() => undefined, { autosave: true })}
/>
</label>
<label> <label>
Icon Icon
<input <input
@@ -2100,6 +2124,15 @@
bind:value={port.speedGbps} bind:value={port.speedGbps}
on:input={() => mutateDraft(() => undefined, { autosave: true })} on:input={() => mutateDraft(() => undefined, { autosave: true })}
/> />
</label>
<label>
MAC Address
<input
type="text"
placeholder="aa:bb:cc:dd:ee:ff"
bind:value={port.macAddress}
on:input={() => mutateDraft(() => undefined, { autosave: true })}
/>
</label> </label>
<label> <label>
Connect To Device Connect To Device
@@ -2264,6 +2297,15 @@
bind:value={port.speedGbps} bind:value={port.speedGbps}
on:input={() => mutateDraft(() => undefined, { autosave: true })} on:input={() => mutateDraft(() => undefined, { autosave: true })}
/> />
</label>
<label>
MAC Address
<input
type="text"
placeholder="aa:bb:cc:dd:ee:ff"
bind:value={port.macAddress}
on:input={() => mutateDraft(() => undefined, { autosave: true })}
/>
</label> </label>
<label> <label>
Connect To Device Connect To Device
@@ -2374,6 +2416,23 @@
)} )}
/> />
</label> </label>
<label>
MAC Address
<input
type="text"
placeholder="aa:bb:cc:dd:ee:ff"
value={selectedVm.macAddress ?? ''}
on:input={(event) =>
mutateDraft(
(draft) => {
draft.machines[selectedTarget.machineIndex].software.vms[
selectedTarget.vmIndex
].macAddress = (event.currentTarget as HTMLInputElement).value;
},
{ autosave: true }
)}
/>
</label>
</section> </section>
<div class="modal-footer"> <div class="modal-footer">
<button <button
@@ -2454,6 +2513,10 @@
Operating System Operating System
<input type="text" bind:value={newMachineDraft.operatingSystem} /> <input type="text" bind:value={newMachineDraft.operatingSystem} />
</label> </label>
<label>
Notes
<textarea bind:value={newMachineDraft.notes}></textarea>
</label>
</section> </section>
{:else if addModalKind === 'device'} {:else if addModalKind === 'device'}
<section class="edit-section"> <section class="edit-section">
+4 -1
View File
@@ -110,7 +110,8 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
const machineVms = sourceMachineVms.map((vm) => ({ const machineVms = sourceMachineVms.map((vm) => ({
name: vm.name, name: vm.name,
ip: vm.ipAddress, ip: vm.ipAddress,
role: vm.role role: vm.role,
macAddress: vm.macAddress
})); }));
nodes.push({ nodes.push({
@@ -133,6 +134,7 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
cpu: machine.hardware?.cpu ?? 'Unknown', cpu: machine.hardware?.cpu ?? 'Unknown',
ram: machine.hardware?.ram ?? 'Unknown', ram: machine.hardware?.ram ?? 'Unknown',
gpu: machine.hardware?.gpu ?? undefined, gpu: machine.hardware?.gpu ?? undefined,
notes: machine.notes,
ports: machinePortsList, ports: machinePortsList,
vmCount: machineVms.length, vmCount: machineVms.length,
vms: machineVms vms: machineVms
@@ -166,6 +168,7 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
iconKey: vm.iconKey, iconKey: vm.iconKey,
ip: vm.ipAddress, ip: vm.ipAddress,
role: vm.role, role: vm.role,
macAddress: vm.macAddress,
hostName: machine.machineName hostName: machine.machineName
} }
} }
+4
View File
@@ -4,6 +4,7 @@ export type GraphEdgeKind = 'physical' | 'hosting';
export interface PortDetails { export interface PortDetails {
portName: string; portName: string;
speedGbps?: number; speedGbps?: number;
macAddress?: string;
connectedTo?: { connectedTo?: {
device: string; device: string;
port: string; port: string;
@@ -20,12 +21,14 @@ export interface MachineDetails {
cpu: string; cpu: string;
ram: string; ram: string;
gpu?: string; gpu?: string;
notes?: string;
ports: PortDetails[]; ports: PortDetails[];
vmCount: number; vmCount: number;
vms: Array<{ vms: Array<{
name: string; name: string;
ip: string; ip: string;
role: string; role: string;
macAddress?: string;
}>; }>;
} }
@@ -45,6 +48,7 @@ export interface VmDetails {
iconKey?: string; iconKey?: string;
ip: string; ip: string;
role: string; role: string;
macAddress?: string;
hostName: string; hostName: string;
} }
+16 -4
View File
@@ -1,10 +1,10 @@
/** /**
* @typedef {{ path: string; message: string }} ValidationIssue * @typedef {{ path: string; message: string }} ValidationIssue
* @typedef {{ device: string; port: string }} ConnectedTo * @typedef {{ device: string; port: string }} ConnectedTo
* @typedef {{ portName: string; speedGbps?: number; connectedTo?: ConnectedTo }} Port * @typedef {{ portName: string; speedGbps?: number; macAddress?: string; connectedTo?: ConnectedTo }} Port
* @typedef {{ name: string; role: string; ipAddress: string; iconKey?: string }} VM * @typedef {{ name: string; role: string; ipAddress: string; iconKey?: string; macAddress?: string }} VM
* @typedef {{ cpu: string; ram: string; networkPorts: number; networkPortSpeedGbps?: number; gpu?: string }} Hardware * @typedef {{ cpu: string; ram: string; networkPorts: number; networkPortSpeedGbps?: number; gpu?: string }} Hardware
* @typedef {{ machineName: string; ipAddress: string; role: string; operatingSystem: string; iconKey?: string; software: { vms: VM[] }; hardware: Hardware; ports?: Port[] }} Machine * @typedef {{ machineName: string; ipAddress: string; role: string; operatingSystem: string; iconKey?: string; notes?: string; software: { vms: VM[] }; hardware: Hardware; ports?: Port[] }} Machine
* @typedef {{ name: string; ipAddress: string; type: string; iconKey?: string; notes?: string; ports?: Port[] }} NetworkDevice * @typedef {{ name: string; ipAddress: string; type: string; iconKey?: string; notes?: string; ports?: Port[] }} NetworkDevice
* @typedef {{ machines: Machine[]; devices: NetworkDevice[] }} NetworkData * @typedef {{ machines: Machine[]; devices: NetworkDevice[] }} NetworkData
* @typedef {{ valid: boolean; errors: ValidationIssue[]; data?: NetworkData }} ValidationResult * @typedef {{ valid: boolean; errors: ValidationIssue[]; data?: NetworkData }} ValidationResult
@@ -105,6 +105,10 @@ export function normalizePort(issues, path, value, ownerLabel) {
optional: true, optional: true,
min: 0 min: 0
}); });
const macAddress = readString(issues, `${path}.macAddress`, value.macAddress, {
optional: true,
allowEmpty: true
});
let connectedTo; let connectedTo;
if (value.connectedTo !== undefined) { if (value.connectedTo !== undefined) {
@@ -126,6 +130,7 @@ export function normalizePort(issues, path, value, ownerLabel) {
return { return {
portName, portName,
...(typeof speedGbps === 'number' ? { speedGbps } : {}), ...(typeof speedGbps === 'number' ? { speedGbps } : {}),
...(macAddress ? { macAddress } : {}),
...(connectedTo ? { connectedTo } : {}) ...(connectedTo ? { connectedTo } : {})
}; };
} }
@@ -166,6 +171,10 @@ export function normalizeVm(issues, path, value) {
const role = readString(issues, `${path}.role`, value.role); const role = readString(issues, `${path}.role`, value.role);
const ipAddress = readString(issues, `${path}.ipAddress`, value.ipAddress); const ipAddress = readString(issues, `${path}.ipAddress`, value.ipAddress);
const iconKey = readString(issues, `${path}.iconKey`, value.iconKey, { optional: true }); const iconKey = readString(issues, `${path}.iconKey`, value.iconKey, { optional: true });
const macAddress = readString(issues, `${path}.macAddress`, value.macAddress, {
optional: true,
allowEmpty: true
});
if (!name || !role || !ipAddress) { if (!name || !role || !ipAddress) {
return null; return null;
@@ -175,7 +184,8 @@ export function normalizeVm(issues, path, value) {
name, name,
role, role,
ipAddress, ipAddress,
...(iconKey ? { iconKey } : {}) ...(iconKey ? { iconKey } : {}),
...(macAddress ? { macAddress } : {})
}; };
} }
@@ -196,6 +206,7 @@ function normalizeMachine(issues, path, value) {
const role = readString(issues, `${path}.role`, value.role); const role = readString(issues, `${path}.role`, value.role);
const operatingSystem = readString(issues, `${path}.operatingSystem`, value.operatingSystem); const operatingSystem = readString(issues, `${path}.operatingSystem`, value.operatingSystem);
const iconKey = readString(issues, `${path}.iconKey`, value.iconKey, { optional: true }); const iconKey = readString(issues, `${path}.iconKey`, value.iconKey, { optional: true });
const notes = readString(issues, `${path}.notes`, value.notes, { optional: true, allowEmpty: true });
const softwareRaw = value.software; const softwareRaw = value.software;
if (!isRecord(softwareRaw)) { if (!isRecord(softwareRaw)) {
@@ -287,6 +298,7 @@ function normalizeMachine(issues, path, value) {
role, role,
operatingSystem, operatingSystem,
...(iconKey ? { iconKey } : {}), ...(iconKey ? { iconKey } : {}),
...(notes !== undefined ? { notes } : {}),
software: { vms }, software: { vms },
hardware: { hardware: {
cpu: hardware.cpu, cpu: hardware.cpu,
+3
View File
@@ -6,6 +6,7 @@ export interface ConnectedPortRef {
export interface Port { export interface Port {
portName: string; portName: string;
speedGbps?: number; // default to 1 if not provided speedGbps?: number; // default to 1 if not provided
macAddress?: string;
connectedTo?: ConnectedPortRef; connectedTo?: ConnectedPortRef;
} }
@@ -22,6 +23,7 @@ export interface VM {
role: string; role: string;
ipAddress: string; ipAddress: string;
iconKey?: string; iconKey?: string;
macAddress?: string;
} }
export interface Software { export interface Software {
@@ -34,6 +36,7 @@ export interface Machine {
role: string; role: string;
operatingSystem: string; operatingSystem: string;
iconKey?: string; iconKey?: string;
notes?: string;
software: Software; software: Software;
hardware: Hardware; hardware: Hardware;
ports?: Port[]; ports?: Port[];
+4 -1
View File
@@ -6,12 +6,14 @@
"role": "Hypervisor", "role": "Hypervisor",
"operatingSystem": "Proxmox", "operatingSystem": "Proxmox",
"iconKey": "homarr:proxmox", "iconKey": "homarr:proxmox",
"notes": "Primary router box; do not power off without failover in place.",
"software": { "software": {
"vms": [ "vms": [
{ {
"name": "OpnSense", "name": "OpnSense",
"role": "Router/Firewall/Gateway (DHCP 10.0.0.100-254)", "role": "Router/Firewall/Gateway (DHCP 10.0.0.100-254)",
"ipAddress": "10.0.0.1" "ipAddress": "10.0.0.1",
"macAddress": "bc:24:11:5a:2e:01"
}, },
{ {
"name": "TPLink Omada Controller", "name": "TPLink Omada Controller",
@@ -50,6 +52,7 @@
{ {
"portName": "eth0", "portName": "eth0",
"speedGbps": 1, "speedGbps": 1,
"macAddress": "84:47:09:1c:aa:10",
"connectedTo": { "connectedTo": {
"device": "Gigabit Switch", "device": "Gigabit Switch",
"port": "1" "port": "1"