mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-08-18 16:37:24 +02:00
Ship Meshtastic Chat UX, embedded Infonet/SHELL panels, and Docker dev polish.
Rename Mesh Chat to Meshtastic Chat, embed the Infonet terminal with Arti/Tor warmup, improve the agent shell PTY (git in the backend image, operator PATH), and add docker-compose.override for local image builds. Gitignore Hermes Agent runtime installs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -137,6 +137,9 @@ interface CommandHistory {
|
||||
|
||||
interface InfonetShellProps {
|
||||
isOpen: boolean;
|
||||
embedded?: boolean;
|
||||
launchGate?: string;
|
||||
onLaunchGateConsumed?: () => void;
|
||||
onClose: () => void;
|
||||
onOpenLiveGate?: (gate: string) => void;
|
||||
onOpenDeadDrop?: (peerId: string, options?: { showSas?: boolean }) => void;
|
||||
@@ -144,6 +147,9 @@ interface InfonetShellProps {
|
||||
|
||||
export default function InfonetShell({
|
||||
isOpen,
|
||||
embedded = false,
|
||||
launchGate = '',
|
||||
onLaunchGateConsumed,
|
||||
onClose,
|
||||
onOpenLiveGate,
|
||||
onOpenDeadDrop,
|
||||
@@ -176,6 +182,7 @@ export default function InfonetShell({
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const gateLaunchAttemptRef = useRef(0);
|
||||
const launchGateConsumedRef = useRef('');
|
||||
|
||||
// Real mesh identity
|
||||
const nodeIdentity = useMemo(() => getNodeIdentity(), []);
|
||||
@@ -203,6 +210,7 @@ export default function InfonetShell({
|
||||
setPendingGate(null);
|
||||
setInput('');
|
||||
gateLaunchAttemptRef.current += 1;
|
||||
launchGateConsumedRef.current = '';
|
||||
setIsBooting(true);
|
||||
setBootText([]);
|
||||
|
||||
@@ -600,6 +608,14 @@ export default function InfonetShell({
|
||||
setHistory(prev => [...prev, { command: cmd, output }]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const gate = String(launchGate || '').trim().toLowerCase();
|
||||
if (!isOpen || isBooting || !gate || launchGateConsumedRef.current === gate) return;
|
||||
launchGateConsumedRef.current = gate;
|
||||
handleCommand(`join ${gate}`);
|
||||
onLaunchGateConsumed?.();
|
||||
}, [isOpen, isBooting, launchGate, onLaunchGateConsumed]);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (inputMode === 'normal' && input.startsWith('g/') && searchMatch) {
|
||||
@@ -635,7 +651,12 @@ export default function InfonetShell({
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="h-full bg-[#0a0a0a] text-gray-300 p-4 md:p-8 font-mono relative flex flex-col overflow-hidden">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`h-full min-h-0 bg-[#0a0a0a] text-gray-300 font-mono relative flex flex-col overflow-hidden ${
|
||||
embedded ? 'p-2 md:p-3 text-[13px]' : 'p-4 md:p-8'
|
||||
}`}
|
||||
>
|
||||
{currentView === 'terminal' && (
|
||||
<>
|
||||
{/* Top Navigation / Quick Launch */}
|
||||
|
||||
@@ -13,11 +13,12 @@ interface Stats {
|
||||
seedPeers: number;
|
||||
nodeEnabled: boolean;
|
||||
syncOutcome: string;
|
||||
syncError: string;
|
||||
}
|
||||
|
||||
const EMPTY: Stats = {
|
||||
meshtastic: 0, aprs: 0, ledgerNodes: 0, infonetEvents: 0,
|
||||
syncPeers: 0, seedPeers: 0, nodeEnabled: false, syncOutcome: 'offline',
|
||||
syncPeers: 0, seedPeers: 0, nodeEnabled: false, syncOutcome: 'offline', syncError: '',
|
||||
};
|
||||
|
||||
export default function NetworkStats() {
|
||||
@@ -41,6 +42,7 @@ export default function NetworkStats() {
|
||||
?? infonet?.bootstrap?.default_sync_peer_count
|
||||
?? 0,
|
||||
);
|
||||
const syncOutcome = String(infonet?.sync_runtime?.last_outcome || 'offline').toLowerCase();
|
||||
setStats({
|
||||
meshtastic: Number(channelsRes?.total_live || channelsRes?.total_nodes || meshRes?.signal_counts?.meshtastic || 0),
|
||||
aprs: Number(meshRes?.signal_counts?.aprs || 0),
|
||||
@@ -49,26 +51,36 @@ export default function NetworkStats() {
|
||||
syncPeers: syncPeerCount,
|
||||
seedPeers: seedPeerCount,
|
||||
nodeEnabled: Boolean(infonet?.node_enabled),
|
||||
syncOutcome: String(infonet?.sync_runtime?.last_outcome || 'offline').toLowerCase(),
|
||||
syncOutcome,
|
||||
syncError: String(infonet?.sync_runtime?.last_error || '').trim(),
|
||||
});
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
poll();
|
||||
const interval = setInterval(poll, 15000);
|
||||
const interval = setInterval(poll, 8000);
|
||||
return () => { alive = false; clearInterval(interval); };
|
||||
}, []);
|
||||
|
||||
const nodeColor = stats.syncOutcome === 'ok' ? 'text-green-400'
|
||||
const syncErrorLower = stats.syncError.toLowerCase();
|
||||
const artiBlocked = syncErrorLower.includes('arti') || syncErrorLower.includes('onion');
|
||||
const nodeColor = stats.syncOutcome === 'ok' || stats.syncOutcome === 'solo' ? 'text-green-400'
|
||||
: stats.syncOutcome === 'running' ? 'text-amber-400'
|
||||
: stats.nodeEnabled ? 'text-amber-400' : 'text-gray-600';
|
||||
const nodeLabel = stats.syncOutcome === 'ok' ? 'SEED SYNCED'
|
||||
: stats.syncOutcome === 'solo' ? 'SOLO'
|
||||
: stats.syncOutcome === 'running' ? 'SYNCING'
|
||||
: stats.syncOutcome === 'error' || stats.syncOutcome === 'fork' ? 'RETRYING'
|
||||
: stats.syncOutcome === 'error' || stats.syncOutcome === 'fork'
|
||||
? (artiBlocked ? 'ARTI WARMING' : 'SYNC BACKOFF')
|
||||
: stats.nodeEnabled ? 'WAITING' : 'OFFLINE';
|
||||
const nodeTitle = stats.syncError
|
||||
? `Infonet seed sync: ${stats.syncError}`
|
||||
: stats.nodeEnabled
|
||||
? 'Participant node enabled; waiting for seed ledger sync.'
|
||||
: 'Participant node offline.';
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-5 gap-y-1 mt-5 text-sm font-mono text-gray-500">
|
||||
<span>NODE <span className={nodeColor}>{nodeLabel}</span></span>
|
||||
<span title={nodeTitle}>NODE <span className={nodeColor}>{nodeLabel}</span></span>
|
||||
<span className="text-gray-700">|</span>
|
||||
<span>MESH <span className={stats.meshtastic > 0 ? 'text-green-400' : 'text-gray-600'}>{stats.meshtastic.toLocaleString()}</span></span>
|
||||
<span className="text-gray-700">|</span>
|
||||
|
||||
@@ -4,9 +4,7 @@ import React, { useEffect } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { X } from 'lucide-react';
|
||||
import {
|
||||
fetchInfonetNodeStatusSnapshot,
|
||||
setInfonetNodeEnabled,
|
||||
startTorHiddenService,
|
||||
ensureInfonetParticipantNodeReady,
|
||||
} from '@/mesh/controlPlaneStatusClient';
|
||||
import InfonetShell from './InfonetShell';
|
||||
|
||||
@@ -39,16 +37,8 @@ export default function InfonetTerminal({
|
||||
|
||||
const connectParticipantNode = async () => {
|
||||
try {
|
||||
const nodeStatus = await fetchInfonetNodeStatusSnapshot(true).catch(() => null);
|
||||
if (cancelled || nodeStatus?.node_enabled) return;
|
||||
|
||||
const torStatus = await startTorHiddenService().catch(() => null);
|
||||
if (cancelled || !torStatus?.running || !torStatus?.onion_address) return;
|
||||
|
||||
await setInfonetNodeEnabled(true);
|
||||
if (!cancelled) {
|
||||
await fetchInfonetNodeStatusSnapshot(true).catch(() => null);
|
||||
}
|
||||
if (cancelled) return;
|
||||
await ensureInfonetParticipantNodeReady();
|
||||
} catch {
|
||||
// Remote/shared viewers may not have local-operator rights. Leave manual controls intact.
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user