feat(dashboard): add morning brief preview and test-send action
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user