Some checks are pending
build-and-push / image (push) Waiting to run
- All 14 panes rewritten with rich controls: KPIs, sparklines, meter gauges, presets, history sidebars, search/filter, bulk actions, schema-driven forms, copy buttons, auto-refresh. - Backend /api/system enriched with cpu_count, rss_bytes, mem_total, mem_used_pct, loadavg[], user/system CPU times, max_rss_kb. - CSS additions: .meter/.gauge, .seg, .chip(s), .spark, .slider, .dl, .link-card, details.card-d plus utility classes. - ui.js helpers: copyToClipboard, withCopy/bindCopyButtons, sparkline, meterRow, fmtBytes/fmtNum, historyStore, inputFromSchema, collectSchemaForm. - Files pane added; iCloud duplicate '*2' files removed.
132 lines
6.2 KiB
JavaScript
132 lines
6.2 KiB
JavaScript
// panes/slack.js — Slack sidecar with respond form, presets, tools.
|
|
import { registerPane } from '../app.js';
|
|
import { jget, jpost, escapeHtml } from '../lib/api.js';
|
|
import { fmtJSON, err, ok, copyToClipboard } from '../lib/ui.js';
|
|
|
|
const TPL = `
|
|
<div class="pane-head">
|
|
<div><div class="title">Slack</div><div class="sub">Sidecar health, conversation memory, ad-hoc responses.</div></div>
|
|
<div class="grow"></div>
|
|
<span class="pill" id="slack-pill"><span class="dot"></span><span id="slack-pill-text">—</span></span>
|
|
<button class="btn btn-secondary" id="slack-refresh">Refresh</button>
|
|
</div>
|
|
|
|
<div class="grid cols-2 mb-3">
|
|
<div class="card">
|
|
<div class="card-title"><h2>Respond</h2><span class="muted xsmall" id="slack-resp-meta">—</span></div>
|
|
<form id="slack-form" class="grid gap-3">
|
|
<label class="field">
|
|
<span class="lbl">Message</span>
|
|
<textarea class="input" id="slack-text" rows="3" required placeholder="Hello team!"></textarea>
|
|
</label>
|
|
<div class="grid cols-2 gap-3">
|
|
<label class="field"><span class="lbl">Channel</span><input class="input" id="slack-channel" placeholder="rest" value="rest"/></label>
|
|
<label class="field"><span class="lbl">Thread ts</span><input class="input" id="slack-thread" placeholder="rest" value="rest"/></label>
|
|
</div>
|
|
<div class="btn-row">
|
|
<button class="btn btn-primary" type="submit">Generate reply</button>
|
|
<button type="button" class="btn btn-secondary" data-preset="standup">Standup</button>
|
|
<button type="button" class="btn btn-secondary" data-preset="bug">Bug report</button>
|
|
<button type="button" class="btn btn-secondary" data-preset="ship">Ship update</button>
|
|
</div>
|
|
</form>
|
|
<div class="divider"></div>
|
|
<div class="card-title"><h2 style="font-size:14px">Reply</h2><button class="btn btn-ghost" id="slack-copy">Copy</button></div>
|
|
<pre id="slack-reply" class="code muted" style="max-height:24vh">—</pre>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title"><h2>Tools</h2><span class="muted xsmall" id="slack-tools-meta">—</span></div>
|
|
<div id="slack-tools-list" class="scroll" style="max-height:22vh">…</div>
|
|
|
|
<div class="divider"></div>
|
|
<div class="card-title"><h2 style="font-size:14px">Conversation memory</h2>
|
|
<button class="btn btn-ghost" id="slack-mem-copy">Copy</button>
|
|
</div>
|
|
<pre id="slack-memory" class="code muted" style="max-height:28vh">…</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid cols-2">
|
|
<div class="card">
|
|
<div class="card-title"><h2>healthz</h2></div>
|
|
<pre id="slack-health" class="code" style="max-height:24vh">…</pre>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title"><h2>info</h2></div>
|
|
<pre id="slack-info" class="code" style="max-height:24vh">…</pre>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const PRESETS = {
|
|
standup: 'Daily standup: ✅ Yesterday — ___ · 🎯 Today — ___ · 🚧 Blockers — none',
|
|
bug: 'Bug report: **Title** — short summary\n\n**Steps to reproduce:**\n1. ___\n2. ___\n\n**Expected:** ___\n**Actual:** ___\n**Env:** ___',
|
|
ship: '🚀 Shipped: **<feature>** — short summary. Try it at <link>. Feedback welcome 🙏',
|
|
};
|
|
|
|
async function refresh(host) {
|
|
try {
|
|
const [h, info, mem, tools] = await Promise.all([
|
|
jget('/api/slack/healthz').catch(e => ({ error: e.message })),
|
|
jget('/api/slack/info').catch(e => ({ error: e.message })),
|
|
jget('/api/slack/memory').catch(e => ({ error: e.message })),
|
|
jget('/api/slack/tools').catch(e => ({ error: e.message })),
|
|
]);
|
|
host.querySelector('#slack-health').textContent = fmtJSON(h);
|
|
host.querySelector('#slack-info').textContent = fmtJSON(info);
|
|
host.querySelector('#slack-memory').textContent = fmtJSON(mem);
|
|
const items = tools.tools || [];
|
|
host.querySelector('#slack-tools-meta').textContent = `${items.length} tools`;
|
|
host.querySelector('#slack-tools-list').innerHTML = items.length
|
|
? items.map(t => `<div class="row">
|
|
<div class="grow"><div class="ttl mono">${escapeHtml(t.name)}</div><div class="desc">${escapeHtml(t.description || '')}</div></div>
|
|
</div>`).join('')
|
|
: '<div class="muted xsmall p-2">(none)</div>';
|
|
|
|
const ok2 = !h?.error;
|
|
const pill = host.querySelector('#slack-pill');
|
|
pill.classList.remove('ok', 'err'); pill.classList.add(ok2 ? 'ok' : 'err');
|
|
host.querySelector('#slack-pill-text').textContent = ok2 ? 'up' : 'down';
|
|
} catch (e) {
|
|
host.querySelector('#slack-health').textContent = e.message;
|
|
}
|
|
}
|
|
|
|
registerPane('slack', {
|
|
label: 'Slack',
|
|
init(host) {
|
|
host.innerHTML = TPL;
|
|
host.querySelector('#slack-refresh').addEventListener('click', () => refresh(host));
|
|
host.querySelectorAll('[data-preset]').forEach(b => b.addEventListener('click', () => {
|
|
host.querySelector('#slack-text').value = PRESETS[b.dataset.preset] || '';
|
|
}));
|
|
host.querySelector('#slack-copy').addEventListener('click', () => copyToClipboard(host.querySelector('#slack-reply').textContent));
|
|
host.querySelector('#slack-mem-copy').addEventListener('click', () => copyToClipboard(host.querySelector('#slack-memory').textContent));
|
|
|
|
host.querySelector('#slack-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const body = {
|
|
text: host.querySelector('#slack-text').value,
|
|
channel: host.querySelector('#slack-channel').value || 'rest',
|
|
thread_ts: host.querySelector('#slack-thread').value || 'rest',
|
|
};
|
|
host.querySelector('#slack-reply').textContent = 'thinking…';
|
|
host.querySelector('#slack-resp-meta').textContent = '…';
|
|
const t0 = performance.now();
|
|
try {
|
|
const r = await jpost('/api/slack/respond', body);
|
|
host.querySelector('#slack-reply').textContent = r.reply || fmtJSON(r);
|
|
host.querySelector('#slack-resp-meta').textContent = `${(performance.now() - t0).toFixed(0)}ms`;
|
|
ok('replied');
|
|
} catch (e) {
|
|
host.querySelector('#slack-reply').textContent = e.body?.detail || e.message;
|
|
host.querySelector('#slack-resp-meta').textContent = `error ${e.status ?? ''}`;
|
|
err(e.message);
|
|
}
|
|
});
|
|
refresh(host);
|
|
},
|
|
refresh,
|
|
});
|