← 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 →