[object Object]

← back to Timeout Agent

add sort + density controls to process grid

60d0d0861b3cc83e769d8f10bf53c41a85529843 · 2026-05-19 21:26:47 -0700 · steve

Process Status card now has a sort <select> (Status / Name A→Z / Name Z→A /
Restarts ↓↑ / Memory ↓↑) and a density <input type=range> for row padding.
Both persist to localStorage (timeout-agent.proc-sort, .proc-density) and
hydrate before the first refresh() so the initial /api/processes call uses
the saved sort. Server-side /api/processes now honors ?sort=<mode> and
defaults to status-then-name (errored → stopped → online).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 60d0d0861b3cc83e769d8f10bf53c41a85529843
Author: steve <steve@designerwallcoverings.com>
Date:   Tue May 19 21:26:47 2026 -0700

    add sort + density controls to process grid
    
    Process Status card now has a sort <select> (Status / Name A→Z / Name Z→A /
    Restarts ↓↑ / Memory ↓↑) and a density <input type=range> for row padding.
    Both persist to localStorage (timeout-agent.proc-sort, .proc-density) and
    hydrate before the first refresh() so the initial /api/processes call uses
    the saved sort. Server-side /api/processes now honors ?sort=<mode> and
    defaults to status-then-name (errored → stopped → online).
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 server.js | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 91 insertions(+), 10 deletions(-)

diff --git a/server.js b/server.js
index 77c5a96..588b73c 100644
--- a/server.js
+++ b/server.js
@@ -829,14 +829,35 @@ app.get('/api/status', (req, res) => {
 app.get('/api/processes', (req, res) => {
   const list = Object.entries(watchState.processStatus).map(([name, ps]) => ({
     name, ...ps
-  })).sort((a, b) => {
-    // Sort: errored first, then by name
-    if (a.status === 'errored' && b.status !== 'errored') return -1;
-    if (b.status === 'errored' && a.status !== 'errored') return 1;
-    if (a.status === 'stopped' && b.status !== 'stopped') return -1;
-    if (b.status === 'stopped' && a.status !== 'stopped') return 1;
-    return a.name.localeCompare(b.name);
-  });
+  }));
+  const sort = String(req.query.sort || 'status').toLowerCase();
+  const statusRank = (s) => s === 'errored' ? 0 : s === 'stopped' ? 1 : s === 'online' ? 2 : 3;
+  const byName = (a, b) => a.name.localeCompare(b.name);
+  switch (sort) {
+    case 'name':
+    case 'name-asc':
+      list.sort(byName);
+      break;
+    case 'name-desc':
+      list.sort((a, b) => b.name.localeCompare(a.name));
+      break;
+    case 'restarts-desc':
+      list.sort((a, b) => (b.restarts || 0) - (a.restarts || 0) || byName(a, b));
+      break;
+    case 'restarts-asc':
+      list.sort((a, b) => (a.restarts || 0) - (b.restarts || 0) || byName(a, b));
+      break;
+    case 'mem-desc':
+      list.sort((a, b) => (b.memMB || 0) - (a.memMB || 0) || byName(a, b));
+      break;
+    case 'mem-asc':
+      list.sort((a, b) => (a.memMB || 0) - (b.memMB || 0) || byName(a, b));
+      break;
+    case 'status':
+    default:
+      // Errored first, then stopped, then online; tiebreak by name
+      list.sort((a, b) => statusRank(a.status) - statusRank(b.status) || byName(a, b));
+  }
   res.json({ count: list.length, processes: list });
 });
 
@@ -953,6 +974,11 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
 .toggle-btn.off{background:#475569}
 .live-log{background:#0f172a;border:1px solid #334155;border-radius:8px;padding:10px;max-height:200px;overflow-y:auto;font-family:monospace;font-size:11px;color:#94a3b8;margin-top:15px}
 .live-line{padding:2px 0}
+.grid-controls{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
+.grid-controls label{font-size:11px;color:#94a3b8;text-transform:uppercase;letter-spacing:1px}
+.grid-controls select{background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:6px;padding:5px 8px;font-size:12px;cursor:pointer}
+.grid-controls input[type=range]{width:90px;accent-color:${AGENT_COLOR}}
+.grid-controls .density-val{font-size:11px;color:#64748b;font-family:monospace;min-width:24px;text-align:right}
 </style></head><body>
 <div class="header">
   <img class="agent-avatar" src="http://45.61.58.125/agent-avatars/the-warden.png" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'"><div class="agent-avatar-fallback">W</div>
@@ -973,7 +999,24 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
 </div>
 <div class="main">
   <div class="card">
-    <div class="card-header">Process Status <span id="procCount" style="color:#64748b;font-weight:400"></span></div>
+    <div class="card-header">
+      <span>Process Status <span id="procCount" style="color:#64748b;font-weight:400"></span></span>
+      <div class="grid-controls">
+        <label for="sortSel">Sort</label>
+        <select id="sortSel" onchange="onSortChange()">
+          <option value="status">Status (errored first)</option>
+          <option value="name">Name A→Z</option>
+          <option value="name-desc">Name Z→A</option>
+          <option value="restarts-desc">Restarts ↓</option>
+          <option value="restarts-asc">Restarts ↑</option>
+          <option value="mem-desc">Memory ↓</option>
+          <option value="mem-asc">Memory ↑</option>
+        </select>
+        <label for="densSel">Density</label>
+        <input id="densSel" type="range" min="2" max="14" step="1" value="8" oninput="onDensityChange(this.value)">
+        <span class="density-val" id="densVal">8</span>
+      </div>
+    </div>
     <div class="card-body" id="procList">Loading...</div>
   </div>
   <div class="card">
@@ -991,11 +1034,48 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
 const AUTH='Basic '+btoa('admin:DWSecure2024!');
 const H={headers:{Authorization:AUTH}};
 
+// ── Grid controls: sort + density, persisted to localStorage ──
+const LS_SORT='timeout-agent.proc-sort';
+const LS_DENS='timeout-agent.proc-density';
+let currentSort=localStorage.getItem(LS_SORT)||'status';
+let currentDensity=parseInt(localStorage.getItem(LS_DENS),10);
+if(!Number.isFinite(currentDensity)||currentDensity<2||currentDensity>14)currentDensity=8;
+// Hydrate controls BEFORE first refresh() so the initial API call uses the saved sort
+(function hydrateGridControls(){
+  const ss=document.getElementById('sortSel');
+  if(ss)ss.value=currentSort;
+  const ds=document.getElementById('densSel');
+  if(ds)ds.value=String(currentDensity);
+  const dv=document.getElementById('densVal');
+  if(dv)dv.textContent=String(currentDensity);
+})();
+function onSortChange(){
+  const ss=document.getElementById('sortSel');
+  currentSort=ss?ss.value:'status';
+  try{localStorage.setItem(LS_SORT,currentSort);}catch(e){}
+  refresh();
+}
+function onDensityChange(v){
+  const n=parseInt(v,10);
+  if(!Number.isFinite(n))return;
+  currentDensity=n;
+  try{localStorage.setItem(LS_DENS,String(n));}catch(e){}
+  const dv=document.getElementById('densVal');if(dv)dv.textContent=String(n);
+  applyDensity();
+}
+function applyDensity(){
+  // Density slider drives vertical padding on each proc row (2 = tightest, 14 = loosest)
+  document.querySelectorAll('#procList .proc-row').forEach(el=>{
+    el.style.paddingTop=currentDensity+'px';
+    el.style.paddingBottom=currentDensity+'px';
+  });
+}
+
 async function refresh(){
   try{
     const[st,pr,ac,stats]=await Promise.all([
       fetch('/api/status',H).then(r=>r.json()),
-      fetch('/api/processes',H).then(r=>r.json()),
+      fetch('/api/processes?sort='+encodeURIComponent(currentSort),H).then(r=>r.json()),
       fetch('/api/actions?limit=30',H).then(r=>r.json()),
       fetch('/api/stats',H).then(r=>r.json())
     ]);
@@ -1027,6 +1107,7 @@ async function refresh(){
         '</div>';
     }).join('');
     document.getElementById('procCount').textContent='('+pr.count+')';
+    applyDensity();
 
     // Actions
     const al=document.getElementById('actionList');

← 25967f9 broaden .gitignore to exclude snapshot/swap files  ·  back to Timeout Agent  ·  security: strip hardcoded dw_admin DSN password -> env-first 2f254cd →