diff --git a/app/static/app.js b/app/static/app.js
index c368079..df078a6 100644
--- a/app/static/app.js
+++ b/app/static/app.js
@@ -14,6 +14,8 @@ let devices = [];
let selectedDeviceId = null;
let selectedDevice = null;
let saveInFlight = false;
+let activeSectionOpen = true;
+let inactiveSectionOpen = false;
function setStatus(msg) {
statusText.textContent = msg;
@@ -105,6 +107,21 @@ function compareIpNumeric(a, b) {
return String(a.ip || '').localeCompare(String(b.ip || ''));
}
+function compareDevices(a, b) {
+ const aName = String(a.hostname || '').trim().toLocaleLowerCase();
+ const bName = String(b.hostname || '').trim().toLocaleLowerCase();
+
+ if (aName && bName) {
+ const byName = aName.localeCompare(bName);
+ if (byName !== 0) return byName;
+ return compareIpNumeric(a, b);
+ }
+
+ if (aName) return -1;
+ if (bName) return 1;
+ return compareIpNumeric(a, b);
+}
+
function renderDeviceList() {
deviceListEl.innerHTML = '';
@@ -113,20 +130,69 @@ function renderDeviceList() {
return;
}
- devices.forEach((d) => {
- const li = document.createElement('li');
- li.className = `device-item ${selectedDeviceId === d.id ? 'active' : ''}`;
- li.innerHTML = `
-
${deviceTitle(d)}
- ${d.os_name || 'OS unknown'} | ${d.is_active ? 'Active' : 'Missing'}
- `;
- li.addEventListener('click', () => {
- selectedDeviceId = d.id;
+ const activeDevices = devices.filter((device) => device.is_active);
+ const inactiveDevices = devices.filter((device) => !device.is_active);
+
+ deviceListEl.appendChild(createSection('Active Machines', activeDevices, activeSectionOpen, true));
+ deviceListEl.appendChild(createSection('Inactive Machines', inactiveDevices, inactiveSectionOpen, false));
+ attachDeviceListHandlers();
+}
+
+function createSection(title, sectionDevices, isOpen, isActiveSection) {
+ const sectionItem = document.createElement('li');
+ sectionItem.className = 'device-section-item';
+
+ const details = document.createElement('details');
+ details.className = 'device-section';
+ details.dataset.section = isActiveSection ? 'active' : 'inactive';
+ details.open = isOpen;
+
+ const itemsHtml = sectionDevices.length
+ ? sectionDevices.map((device) => `
+
+ ${escapeHtml(deviceTitle(device))}
+ ${escapeHtml(device.os_name || 'OS unknown')} | ${device.is_active ? 'Active' : 'Missing'}
+
+ `).join('')
+ : 'No machines in this section.';
+
+ details.innerHTML = `
+
+ ${escapeHtml(title)}
+ ${sectionDevices.length}
+
+
+ `;
+
+ sectionItem.appendChild(details);
+ return sectionItem;
+}
+
+function attachDeviceListHandlers() {
+ deviceListEl.querySelectorAll('.device-item[data-device-id]').forEach((item) => {
+ item.addEventListener('click', () => {
+ const deviceId = Number(item.dataset.deviceId);
+ if (!deviceId) return;
+ selectedDeviceId = deviceId;
renderDeviceList();
- loadDevice(d.id);
+ loadDevice(deviceId);
});
- deviceListEl.appendChild(li);
});
+
+ const activeSection = deviceListEl.querySelector('.device-section[data-section="active"]');
+ const inactiveSection = deviceListEl.querySelector('.device-section[data-section="inactive"]');
+
+ if (activeSection) {
+ activeSection.addEventListener('toggle', () => {
+ activeSectionOpen = activeSection.open;
+ });
+ }
+
+ if (inactiveSection) {
+ inactiveSection.addEventListener('toggle', () => {
+ inactiveSectionOpen = inactiveSection.open;
+ });
+ }
}
function renderMachineInfo(d) {
@@ -201,10 +267,7 @@ Headers:\n${headers}
async function loadDevices() {
devices = await api('/api/devices');
- devices.sort((a, b) => {
- if (a.is_active !== b.is_active) return b.is_active - a.is_active;
- return compareIpNumeric(a, b);
- });
+ devices.sort(compareDevices);
renderDeviceList();
if (!selectedDeviceId && devices.length) {
@@ -277,10 +340,7 @@ function resetDeviceEdits() {
function updateDeviceInList(updatedDevice) {
devices = devices.map((device) => (device.id === updatedDevice.id ? { ...device, ...updatedDevice } : device));
- devices.sort((a, b) => {
- if (a.is_active !== b.is_active) return b.is_active - a.is_active;
- return compareIpNumeric(a, b);
- });
+ devices.sort(compareDevices);
renderDeviceList();
}
diff --git a/app/static/styles.css b/app/static/styles.css
index 1980e3c..5956198 100644
--- a/app/static/styles.css
+++ b/app/static/styles.css
@@ -164,6 +164,51 @@ button:disabled {
overflow: auto;
}
+.device-section-item {
+ list-style: none;
+ margin-bottom: 8px;
+}
+
+.device-section {
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ background: rgba(255,255,255,0.015);
+ overflow: hidden;
+}
+
+.device-section-summary {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 11px 12px;
+ cursor: pointer;
+ font-weight: 600;
+ list-style: none;
+ user-select: none;
+}
+
+.device-section-summary::-webkit-details-marker {
+ display: none;
+}
+
+.device-section-count {
+ min-width: 28px;
+ padding: 2px 8px;
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ background: rgba(255,255,255,0.03);
+ color: var(--muted);
+ font-size: 0.78rem;
+ text-align: center;
+}
+
+.device-section-list {
+ list-style: none;
+ margin: 0;
+ padding: 0 6px 6px;
+ border-top: 1px solid rgba(255,255,255,0.04);
+}
+
.device-item {
border: 1px solid transparent;
border-radius: 10px;
@@ -173,6 +218,16 @@ button:disabled {
cursor: pointer;
}
+.device-section-list .device-item:first-child {
+ margin-top: 6px;
+}
+
+.device-empty {
+ padding: 12px 10px;
+ color: var(--muted);
+ font-size: 0.84rem;
+}
+
.device-item:hover { border-color: #33516b; }
.device-item.active { border-color: var(--accent); background: rgba(93,196,255,0.08); }
diff --git a/codex-session.bat b/codex-session.bat
new file mode 100644
index 0000000..631ba06
--- /dev/null
+++ b/codex-session.bat
@@ -0,0 +1 @@
+codex resume 019ccf04-af34-7883-a705-2802dd142306