feat(v9): Phase 4 — Agent-Cockpit (Status/Cron/Skills) im Dashboard
Macht das "geschenkte v9" sichtbar. Read-only Control-Plane-Reads:
- hermes_control.py: gateway_state.json + cron-Heartbeat (Status),
`hermes cron list` + `hermes skills list` via COLUMNS=400 (kein
Rich-Truncation) als kanonische Quelle, .usage.json fuer Nutzung.
TTL-Cache (10s) gegen Polling-Last.
- routers/hermes.py: GET /api/hermes/{agent,cron,skills}.
- HermesPanel: aufklappbares "Cockpit" — Status-Badges, Cron-Jobs,
Skills (sortiert nach Nutzung, aktiv/inaktiv).
- config.py: HERMES_HOME, HERMES_BIN.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,11 @@
|
||||
let recording = $state(false)
|
||||
let status = $state<any>(null)
|
||||
let setupOpen = $state(false)
|
||||
let cockpitOpen = $state(false)
|
||||
let cockpitLoading = $state(false)
|
||||
let agent = $state<any>(null)
|
||||
let cronJobs = $state<any[]>([])
|
||||
let skills = $state<any[]>([])
|
||||
let pubkey = $state('')
|
||||
let ws = $state<WebSocket | null>(null)
|
||||
let currentBot = $state('') // aktuell gestreamter Hermes-Token
|
||||
@@ -195,6 +200,29 @@
|
||||
} catch { pubkey = '' }
|
||||
}
|
||||
|
||||
// ---- Cockpit (Phase 4): Agent-Status, Cron, Skills ----
|
||||
async function loadCockpit() {
|
||||
cockpitLoading = true
|
||||
try {
|
||||
const [a, c, s] = await Promise.all([
|
||||
api('/api/hermes/agent'),
|
||||
api('/api/hermes/cron'),
|
||||
api('/api/hermes/skills'),
|
||||
])
|
||||
agent = a
|
||||
cronJobs = c.jobs || []
|
||||
skills = s.skills || []
|
||||
} catch { agent = null }
|
||||
cockpitLoading = false
|
||||
}
|
||||
|
||||
// Skills sortiert: meistgenutzt zuerst, dann alphabetisch
|
||||
let skillsSorted = $derived(
|
||||
[...skills].sort((a, b) =>
|
||||
(b.use_count || 0) - (a.use_count || 0) || (a.name || '').localeCompare(b.name || ''))
|
||||
)
|
||||
let skillsEnabled = $derived(skills.filter(s => (s.status || '').includes('enabled')).length)
|
||||
|
||||
// ---- Hilfsfunktionen ----
|
||||
function toolLabel(name: string) {
|
||||
const map: Record<string, string> = {
|
||||
@@ -245,6 +273,11 @@
|
||||
onclick={() => voiceOn = !voiceOn}>
|
||||
🔊 Voice {voiceOn ? 'an' : 'aus'}
|
||||
</button>
|
||||
<!-- Cockpit (Phase 4) -->
|
||||
<button class="ghost" style="padding:4px 10px;font-size:12px;{cockpitOpen ? 'border-color:var(--accent);color:var(--accent)' : ''}"
|
||||
onclick={() => { cockpitOpen = !cockpitOpen; if (cockpitOpen) loadCockpit() }}>
|
||||
📡 Cockpit
|
||||
</button>
|
||||
<!-- Setup -->
|
||||
<button class="ghost" style="padding:4px 10px;font-size:12px"
|
||||
onclick={() => { setupOpen = !setupOpen; if (setupOpen) { loadStatus(); loadPubkey() } }}>
|
||||
@@ -253,6 +286,97 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== COCKPIT (Phase 4) ==================== -->
|
||||
{#if cockpitOpen}
|
||||
<div class="card" style="border-color:rgba(45,212,191,.3)">
|
||||
<div class="card-h" style="display:flex;justify-content:space-between;align-items:center">
|
||||
<h3>Agent-Cockpit</h3>
|
||||
<button class="ghost" style="font-size:11px;padding:2px 10px" onclick={loadCockpit}>
|
||||
{cockpitLoading ? '…' : '↻ Aktualisieren'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Agent-Status -->
|
||||
<div class="tile" style="margin-bottom:10px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px">
|
||||
<div>
|
||||
<b>Status</b>
|
||||
<div class="card-sub" style="font-size:12px">Hermes-Gateway-Dienst (:8642) auf dem Bosgame</div>
|
||||
</div>
|
||||
<div class="flex gap-2" style="flex-wrap:wrap;align-items:center">
|
||||
<span class="chip" style="font-size:11px;{agent?.gateway_state === 'running'
|
||||
? 'color:#7ee29a;border-color:rgba(126,226,154,.4)' : 'color:#ff9b95;border-color:rgba(255,155,149,.4)'}">
|
||||
● {agent?.gateway_state ?? 'offline'}
|
||||
</span>
|
||||
<span class="chip" style="font-size:11px;{agent?.cron_running
|
||||
? 'color:var(--accent);border-color:var(--accent)' : 'color:var(--mut)'}">
|
||||
⏱ Scheduler {agent?.cron_running ? 'läuft' : 'inaktiv'}
|
||||
</span>
|
||||
{#if agent?.heartbeat_age_s != null}
|
||||
<span class="chip" style="font-size:11px;color:var(--mut)">Ticker vor {agent.heartbeat_age_s}s</span>
|
||||
{/if}
|
||||
{#if agent?.version}
|
||||
<span class="chip" style="font-size:11px;color:var(--mut)">v{agent.version}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cron-Scheduler -->
|
||||
<div class="tile" style="margin-bottom:10px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
|
||||
<b>Geplante Aufgaben <span class="card-sub" style="font-size:11px">({cronJobs.length})</span></b>
|
||||
</div>
|
||||
{#if cronJobs.length === 0}
|
||||
<div class="card-sub" style="font-size:12px">
|
||||
Keine geplanten Jobs. Hermes kann selbst welche anlegen — sag ihm z. B.
|
||||
„Erinnere mich jeden Morgen um 9 Uhr an die offenen Modell-Upgrades".
|
||||
</div>
|
||||
{:else}
|
||||
<div style="display:flex;flex-direction:column;gap:6px">
|
||||
{#each cronJobs as job}
|
||||
<div style="display:flex;align-items:center;gap:8px;font-size:12.5px;
|
||||
padding:6px 8px;background:rgba(255,255,255,.02);border-radius:6px">
|
||||
{#each Object.entries(job) as [k, v]}
|
||||
{#if v}<span style="color:var(--mut)"><span style="opacity:.6">{k}:</span> {v}</span>{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Skills -->
|
||||
<div class="tile">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
|
||||
<b>Skills <span class="card-sub" style="font-size:11px">({skillsEnabled} aktiv von {skills.length})</span></b>
|
||||
</div>
|
||||
{#if skills.length === 0}
|
||||
<div class="card-sub" style="font-size:12px">Keine Skills geladen (oder Agent offline).</div>
|
||||
{:else}
|
||||
<div style="max-height:260px;overflow-y:auto;display:flex;flex-direction:column;gap:3px">
|
||||
{#each skillsSorted as s}
|
||||
<div style="display:flex;align-items:center;gap:8px;font-size:12.5px;padding:4px 8px;
|
||||
border-radius:6px;{(s.status || '').includes('enabled') ? '' : 'opacity:.45'}">
|
||||
<span style="width:7px;height:7px;border-radius:50%;flex-shrink:0;
|
||||
background:{(s.status || '').includes('enabled') ? 'var(--accent)' : 'var(--mut)'}"></span>
|
||||
<span style="font-weight:500">{s.name}</span>
|
||||
{#if s.category}<span class="card-sub" style="font-size:11px">{s.category}</span>{/if}
|
||||
<span style="flex:1"></span>
|
||||
{#if s.use_count > 0}
|
||||
<span class="chip" style="font-size:10px;color:var(--accent);border-color:rgba(45,212,191,.3)">
|
||||
{s.use_count}× genutzt
|
||||
</span>
|
||||
{/if}
|
||||
<span class="card-sub" style="font-size:10px">{s.source}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- ==================== SETUP-WIZARD ==================== -->
|
||||
{#if setupOpen}
|
||||
<div class="card" style="border-color:rgba(45,212,191,.3)">
|
||||
|
||||
Reference in New Issue
Block a user