diff --git a/README.md b/README.md index 88fb836..1c156ce 100644 --- a/README.md +++ b/README.md @@ -487,7 +487,7 @@ Flynn includes a built-in web control dashboard served by the WebSocket gateway. | Page | Description | |------|-------------| -| **Dashboard** | System health cards, channel status, usage stats. Auto-refreshes every 10s | +| **Dashboard** | System health cards, channel status, usage stats, assistant-health quick actions, and daily briefing preview/test-send. Auto-refreshes every 10s | | **Chat** | Session selector, streaming tool events, markdown rendering with syntax highlighting | | **Sessions** | Browse all sessions, view message history, delete sessions | | **Usage** | Token usage summary cards, per-session breakdown table, auto-refresh | diff --git a/docs/plans/state.json b/docs/plans/state.json index 33b8151..f58c2d4 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -5480,6 +5480,19 @@ "docs/plans/state.json" ], "test_status": "pnpm typecheck passing" + }, + "dashboard-morning-brief-preview-test-send": { + "status": "completed", + "date": "2026-02-18", + "updated": "2026-02-18", + "summary": "Extended Dashboard Assistant Health with a Morning Brief Preview panel (prompt/schedule/tier/output metadata) and a one-click Test Briefing action that triggers the configured daily briefing via cron.trigger through gateway tools.invoke, including inline save/trigger status feedback.", + "files_modified": [ + "src/gateway/ui/pages/dashboard.js", + "src/gateway/ui/style.css", + "README.md", + "docs/plans/state.json" + ], + "test_status": "pnpm typecheck passing" } }, "overall_progress": { diff --git a/src/gateway/ui/pages/dashboard.js b/src/gateway/ui/pages/dashboard.js index 1eda2e8..0b7c0f7 100644 --- a/src/gateway/ui/pages/dashboard.js +++ b/src/gateway/ui/pages/dashboard.js @@ -453,6 +453,39 @@ async function applyAssistantPatch(patches, statusEl) { } } +async function triggerDailyBriefingTest(jobName, statusEl) { + if (!_dashboardClient) {return;} + if (statusEl) { + statusEl.textContent = 'Triggering test briefing...'; + statusEl.className = 'text-sm text-muted'; + } + try { + const result = await _dashboardClient.call('tools.invoke', { + tool: 'cron.trigger', + args: { name: jobName }, + }); + + if (result?.success) { + const output = typeof result.output === 'string' ? result.output : 'Triggered.'; + if (statusEl) { + statusEl.textContent = output; + statusEl.className = 'text-sm text-success'; + } + return; + } + + if (statusEl) { + statusEl.textContent = result?.error ?? 'Failed to trigger briefing.'; + statusEl.className = 'text-sm text-error'; + } + } catch (error) { + if (statusEl) { + statusEl.textContent = `Trigger error: ${error instanceof Error ? error.message : String(error)}`; + statusEl.className = 'text-sm text-error'; + } + } +} + function updateAssistantHealth(configData) { const el = document.getElementById('ops-assistant-health'); if (!el) {return;} @@ -468,6 +501,17 @@ function updateAssistantHealth(configData) { const memoryProactive = Boolean(memory.proactive_extract?.enabled); const proactiveThreshold = Number(memory.proactive_extract?.min_tool_calls ?? 1); const ttsEnabled = Boolean(tts.enabled); + const briefing = automation.daily_briefing ?? {}; + const briefingName = briefing.name ?? 'daily-briefing'; + const briefingSchedule = briefing.schedule ?? '0 8 * * *'; + const briefingPrompt = briefing.prompt ?? ''; + const briefingOutput = briefing.output ?? null; + const briefingModelTier = briefing.model_tier ?? 'default'; + const briefingTimezone = briefing.timezone ?? 'system'; + const briefingOutputLabel = briefingOutput?.channel && briefingOutput?.peer + ? `${briefingOutput.channel}/${briefingOutput.peer}` + : 'not configured'; + const briefingReady = dailyBriefing && Boolean(briefingOutput?.channel && briefingOutput?.peer); const chip = (label, value) => `
@@ -505,6 +549,23 @@ function updateAssistantHealth(configData) { ${ttsEnabled ? 'Disable TTS' : 'Enable TTS'}
+
+
+
Morning Brief Preview
+ +
+
+ name: ${escapeHtml(briefingName)} + schedule: ${escapeHtml(briefingSchedule)} + timezone: ${escapeHtml(briefingTimezone)} + tier: ${escapeHtml(briefingModelTier)} + output: ${escapeHtml(briefingOutputLabel)} +
+
${escapeHtml(briefingPrompt || 'No daily briefing prompt configured.')}
+ ${briefingReady ? '' : '
Enable daily briefing and set output channel/peer to test-send.
'} +
`; @@ -524,6 +585,9 @@ function updateAssistantHealth(configData) { patches = { 'memory.proactive_extract.enabled': !memoryProactive }; } else if (action === 'toggle-tts') { patches = { 'tts.enabled': !ttsEnabled }; + } else if (action === 'test-daily-briefing') { + await triggerDailyBriefingTest(briefingName, statusEl); + return; } if (!patches) {return;} await applyAssistantPatch(patches, statusEl); diff --git a/src/gateway/ui/style.css b/src/gateway/ui/style.css index 4d9fa97..78364e5 100644 --- a/src/gateway/ui/style.css +++ b/src/gateway/ui/style.css @@ -1571,6 +1571,47 @@ tr:hover td { color: var(--text-primary); } +.assistant-preview { + margin-top: 12px; + padding: 12px; + border: 1px solid var(--border); + border-radius: var(--radius); + background-color: var(--bg-secondary); +} + +.assistant-preview-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + margin-bottom: 10px; +} + +.assistant-preview-title { + font-size: var(--font-size-sm); + font-weight: 700; + color: var(--text-primary); +} + +.assistant-preview-meta { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 10px; +} + +.assistant-preview-body { + max-height: 180px; + overflow-y: auto; + border: 1px solid var(--border-light); + border-radius: var(--radius); + padding: 10px; + background-color: var(--bg-tertiary); + white-space: pre-wrap; + word-break: break-word; + font-size: var(--font-size-sm); +} + /* ── Responsive: Mobile ─────────────────────────────────────── */ @media (max-width: 768px) {