quoxcli 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +35 -0
- package/README.md +142 -2
- package/bin/quox.js +292 -0
- package/bin/quoxon.js +8 -0
- package/lib/api/endpoints/admin.js +125 -0
- package/lib/api/endpoints/aee.js +38 -0
- package/lib/api/endpoints/agent-attach.js +126 -0
- package/lib/api/endpoints/agents.js +56 -0
- package/lib/api/endpoints/assistants.js +50 -0
- package/lib/api/endpoints/audit.js +14 -0
- package/lib/api/endpoints/auth.js +50 -0
- package/lib/api/endpoints/awareness.js +34 -0
- package/lib/api/endpoints/backup-schedules.js +20 -0
- package/lib/api/endpoints/backups.js +86 -0
- package/lib/api/endpoints/brain.js +140 -0
- package/lib/api/endpoints/budgets.js +62 -0
- package/lib/api/endpoints/calendarq.js +71 -0
- package/lib/api/endpoints/catalog.js +14 -0
- package/lib/api/endpoints/chat.js +54 -0
- package/lib/api/endpoints/consent.js +26 -0
- package/lib/api/endpoints/contactsq.js +66 -0
- package/lib/api/endpoints/conversations.js +32 -0
- package/lib/api/endpoints/custom-roles.js +92 -0
- package/lib/api/endpoints/divisions.js +56 -0
- package/lib/api/endpoints/domains.js +38 -0
- package/lib/api/endpoints/engagement.js +231 -0
- package/lib/api/endpoints/engine.js +11 -0
- package/lib/api/endpoints/entity.js +50 -0
- package/lib/api/endpoints/executions.js +32 -0
- package/lib/api/endpoints/file.js +20 -0
- package/lib/api/endpoints/files.js +26 -0
- package/lib/api/endpoints/github.js +21 -0
- package/lib/api/endpoints/governance.js +43 -0
- package/lib/api/endpoints/hat.js +36 -0
- package/lib/api/endpoints/inbox.js +32 -0
- package/lib/api/endpoints/inboxq.js +120 -0
- package/lib/api/endpoints/index.js +228 -0
- package/lib/api/endpoints/invites.js +19 -0
- package/lib/api/endpoints/llm-policy.js +43 -0
- package/lib/api/endpoints/lucid.js +89 -0
- package/lib/api/endpoints/members.js +32 -0
- package/lib/api/endpoints/memory.js +75 -0
- package/lib/api/endpoints/mfa.js +32 -0
- package/lib/api/endpoints/mon.js +178 -0
- package/lib/api/endpoints/notesq.js +43 -0
- package/lib/api/endpoints/objectives.js +62 -0
- package/lib/api/endpoints/orgs.js +38 -0
- package/lib/api/endpoints/plugins.js +134 -0
- package/lib/api/endpoints/powers.js +75 -0
- package/lib/api/endpoints/prompts.js +69 -0
- package/lib/api/endpoints/qlarity.js +122 -0
- package/lib/api/endpoints/quoxbox.js +55 -0
- package/lib/api/endpoints/quoxchat.js +54 -0
- package/lib/api/endpoints/quoxflow.js +32 -0
- package/lib/api/endpoints/quoxplan.js +114 -0
- package/lib/api/endpoints/quoxseo.js +108 -0
- package/lib/api/endpoints/reach.js +47 -0
- package/lib/api/endpoints/runs.js +38 -0
- package/lib/api/endpoints/safety.js +46 -0
- package/lib/api/endpoints/scim.js +46 -0
- package/lib/api/endpoints/scopes.js +14 -0
- package/lib/api/endpoints/security.js +69 -0
- package/lib/api/endpoints/setup.js +90 -0
- package/lib/api/endpoints/skills.js +180 -0
- package/lib/api/endpoints/sso.js +67 -0
- package/lib/api/endpoints/tags.js +32 -0
- package/lib/api/endpoints/teams.js +56 -0
- package/lib/api/endpoints/telephony.js +46 -0
- package/lib/api/endpoints/tg.js +56 -0
- package/lib/api/endpoints/tools.js +50 -0
- package/lib/api/endpoints/traces.js +157 -0
- package/lib/api/endpoints/triggers.js +70 -0
- package/lib/api/endpoints/user-library.js +41 -0
- package/lib/api/endpoints/users.js +20 -0
- package/lib/api/endpoints/v1.js +499 -0
- package/lib/api/endpoints/vault.js +192 -0
- package/lib/api/endpoints/voice.js +22 -0
- package/lib/api/endpoints/volt.js +84 -0
- package/lib/api/endpoints/ward.js +40 -0
- package/lib/api/endpoints/webhooks.js +46 -0
- package/lib/api/endpoints/workflows.js +162 -0
- package/lib/api/endpoints/x.js +24 -0
- package/lib/api/endpoints.js +15 -0
- package/lib/commands/admin.js +359 -0
- package/lib/commands/aee.js +99 -0
- package/lib/commands/agent.js +201 -0
- package/lib/commands/agentAttach.js +1440 -0
- package/lib/commands/approvals.js +47 -0
- package/lib/commands/assistant.js +114 -0
- package/lib/commands/auth.js +458 -0
- package/lib/commands/awareness.js +88 -0
- package/lib/commands/backup.js +373 -0
- package/lib/commands/brain.js +994 -0
- package/lib/commands/budget.js +241 -0
- package/lib/commands/build.js +156 -0
- package/lib/commands/calendar.js +216 -0
- package/lib/commands/capabilities.js +34 -0
- package/lib/commands/chat.js +240 -0
- package/lib/commands/claude.js +354 -0
- package/lib/commands/config-cmd.js +106 -0
- package/lib/commands/consent.js +84 -0
- package/lib/commands/contacts.js +967 -0
- package/lib/commands/contactsSelftest.js +132 -0
- package/lib/commands/conversation.js +74 -0
- package/lib/commands/department.js +75 -0
- package/lib/commands/dev.js +620 -0
- package/lib/commands/devResident.js +314 -0
- package/lib/commands/devSupervisor.js +1050 -0
- package/lib/commands/discord.js +96 -0
- package/lib/commands/doctor.js +181 -0
- package/lib/commands/domain.js +114 -0
- package/lib/commands/done.js +277 -0
- package/lib/commands/engagement.js +586 -0
- package/lib/commands/entity.js +119 -0
- package/lib/commands/facebook.js +212 -0
- package/lib/commands/file.js +128 -0
- package/lib/commands/fleet.js +882 -0
- package/lib/commands/governance.js +638 -0
- package/lib/commands/gripe.js +324 -0
- package/lib/commands/hat.js +205 -0
- package/lib/commands/helpers.js +79 -0
- package/lib/commands/inbox.js +122 -0
- package/lib/commands/instagram.js +264 -0
- package/lib/commands/integration.js +100 -0
- package/lib/commands/inventory.js +115 -0
- package/lib/commands/invites.js +45 -0
- package/lib/commands/licence.js +280 -0
- package/lib/commands/linkedin.js +178 -0
- package/lib/commands/llm-policy.js +117 -0
- package/lib/commands/llm.js +45 -0
- package/lib/commands/lucid.js +591 -0
- package/lib/commands/mail.js +531 -0
- package/lib/commands/memory.js +284 -0
- package/lib/commands/mon.js +514 -0
- package/lib/commands/monitoring.js +86 -0
- package/lib/commands/motion.js +22 -0
- package/lib/commands/notes.js +1246 -0
- package/lib/commands/notesBatch.js +160 -0
- package/lib/commands/notesBridges.js +56 -0
- package/lib/commands/notesLinks.js +60 -0
- package/lib/commands/notification.js +57 -0
- package/lib/commands/nucleus.js +92 -0
- package/lib/commands/objective.js +229 -0
- package/lib/commands/onboard.js +328 -0
- package/lib/commands/open.js +56 -0
- package/lib/commands/org.js +266 -0
- package/lib/commands/plan.js +906 -0
- package/lib/commands/plugin-doctor.js +1091 -0
- package/lib/commands/plugin.js +539 -0
- package/lib/commands/policy.js +78 -0
- package/lib/commands/powers.js +296 -0
- package/lib/commands/prompt.js +350 -0
- package/lib/commands/qlarity.js +690 -0
- package/lib/commands/quoxbox.js +270 -0
- package/lib/commands/quoxchat.js +266 -0
- package/lib/commands/quoxseo.js +340 -0
- package/lib/commands/reach.js +158 -0
- package/lib/commands/role.js +286 -0
- package/lib/commands/run.js +132 -0
- package/lib/commands/safety.js +107 -0
- package/lib/commands/scim.js +88 -0
- package/lib/commands/scope.js +58 -0
- package/lib/commands/security.js +157 -0
- package/lib/commands/service.js +89 -0
- package/lib/commands/settings.js +99 -0
- package/lib/commands/setup.js +543 -0
- package/lib/commands/skillManifest.js +113 -0
- package/lib/commands/skillRightsFormat.js +38 -0
- package/lib/commands/skills.js +644 -0
- package/lib/commands/slot.js +1198 -0
- package/lib/commands/sso.js +229 -0
- package/lib/commands/status.js +137 -0
- package/lib/commands/tag.js +91 -0
- package/lib/commands/team.js +140 -0
- package/lib/commands/tg.js +240 -0
- package/lib/commands/tiktok.js +218 -0
- package/lib/commands/tool.js +216 -0
- package/lib/commands/trace.js +525 -0
- package/lib/commands/trigger.js +128 -0
- package/lib/commands/vault.js +1424 -0
- package/lib/commands/voice.js +182 -0
- package/lib/commands/volt.js +528 -0
- package/lib/commands/watch.js +115 -0
- package/lib/commands/webhook.js +137 -0
- package/lib/commands/workflow.js +503 -0
- package/lib/commands/x.js +439 -0
- package/lib/core/actingAs.js +136 -0
- package/lib/core/aee.js +53 -0
- package/lib/core/agentAlias.js +163 -0
- package/lib/core/approvalContext.js +61 -0
- package/lib/core/auth.js +350 -0
- package/lib/core/banner.js +143 -0
- package/lib/core/brainPrime.js +90 -0
- package/lib/core/callerSource.js +129 -0
- package/lib/core/claudeLs.js +94 -0
- package/lib/core/client.js +584 -0
- package/lib/core/completions.js +163 -0
- package/lib/core/config.js +551 -0
- package/lib/core/confirm.js +37 -0
- package/lib/core/deeplinks.js +76 -0
- package/lib/core/doneEngineFetch.js +219 -0
- package/lib/core/errors.js +132 -0
- package/lib/core/global-flags.js +90 -0
- package/lib/core/governedSession.js +402 -0
- package/lib/core/lazyCommands.js +119 -0
- package/lib/core/liveSessionsReport.js +112 -0
- package/lib/core/org-override.js +52 -0
- package/lib/core/output.js +239 -0
- package/lib/core/prodEnv.js +7 -0
- package/lib/core/quoxglyph.js +158 -0
- package/lib/core/residentAlarm.js +282 -0
- package/lib/core/residentAuth.js +243 -0
- package/lib/core/residentPump.js +229 -0
- package/lib/core/residentSeed.js +104 -0
- package/lib/core/sessionHeartbeat.js +192 -0
- package/lib/core/sessionsOverview.js +160 -0
- package/lib/core/slotKeeper.js +282 -0
- package/lib/core/supervisorEngine.js +864 -0
- package/lib/core/systemdInstaller.js +256 -0
- package/lib/core/version.js +87 -0
- package/lib/core/websiteSession.js +148 -0
- package/lib/lucid/LucidIntro.jsx +105 -0
- package/lib/lucid/blind.js +71 -0
- package/lib/lucid/deck.js +32 -0
- package/lib/lucid/fmt.js +139 -0
- package/lib/lucid/sitrep.js +173 -0
- package/lib/plugins/loader.js +93 -0
- package/lib/quoxon/activity.js +104 -0
- package/lib/quoxon/api.js +473 -0
- package/lib/quoxon/app.js +1262 -0
- package/lib/quoxon/capabilities.js +75 -0
- package/lib/quoxon/commands.js +154 -0
- package/lib/quoxon/connect.js +467 -0
- package/lib/quoxon/connectCard.js +28 -0
- package/lib/quoxon/context.js +68 -0
- package/lib/quoxon/copy.js +33 -0
- package/lib/quoxon/demo.fixtures.js +24 -0
- package/lib/quoxon/demo.js +35 -0
- package/lib/quoxon/egg.js +36 -0
- package/lib/quoxon/events.js +458 -0
- package/lib/quoxon/gallery.js +223 -0
- package/lib/quoxon/indicator.js +193 -0
- package/lib/quoxon/intent.js +41 -0
- package/lib/quoxon/keys.js +121 -0
- package/lib/quoxon/main.js +244 -0
- package/lib/quoxon/markdown.js +317 -0
- package/lib/quoxon/model.js +205 -0
- package/lib/quoxon/motion/clock.js +69 -0
- package/lib/quoxon/motion/color.js +35 -0
- package/lib/quoxon/motion/frame.js +14 -0
- package/lib/quoxon/motion/reveal.js +54 -0
- package/lib/quoxon/motion/ring.js +79 -0
- package/lib/quoxon/objects/agent.js +61 -0
- package/lib/quoxon/objects/crystal.js +100 -0
- package/lib/quoxon/objects/fetch.js +170 -0
- package/lib/quoxon/objects/index.js +85 -0
- package/lib/quoxon/objects/ladder.js +71 -0
- package/lib/quoxon/objects/memory.js +44 -0
- package/lib/quoxon/objects/model.js +118 -0
- package/lib/quoxon/objects/proof.js +110 -0
- package/lib/quoxon/objects/skill.js +105 -0
- package/lib/quoxon/objects/view.js +76 -0
- package/lib/quoxon/objects/workflow.js +119 -0
- package/lib/quoxon/observers.js +42 -0
- package/lib/quoxon/onboarding/copy.js +235 -0
- package/lib/quoxon/onboarding/credential.js +95 -0
- package/lib/quoxon/onboarding/detect.js +22 -0
- package/lib/quoxon/onboarding/finish.js +119 -0
- package/lib/quoxon/onboarding/index.js +67 -0
- package/lib/quoxon/onboarding/installRunner.js +225 -0
- package/lib/quoxon/onboarding/intent.js +53 -0
- package/lib/quoxon/onboarding/journey.js +73 -0
- package/lib/quoxon/onboarding/preflight.js +142 -0
- package/lib/quoxon/onboarding/runner.js +58 -0
- package/lib/quoxon/onboarding/view.js +625 -0
- package/lib/quoxon/print.js +94 -0
- package/lib/quoxon/qart.js +379 -0
- package/lib/quoxon/qglyph.js +143 -0
- package/lib/quoxon/qhello.js +58 -0
- package/lib/quoxon/qmotion.js +111 -0
- package/lib/quoxon/render.js +648 -0
- package/lib/quoxon/roster.js +221 -0
- package/lib/quoxon/route.js +38 -0
- package/lib/quoxon/rules.js +130 -0
- package/lib/quoxon/session.js +68 -0
- package/lib/quoxon/skills.js +30 -0
- package/lib/quoxon/slash/compact.js +90 -0
- package/lib/quoxon/slash/ledger.js +83 -0
- package/lib/quoxon/slash/listView.js +28 -0
- package/lib/quoxon/slash/palette.js +104 -0
- package/lib/quoxon/slash/paletteController.js +59 -0
- package/lib/quoxon/slash/panels.js +101 -0
- package/lib/quoxon/slash/picker.js +38 -0
- package/lib/quoxon/slash/restart.js +144 -0
- package/lib/quoxon/slash/resume.js +59 -0
- package/lib/quoxon/slash/settings.js +50 -0
- package/lib/quoxon/sound/index.js +186 -0
- package/lib/quoxon/sound/mapping.js +119 -0
- package/lib/quoxon/sound/output.js +52 -0
- package/lib/quoxon/sound/synth.js +122 -0
- package/lib/quoxon/term.js +106 -0
- package/lib/quoxon/textBuffer.js +242 -0
- package/lib/quoxon/theme.js +253 -0
- package/lib/quoxon/transcript.js +150 -0
- package/lib/quoxon/web/qface.js +81 -0
- package/lib/quoxon/welcome.js +52 -0
- package/lib/tui/app.jsx +289 -0
- package/lib/tui/components/Breadcrumb.jsx +42 -0
- package/lib/tui/components/ChatInput.jsx +68 -0
- package/lib/tui/components/Confirm.jsx +32 -0
- package/lib/tui/components/DataTable.jsx +270 -0
- package/lib/tui/components/DetailPanel.jsx +241 -0
- package/lib/tui/components/HelpOverlay.jsx +67 -0
- package/lib/tui/components/Layout.jsx +129 -0
- package/lib/tui/components/Palette.jsx +141 -0
- package/lib/tui/components/SearchInput.jsx +125 -0
- package/lib/tui/components/Sparkline.jsx +55 -0
- package/lib/tui/components/StatusBar.jsx +133 -0
- package/lib/tui/components/Toast.jsx +52 -0
- package/lib/tui/hooks/useApi.js +44 -0
- package/lib/tui/hooks/useHealth.js +90 -0
- package/lib/tui/hooks/usePolledApi.js +86 -0
- package/lib/tui/hooks/useTerminalSize.js +46 -0
- package/lib/tui/index.js +56 -0
- package/lib/tui/intro/Intro.jsx +332 -0
- package/lib/tui/jsx-loader.js +32 -0
- package/lib/tui/keys.js +87 -0
- package/lib/tui/registry.js +49 -0
- package/lib/tui/theme.js +103 -0
- package/lib/tui/views/AdminView.jsx +398 -0
- package/lib/tui/views/AgentView.jsx +204 -0
- package/lib/tui/views/BackupView.jsx +225 -0
- package/lib/tui/views/BrainView.jsx +223 -0
- package/lib/tui/views/ChatView.jsx +468 -0
- package/lib/tui/views/FilesView.jsx +218 -0
- package/lib/tui/views/FleetView.jsx +234 -0
- package/lib/tui/views/GovernanceView.jsx +453 -0
- package/lib/tui/views/HealthView.jsx +216 -0
- package/lib/tui/views/InboxView.jsx +228 -0
- package/lib/tui/views/LucidDeck.jsx +727 -0
- package/lib/tui/views/MainMenu.jsx +123 -0
- package/lib/tui/views/MemoryView.jsx +377 -0
- package/lib/tui/views/ObjectivesView.jsx +230 -0
- package/lib/tui/views/OrgView.jsx +229 -0
- package/lib/tui/views/ToolView.jsx +221 -0
- package/lib/tui/views/VaultView.jsx +422 -0
- package/lib/tui/views/WorkflowView.jsx +329 -0
- package/package.json +72 -4
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HealthView — btop-style live health dashboard
|
|
3
|
+
*
|
|
4
|
+
* Single full-width panel showing:
|
|
5
|
+
* - Overall status dot (derived from per-service statuses)
|
|
6
|
+
* - A live Sparkline trend of "healthy service count" across recent polls
|
|
7
|
+
* - A dense list of services with status dot + status + latency
|
|
8
|
+
* - Recent governance incidents (best-effort, defensive)
|
|
9
|
+
*
|
|
10
|
+
* Auto-refreshes every 5 seconds via usePolledApi (only while active).
|
|
11
|
+
* `r` manually refreshes; Esc is handled by the parent app shell.
|
|
12
|
+
*
|
|
13
|
+
* API:
|
|
14
|
+
* GET /api/v1/health/aggregate (service: collector) -> { services: [...] } | { checks: [...] }
|
|
15
|
+
* each entry: { name, status, latency_ms }
|
|
16
|
+
* GET /governance/incidents (service: collector) -> { incidents: [...] } (best-effort, defensive)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
20
|
+
import { Box, Text, useInput } from 'ink';
|
|
21
|
+
import { FullLayout } from '../components/Layout.jsx';
|
|
22
|
+
import { Sparkline } from '../components/Sparkline.jsx';
|
|
23
|
+
import { usePolledApi } from '../hooks/usePolledApi.js';
|
|
24
|
+
import { useTui } from '../app.jsx';
|
|
25
|
+
import { matchKey } from '../keys.js';
|
|
26
|
+
import { colors, styles } from '../theme.js';
|
|
27
|
+
import { resolveEndpoint } from '../../api/endpoints.js';
|
|
28
|
+
|
|
29
|
+
// ── Auto-refresh interval (ms) ───────────────────────────────────────
|
|
30
|
+
const REFRESH_INTERVAL = 5_000;
|
|
31
|
+
|
|
32
|
+
// ── History cap for the trend sparkline ──────────────────────────────
|
|
33
|
+
const MAX_HISTORY = 40;
|
|
34
|
+
|
|
35
|
+
// ── Statuses considered "healthy" for the overall roll-up ───────────
|
|
36
|
+
const HEALTHY_STATUSES = new Set(['healthy', 'online', 'active', 'up', 'ok']);
|
|
37
|
+
|
|
38
|
+
function isHealthy(status) {
|
|
39
|
+
return HEALTHY_STATUSES.has(status);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Defensively unwrap the aggregate response into a services array —
|
|
44
|
+
* accepts { services: [...] }, { checks: [...] }, or a bare array.
|
|
45
|
+
*/
|
|
46
|
+
function unwrapServices(data) {
|
|
47
|
+
if (!data) return [];
|
|
48
|
+
if (Array.isArray(data)) return data;
|
|
49
|
+
if (Array.isArray(data.services)) return data.services;
|
|
50
|
+
if (Array.isArray(data.checks)) return data.checks;
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Defensively unwrap an incidents response into an array.
|
|
56
|
+
*/
|
|
57
|
+
function unwrapIncidents(data) {
|
|
58
|
+
if (!data) return [];
|
|
59
|
+
if (Array.isArray(data)) return data;
|
|
60
|
+
if (Array.isArray(data.incidents)) return data.incidents;
|
|
61
|
+
if (Array.isArray(data.data)) return data.data;
|
|
62
|
+
if (Array.isArray(data.items)) return data.items;
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatServiceRow(s) {
|
|
67
|
+
const status = s.status || s.state || 'unknown';
|
|
68
|
+
const latency = s.latency_ms ?? s.latencyMs ?? s.latency;
|
|
69
|
+
return {
|
|
70
|
+
name: s.name || s.service || s.id || 'unknown',
|
|
71
|
+
status,
|
|
72
|
+
latency: latency != null ? `${Number(latency).toFixed(0)}ms` : '--',
|
|
73
|
+
_raw: s,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatIncidentRows(incidents) {
|
|
78
|
+
return unwrapIncidents(incidents)
|
|
79
|
+
.slice(0, 8)
|
|
80
|
+
.map((i, idx) => ({
|
|
81
|
+
id: i.id != null ? String(i.id) : `incident-${idx}`,
|
|
82
|
+
title: i.title || i.summary || i.description || 'incident',
|
|
83
|
+
severity: i.severity || i.level || '--',
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Component ─────────────────────────────────────────────────────────
|
|
88
|
+
export function HealthView() {
|
|
89
|
+
const ctx = useTui();
|
|
90
|
+
const { client, screen } = ctx;
|
|
91
|
+
const isActive = screen === 'health';
|
|
92
|
+
|
|
93
|
+
// ── Health trend history (accumulated across polls) ──────────────────
|
|
94
|
+
const historyRef = useRef([]);
|
|
95
|
+
const [history, setHistory] = useState([]);
|
|
96
|
+
|
|
97
|
+
// ── Fetch aggregate health, polled every 5s while active ─────────────
|
|
98
|
+
const { data, loading, error, refresh } = usePolledApi(
|
|
99
|
+
async () => {
|
|
100
|
+
const res = await client.get(resolveEndpoint('v1.health.aggregate.list').path, { service: resolveEndpoint('v1.health.aggregate.list').service });
|
|
101
|
+
return res || {};
|
|
102
|
+
},
|
|
103
|
+
[client],
|
|
104
|
+
{ intervalMs: REFRESH_INTERVAL, active: isActive }
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// ── Fetch recent governance incidents, best-effort ────────────────────
|
|
108
|
+
const { data: incidentsData } = usePolledApi(
|
|
109
|
+
async () => {
|
|
110
|
+
try {
|
|
111
|
+
const res = await client.get(resolveEndpoint('governance.incidents.list').path, { service: resolveEndpoint('governance.incidents.list').service });
|
|
112
|
+
return res;
|
|
113
|
+
} catch {
|
|
114
|
+
// Endpoint may not exist / may 404 — skip silently, view still works.
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
[client],
|
|
119
|
+
{ intervalMs: REFRESH_INTERVAL, active: isActive }
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const services = unwrapServices(data);
|
|
123
|
+
const rows = services.map(formatServiceRow);
|
|
124
|
+
const incidents = formatIncidentRows(incidentsData);
|
|
125
|
+
|
|
126
|
+
// ── Accumulate a healthy-service-count trend for the sparkline ───────
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (!data) return;
|
|
129
|
+
const healthyCount = services.filter((s) => isHealthy(s.status || s.state)).length;
|
|
130
|
+
|
|
131
|
+
historyRef.current = [...historyRef.current, healthyCount].slice(-MAX_HISTORY);
|
|
132
|
+
setHistory(historyRef.current);
|
|
133
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
134
|
+
}, [data]);
|
|
135
|
+
|
|
136
|
+
// ── Keyboard handling ──────────────────────────────────────────────
|
|
137
|
+
useInput((input, key) => {
|
|
138
|
+
if (!isActive) return;
|
|
139
|
+
|
|
140
|
+
// Manual refresh
|
|
141
|
+
if (matchKey('refresh', input, key)) {
|
|
142
|
+
refresh();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}, { isActive });
|
|
146
|
+
|
|
147
|
+
// ── Overall roll-up status ────────────────────────────────────────
|
|
148
|
+
const overallStatus = rows.length === 0
|
|
149
|
+
? 'unknown'
|
|
150
|
+
: rows.every((r) => isHealthy(r.status))
|
|
151
|
+
? 'healthy'
|
|
152
|
+
: 'degraded';
|
|
153
|
+
|
|
154
|
+
// ── Footer hint ────────────────────────────────────────────────────
|
|
155
|
+
const footer = 'r: refresh Esc: back';
|
|
156
|
+
|
|
157
|
+
// ── Render ─────────────────────────────────────────────────────────
|
|
158
|
+
return (
|
|
159
|
+
<FullLayout
|
|
160
|
+
title="Health"
|
|
161
|
+
subtitle={loading ? '(refreshing...)' : `${rows.length} services`}
|
|
162
|
+
footer={footer}
|
|
163
|
+
>
|
|
164
|
+
<Box flexDirection="column">
|
|
165
|
+
{error && (
|
|
166
|
+
<Box marginBottom={1}>
|
|
167
|
+
<Text color={colors.error}>Error: {error.message}</Text>
|
|
168
|
+
</Box>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
<Box marginBottom={1}>
|
|
172
|
+
<Text color={colors.textMuted}>Overall: </Text>
|
|
173
|
+
<Text>{styles.statusDot(overallStatus)} </Text>
|
|
174
|
+
<Text color={colors.text}>{overallStatus}</Text>
|
|
175
|
+
</Box>
|
|
176
|
+
|
|
177
|
+
{history.length > 0 && (
|
|
178
|
+
<Box marginBottom={1}>
|
|
179
|
+
<Text color={colors.textMuted}>Trend: </Text>
|
|
180
|
+
<Sparkline data={history} width={40} color={colors.cyberBlue} />
|
|
181
|
+
<Text color={colors.dim}> ({history[history.length - 1]}/{rows.length} healthy)</Text>
|
|
182
|
+
</Box>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
186
|
+
{loading && rows.length === 0 && (
|
|
187
|
+
<Text color={colors.dim}>Loading...</Text>
|
|
188
|
+
)}
|
|
189
|
+
{!loading && rows.length === 0 && (
|
|
190
|
+
<Text color={colors.dim}>No service health data available</Text>
|
|
191
|
+
)}
|
|
192
|
+
{rows.map((r) => (
|
|
193
|
+
<Box key={r.name}>
|
|
194
|
+
<Text>{styles.statusDot(r.status)} </Text>
|
|
195
|
+
<Text color={colors.text} bold>{r.name.padEnd(16)}</Text>
|
|
196
|
+
<Text color={colors.dim}>{String(r.status).padEnd(12)}</Text>
|
|
197
|
+
<Text color={colors.textMuted}>{r.latency}</Text>
|
|
198
|
+
</Box>
|
|
199
|
+
))}
|
|
200
|
+
</Box>
|
|
201
|
+
|
|
202
|
+
{incidents.length > 0 && (
|
|
203
|
+
<Box flexDirection="column">
|
|
204
|
+
<Text color={colors.accent} bold>Recent Incidents</Text>
|
|
205
|
+
{incidents.map((i, idx) => (
|
|
206
|
+
<Box key={`${i.id}-${idx}`}>
|
|
207
|
+
<Text color={colors.warning}>{String(i.severity).padEnd(10)}</Text>
|
|
208
|
+
<Text color={colors.text}>{i.title}</Text>
|
|
209
|
+
</Box>
|
|
210
|
+
))}
|
|
211
|
+
</Box>
|
|
212
|
+
)}
|
|
213
|
+
</Box>
|
|
214
|
+
</FullLayout>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InboxView — HITL approvals inbox
|
|
3
|
+
*
|
|
4
|
+
* Left panel: DataTable of inbox items (title, type, status, created)
|
|
5
|
+
* Right panel: DetailPanel with inbox item detail
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - `a` key approves the selected item
|
|
9
|
+
* - `d` key denies the selected item
|
|
10
|
+
* - Auto-refreshes every 10 seconds via usePolledApi (only while active —
|
|
11
|
+
* never polls in the background when another screen is showing)
|
|
12
|
+
* - Stats header (pending/total etc) fetched separately
|
|
13
|
+
*
|
|
14
|
+
* API (org-scoped):
|
|
15
|
+
* GET /api/orgs/:orgId/inbox/stats (service: auth) -> stats
|
|
16
|
+
* GET /api/orgs/:orgId/inbox (service: auth) -> data.items
|
|
17
|
+
* GET /api/orgs/:orgId/inbox/:id (service: auth) -> item detail
|
|
18
|
+
* POST /api/orgs/:orgId/inbox/:id/respond (service: auth) -> { action, comment }
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import React, { useState, useCallback } from 'react';
|
|
22
|
+
import { Box, Text, useInput } from 'ink';
|
|
23
|
+
import { Layout } from '../components/Layout.jsx';
|
|
24
|
+
import { DataTable } from '../components/DataTable.jsx';
|
|
25
|
+
import { DetailPanel } from '../components/DetailPanel.jsx';
|
|
26
|
+
import { usePolledApi } from '../hooks/usePolledApi.js';
|
|
27
|
+
import { useTui } from '../app.jsx';
|
|
28
|
+
import { matchKey } from '../keys.js';
|
|
29
|
+
import { colors, styles } from '../theme.js';
|
|
30
|
+
import { resolveEndpoint } from '../../api/endpoints.js';
|
|
31
|
+
|
|
32
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Safely extract the org ID from TUI context.
|
|
36
|
+
*/
|
|
37
|
+
function getOrgId(ctx) {
|
|
38
|
+
return ctx?.auth?.getUser?.()?.org_id
|
|
39
|
+
|| ctx?.config?.getActiveProfile?.()?.auth?.org_id
|
|
40
|
+
|| null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Column definitions ────────────────────────────────────────────────
|
|
44
|
+
const COLUMNS = [
|
|
45
|
+
{ key: 'title', label: 'Title', width: 28 },
|
|
46
|
+
{ key: 'type', label: 'Type', width: 12 },
|
|
47
|
+
{ key: 'status', label: 'Status', width: 12 },
|
|
48
|
+
{ key: 'created', label: 'Created', width: 12 },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// ── Auto-refresh interval (ms) ───────────────────────────────────────
|
|
52
|
+
const REFRESH_INTERVAL = 10_000;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Format raw inbox data into table rows.
|
|
56
|
+
*/
|
|
57
|
+
function formatRows(items) {
|
|
58
|
+
if (!Array.isArray(items)) return [];
|
|
59
|
+
return items.map((i) => ({
|
|
60
|
+
...i,
|
|
61
|
+
title: i.title || i.subject || i.id,
|
|
62
|
+
type: i.type || i.kind || '--',
|
|
63
|
+
status: `${styles.statusDot(i.status || 'unknown')} ${i.status || 'unknown'}`,
|
|
64
|
+
created: i.created_at ? String(i.created_at).slice(0, 10) : '--',
|
|
65
|
+
_raw: i,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build a detail object for the right panel.
|
|
71
|
+
*/
|
|
72
|
+
function buildDetail(raw) {
|
|
73
|
+
if (!raw) return null;
|
|
74
|
+
return {
|
|
75
|
+
'Title': raw.title || raw.subject,
|
|
76
|
+
'ID': raw.id,
|
|
77
|
+
'Type': raw.type || raw.kind,
|
|
78
|
+
'Status': raw.status,
|
|
79
|
+
'Priority': raw.priority,
|
|
80
|
+
'Description':raw.description || raw.body,
|
|
81
|
+
'Requested By': raw.requested_by || raw.requester,
|
|
82
|
+
'Created': raw.created_at,
|
|
83
|
+
'Updated': raw.updated_at,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Component ─────────────────────────────────────────────────────────
|
|
88
|
+
export function InboxView() {
|
|
89
|
+
const ctx = useTui();
|
|
90
|
+
const { client, screen, showToast, orgVersion } = ctx;
|
|
91
|
+
const isActive = screen === 'inbox';
|
|
92
|
+
const orgId = getOrgId(ctx);
|
|
93
|
+
|
|
94
|
+
const [selected, setSelected] = useState(0);
|
|
95
|
+
|
|
96
|
+
// ── Data fetching (auto-refreshes every 10s, only while active) ─────
|
|
97
|
+
const { data, loading, error, refresh } = usePolledApi(
|
|
98
|
+
async () => {
|
|
99
|
+
if (!orgId) return [];
|
|
100
|
+
const res = await client.get(resolveEndpoint('inbox.list', { orgId }).path, { service: resolveEndpoint('inbox.list', { orgId }).service, query: { status: 'all' } });
|
|
101
|
+
return res?.items || res?.inbox || (Array.isArray(res) ? res : []);
|
|
102
|
+
},
|
|
103
|
+
[client, orgId, orgVersion],
|
|
104
|
+
{ intervalMs: REFRESH_INTERVAL, active: isActive }
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// ── Stats header ──────────────────────────────────────────────────
|
|
108
|
+
const { data: stats } = usePolledApi(
|
|
109
|
+
async () => {
|
|
110
|
+
if (!orgId) return null;
|
|
111
|
+
return client.get(resolveEndpoint('inbox.stats.list', { orgId }).path, { service: resolveEndpoint('inbox.stats.list', { orgId }).service });
|
|
112
|
+
},
|
|
113
|
+
[client, orgId, orgVersion],
|
|
114
|
+
{ intervalMs: REFRESH_INTERVAL, active: isActive }
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// ── Formatted rows ─────────────────────────────────────────────────
|
|
118
|
+
const rows = formatRows(data);
|
|
119
|
+
const selectedRow = rows[selected];
|
|
120
|
+
const detail = selectedRow ? buildDetail(selectedRow._raw) : null;
|
|
121
|
+
const isEmptyInbox = !!orgId && !loading && !error && rows.length === 0;
|
|
122
|
+
|
|
123
|
+
// ── Respond to selected item (approve/deny) ──────────────────────────
|
|
124
|
+
const respond = useCallback(async (action) => {
|
|
125
|
+
if (!selectedRow || !orgId) return;
|
|
126
|
+
const itemId = selectedRow._raw.id;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await client.post(resolveEndpoint('inbox.respond', { orgId, itemId }).path, {
|
|
130
|
+
service: resolveEndpoint('inbox.respond', { orgId, itemId }).service,
|
|
131
|
+
body: { action },
|
|
132
|
+
});
|
|
133
|
+
showToast(action === 'approve' ? 'Approved' : 'Denied', 'success');
|
|
134
|
+
refresh();
|
|
135
|
+
} catch (err) {
|
|
136
|
+
showToast(err.message, 'error');
|
|
137
|
+
}
|
|
138
|
+
}, [selectedRow, orgId, client, showToast, refresh]);
|
|
139
|
+
|
|
140
|
+
// ── Keyboard handling ──────────────────────────────────────────────
|
|
141
|
+
useInput((input, key) => {
|
|
142
|
+
if (!isActive) return;
|
|
143
|
+
|
|
144
|
+
// Manual refresh
|
|
145
|
+
if (matchKey('refresh', input, key)) {
|
|
146
|
+
refresh();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Approve
|
|
151
|
+
if (input === 'a') {
|
|
152
|
+
respond('approve');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Deny
|
|
157
|
+
if (input === 'd') {
|
|
158
|
+
respond('reject');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}, { isActive });
|
|
162
|
+
|
|
163
|
+
// ── Stats summary line ────────────────────────────────────────────
|
|
164
|
+
const statsLine = stats
|
|
165
|
+
? Object.entries(stats)
|
|
166
|
+
.filter(([, v]) => typeof v === 'number' || typeof v === 'string')
|
|
167
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
168
|
+
.join(' ')
|
|
169
|
+
: null;
|
|
170
|
+
|
|
171
|
+
// ── Footer hint ────────────────────────────────────────────────────
|
|
172
|
+
const footer = [
|
|
173
|
+
'↑/k ↓/j Navigate',
|
|
174
|
+
'a: approve',
|
|
175
|
+
'd: deny',
|
|
176
|
+
'r: refresh',
|
|
177
|
+
'Esc: back',
|
|
178
|
+
].join(' ');
|
|
179
|
+
|
|
180
|
+
// ── Render ─────────────────────────────────────────────────────────
|
|
181
|
+
return (
|
|
182
|
+
<Layout
|
|
183
|
+
title="Inbox"
|
|
184
|
+
subtitle={orgId ? `org: ${orgId}` : '(no org)'}
|
|
185
|
+
footer={footer}
|
|
186
|
+
left={
|
|
187
|
+
<Box flexDirection="column">
|
|
188
|
+
{error && (
|
|
189
|
+
<Box marginBottom={1}>
|
|
190
|
+
<Text color={colors.error}>Error: {error.message}</Text>
|
|
191
|
+
</Box>
|
|
192
|
+
)}
|
|
193
|
+
{!orgId && !loading && (
|
|
194
|
+
<Box marginBottom={1}>
|
|
195
|
+
<Text color={colors.warning}>No active org. Switch orgs first.</Text>
|
|
196
|
+
</Box>
|
|
197
|
+
)}
|
|
198
|
+
{statsLine && (
|
|
199
|
+
<Box marginBottom={1}>
|
|
200
|
+
<Text color={colors.info}>{statsLine}</Text>
|
|
201
|
+
</Box>
|
|
202
|
+
)}
|
|
203
|
+
{isEmptyInbox ? (
|
|
204
|
+
<Box paddingY={1}>
|
|
205
|
+
<Text color={colors.success}>Inbox zero — nothing awaiting approval ✓</Text>
|
|
206
|
+
</Box>
|
|
207
|
+
) : (
|
|
208
|
+
<DataTable
|
|
209
|
+
columns={COLUMNS}
|
|
210
|
+
rows={rows}
|
|
211
|
+
selected={selected}
|
|
212
|
+
onSelectionChange={setSelected}
|
|
213
|
+
loading={loading}
|
|
214
|
+
emptyMessage={orgId ? 'No inbox items found' : 'No active org'}
|
|
215
|
+
active={isActive}
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
</Box>
|
|
219
|
+
}
|
|
220
|
+
right={
|
|
221
|
+
<DetailPanel
|
|
222
|
+
title={selectedRow ? (selectedRow._raw.title || selectedRow._raw.id) : undefined}
|
|
223
|
+
data={detail}
|
|
224
|
+
/>
|
|
225
|
+
}
|
|
226
|
+
/>
|
|
227
|
+
);
|
|
228
|
+
}
|