feat(dashboard): add morning brief preview and test-send action

This commit is contained in:
William Valentin
2026-02-18 12:09:57 -08:00
parent ef3f401278
commit 7a9ac34618
4 changed files with 119 additions and 1 deletions
+1 -1
View File
@@ -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 |
+13
View File
@@ -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": {
+64
View File
@@ -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) => `
<div class="assistant-chip">
@@ -505,6 +549,23 @@ function updateAssistantHealth(configData) {
${ttsEnabled ? 'Disable TTS' : 'Enable TTS'}
</button>
</div>
<div class="assistant-preview">
<div class="assistant-preview-header">
<div class="assistant-preview-title">Morning Brief Preview</div>
<button class="btn btn-secondary assistant-action-btn" data-action="test-daily-briefing" ${briefingReady ? '' : 'disabled'}>
Send Test Briefing
</button>
</div>
<div class="assistant-preview-meta text-sm text-muted">
<span>name: <code>${escapeHtml(briefingName)}</code></span>
<span>schedule: <code>${escapeHtml(briefingSchedule)}</code></span>
<span>timezone: <code>${escapeHtml(briefingTimezone)}</code></span>
<span>tier: <code>${escapeHtml(briefingModelTier)}</code></span>
<span>output: <code>${escapeHtml(briefingOutputLabel)}</code></span>
</div>
<div class="assistant-preview-body"><code>${escapeHtml(briefingPrompt || 'No daily briefing prompt configured.')}</code></div>
${briefingReady ? '' : '<div class="text-sm text-muted">Enable daily briefing and set output channel/peer to test-send.</div>'}
</div>
<div id="ops-assistant-status" class="text-sm text-muted"></div>
`;
@@ -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);
+41
View File
@@ -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) {