From 17e573911dc14d059937aaded560db5853569fe7 Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Thu, 21 May 2026 10:03:41 +0200 Subject: [PATCH] Add Hermes cron synchronization cockpit --- src/App.tsx | 488 ++++++++++++++++++++++++++++++--- src/index.css | 319 ++++++++++++++++++++- src/services/pollingService.ts | 227 ++++++++++----- 3 files changed, 929 insertions(+), 105 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 561f9ee..16569f6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,13 +3,15 @@ import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Send, Mic, MicOff, Settings, MessageSquare, Volume2, VolumeX, - Sparkles, Cpu, Minimize2, X, AlertCircle, StopCircle, Paperclip + Sparkles, Cpu, Minimize2, X, AlertCircle, StopCircle, Paperclip, + CalendarClock, History, Wrench, Play, Pause, RefreshCw, Trash2, Plus, + CheckCircle2, WifiOff, Edit3, Save } from 'lucide-react'; import { ThreeCanvas, ToolEvent } from './components/ThreeCanvas'; import { AudioService } from './services/audioService'; import { AgentService, AgentConfig, AgentProvider } from './services/agentService'; import { EmotionService, EveAvatar, EveEmotion } from './services/emotionService'; -import { PollingService } from './services/pollingService'; +import { HermesJob, HermesJobDraft, PollingService } from './services/pollingService'; import { AppCallbacks } from './services/toolRegistry'; import { version } from '../package.json'; @@ -19,7 +21,7 @@ interface Message { content: string; emotion?: EveEmotion; timestamp: Date; - source?: 'eveflow' | 'telegram' | 'webhook'; + source?: 'eveflow' | 'telegram' | 'webhook' | 'cron'; images?: string[]; // base64 Data URLs pour affichage local direct } @@ -30,6 +32,25 @@ interface Attachment { size: number; } +type HermesCockpitTab = 'tools' | 'crons' | 'history'; +type HermesSyncState = 'idle' | 'syncing' | 'online' | 'offline'; + +interface HermesRunRecord { + id: string; + jobId: string; + jobName: string; + status: 'succeeded' | 'failed'; + result: string; + at: string; + read: boolean; +} + +interface HermesCronCache { + jobs: HermesJob[]; + runs: HermesRunRecord[]; + lastSyncAt?: string; +} + export const TTS_LONG_TEXT_THRESHOLD = 450; // Helper de compression d'image asynchrone via Canvas HTML5 (Zero Trust & Zero Dependency) @@ -487,32 +508,245 @@ const createWelcomeMessage = (avatarId: EveAvatar): Message => ({ timestamp: new Date() }); -const ToolMonitorWindow: React.FC<{ +const getHermesJobId = (job: HermesJob): string => job.id || String(job.job_id || job.name || Math.random()); + +const getHermesJobStatus = (job: HermesJob): string => { + if (job.state) return String(job.state); + if (job.status) return String(job.status); + if (job.enabled === false) return 'paused'; + if (job.last_status) return String(job.last_status); + return 'scheduled'; +}; + +const formatHermesSchedule = (schedule: HermesJob['schedule']): string => { + if (!schedule) return 'non planifie'; + if (typeof schedule === 'string') return schedule; + return schedule.display || schedule.expr || schedule.kind || 'schedule'; +}; + +const getHermesJobResult = (job: HermesJob): string => ( + job.result || job.last_result || job.output || job.last_output || job.error || job.last_error || '' +); + +const getHermesJobTime = (job: HermesJob): string => ( + job.completed_at || job.last_run_at || job.updated_at || job.created_at || new Date().toISOString() +); + +const buildRunRecordFromJob = (job: HermesJob): HermesRunRecord | null => { + const result = getHermesJobResult(job); + const status = getHermesJobStatus(job).toLowerCase(); + if (!result || (!['completed', 'failed', 'ok'].includes(status) && !job.completed_at && !job.last_run_at)) { + return null; + } + + const at = getHermesJobTime(job); + const jobId = getHermesJobId(job); + return { + id: `${jobId}:${at}:${result.slice(0, 24)}`, + jobId, + jobName: job.name || jobId, + status: status === 'failed' || job.error || job.last_error ? 'failed' : 'succeeded', + result, + at, + read: false + }; +}; + +const previewText = (value: string, max = 96): string => ( + value.length > max ? `${value.slice(0, max - 1)}...` : value +); + +const createDefaultCronDraft = (): HermesJobDraft => ({ + name: '', + schedule: 'every 1h', + prompt: '', + deliver: 'local' +}); + +const HermesCockpitWindow: React.FC<{ events: ToolEvent[]; isWorking: boolean; -}> = ({ events, isWorking }) => { + jobs: HermesJob[]; + runs: HermesRunRecord[]; + syncState: HermesSyncState; + syncDetail?: string; + lastSyncAt?: string; + draft: HermesJobDraft; + editingJobId: string | null; + isSaving: boolean; + onDraftChange: (draft: HermesJobDraft) => void; + onCreate: () => void; + onCancelEdit: () => void; + onStartEdit: (job: HermesJob) => void; + onUpdate: () => void; + onRefresh: () => void; + onPause: (job: HermesJob) => void; + onResume: (job: HermesJob) => void; + onRun: (job: HermesJob) => void; + onDelete: (job: HermesJob) => void; + onOpenRun: (run: HermesRunRecord) => void; +}> = ({ + events, + isWorking, + jobs, + runs, + syncState, + syncDetail, + lastSyncAt, + draft, + editingJobId, + isSaving, + onDraftChange, + onCreate, + onCancelEdit, + onStartEdit, + onUpdate, + onRefresh, + onPause, + onResume, + onRun, + onDelete, + onOpenRun +}) => { const latestEvents = events.slice(-8).reverse(); + const [activeTab, setActiveTab] = useState('tools'); + const unreadRuns = runs.filter(run => !run.read).length; + const hasDraft = draft.name.trim() && draft.schedule.trim() && draft.prompt.trim(); + const canSaveDraft = !!hasDraft && !isSaving; + + const renderTools = () => ( +
+ {latestEvents.length === 0 ? ( +
Aucun outil en cours
+ ) : ( + latestEvents.map((event) => ( +
+ {event.status.toUpperCase()} + {event.label} + {event.detail && {event.detail}} +
+ )) + )} +
+ ); + + const renderCrons = () => ( +
+
+ onDraftChange({ ...draft, name: e.target.value })} + placeholder="Nom du cron" + /> + onDraftChange({ ...draft, schedule: e.target.value })} + placeholder="every 1h / 0 9 * * *" + /> +