#!/usr/bin/env node import blessed from 'blessed'; import { load_client_config } from '../client/config.mjs'; import { get_queue, resolve_queue_item } from '../client/index.mjs'; const POLL_INTERVAL = 2000; const { username, secret } = load_client_config(process.argv); 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, 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 get_queue(username, secret); 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 resolve_queue_item(item.id, decision, username, secret); 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();