Overrides CONDUIT_URL env var. Resolved through load_client_config and threaded into create_conduit_client as base_url parameter. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
167 lines
4.8 KiB
JavaScript
Executable File
167 lines
4.8 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
import blessed from 'blessed';
|
|
import { load_client_config } from '../client/config.mjs';
|
|
import { create_conduit_client } from '../client/conduit.mjs';
|
|
|
|
const POLL_INTERVAL = 2000;
|
|
|
|
const { username, secret, url } = load_client_config(process.argv);
|
|
const client = create_conduit_client(username, secret, url);
|
|
|
|
const screen = blessed.screen({
|
|
smartCSR: true,
|
|
title: 'ccc-queue',
|
|
});
|
|
|
|
// ── Layout ────────────────────────────────────────────────────────────────────
|
|
|
|
const list_box = blessed.list({
|
|
top: 0,
|
|
left: 0,
|
|
width: '40%',
|
|
height: '100%-3',
|
|
border: { type: 'line' },
|
|
label: ' Pending Actions ',
|
|
scrollable: true,
|
|
keys: true,
|
|
vi: true,
|
|
mouse: true,
|
|
tags: true,
|
|
style: {
|
|
selected: { bg: 'blue', fg: 'white', bold: true },
|
|
border: { fg: 'cyan' },
|
|
label: { fg: 'cyan', bold: true },
|
|
},
|
|
});
|
|
|
|
const detail_box = blessed.box({
|
|
top: 0,
|
|
left: '40%',
|
|
width: '60%',
|
|
height: '100%-3',
|
|
border: { type: 'line' },
|
|
label: ' Details ',
|
|
scrollable: true,
|
|
alwaysScroll: true,
|
|
tags: true,
|
|
style: {
|
|
border: { fg: 'cyan' },
|
|
label: { fg: 'cyan', bold: true },
|
|
},
|
|
});
|
|
|
|
const status_bar = blessed.box({
|
|
bottom: 0,
|
|
left: 0,
|
|
width: '100%',
|
|
height: 3,
|
|
border: { type: 'line' },
|
|
tags: true,
|
|
style: { border: { fg: 'grey' } },
|
|
});
|
|
|
|
screen.append(list_box);
|
|
screen.append(detail_box);
|
|
screen.append(status_bar);
|
|
|
|
// ── State ─────────────────────────────────────────────────────────────────────
|
|
|
|
let items = [];
|
|
let status_text = '';
|
|
|
|
function set_status(text, is_error = false) {
|
|
status_text = text;
|
|
const color = is_error ? 'red' : 'green';
|
|
status_bar.setContent(
|
|
` {${color}-fg}${text}{/${color}-fg} ` +
|
|
`{grey-fg}[y]{/grey-fg} approve ` +
|
|
`{grey-fg}[n]{/grey-fg} deny ` +
|
|
`{grey-fg}[r]{/grey-fg} refresh ` +
|
|
`{grey-fg}[q]{/grey-fg} quit`
|
|
);
|
|
screen.render();
|
|
}
|
|
|
|
function render_detail(item) {
|
|
if (!item) {
|
|
detail_box.setContent('{grey-fg}No item selected{/grey-fg}');
|
|
return;
|
|
}
|
|
const lines = [
|
|
`{bold}Action:{/bold} ${item.action}`,
|
|
`{bold}ID:{/bold} ${item.id}`,
|
|
`{bold}Submitted by:{/bold} ${item.submitted_by}`,
|
|
`{bold}Created:{/bold} ${item.created_at}`,
|
|
'',
|
|
'{bold}Params:{/bold}',
|
|
];
|
|
for (const [k, v] of Object.entries(item.params ?? {})) {
|
|
lines.push(` {cyan-fg}${k}{/cyan-fg}: ${v}`);
|
|
}
|
|
detail_box.setContent(lines.join('\n'));
|
|
}
|
|
|
|
function render_list() {
|
|
const selected = list_box.selected ?? 0;
|
|
list_box.clearItems();
|
|
if (items.length === 0) {
|
|
list_box.addItem('{grey-fg}(no pending items){/grey-fg}');
|
|
} else {
|
|
for (const item of items) {
|
|
list_box.addItem(`{yellow-fg}${item.id.slice(0, 8)}{/yellow-fg} ${item.action}`);
|
|
}
|
|
list_box.select(Math.min(selected, items.length - 1));
|
|
}
|
|
render_detail(items[list_box.selected] ?? null);
|
|
screen.render();
|
|
}
|
|
|
|
// ── Polling ───────────────────────────────────────────────────────────────────
|
|
|
|
async function refresh() {
|
|
try {
|
|
items = await client.get_queue();
|
|
render_list();
|
|
set_status(`Last refresh: ${new Date().toLocaleTimeString()}`);
|
|
} catch (err) {
|
|
set_status(`Refresh failed: ${err.message}`, true);
|
|
}
|
|
}
|
|
|
|
// ── Actions ───────────────────────────────────────────────────────────────────
|
|
|
|
async function decide(decision) {
|
|
const item = items[list_box.selected];
|
|
if (!item) {
|
|
return;
|
|
}
|
|
set_status(`Sending ${decision} for ${item.id.slice(0, 8)}…`);
|
|
try {
|
|
await client.resolve_queue_item(item.id, decision);
|
|
await refresh();
|
|
} catch (err) {
|
|
set_status(`Failed: ${err.message}`, true);
|
|
}
|
|
}
|
|
|
|
// ── Keys ──────────────────────────────────────────────────────────────────────
|
|
|
|
screen.key(['q', 'C-c'], () => process.exit(0));
|
|
screen.key('r', refresh);
|
|
screen.key('y', () => decide('approve'));
|
|
screen.key('n', () => decide('deny'));
|
|
|
|
list_box.on('select item', () => {
|
|
render_detail(items[list_box.selected] ?? null);
|
|
screen.render();
|
|
});
|
|
|
|
list_box.focus();
|
|
|
|
// ── Boot ──────────────────────────────────────────────────────────────────────
|
|
|
|
set_status('Connecting…');
|
|
refresh();
|
|
const poll = setInterval(refresh, POLL_INTERVAL);
|
|
poll.unref();
|