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 |
|
| 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 |
|
| **Chat** | Session selector, streaming tool events, markdown rendering with syntax highlighting |
|
||||||
| **Sessions** | Browse all sessions, view message history, delete sessions |
|
| **Sessions** | Browse all sessions, view message history, delete sessions |
|
||||||
| **Usage** | Token usage summary cards, per-session breakdown table, auto-refresh |
|
| **Usage** | Token usage summary cards, per-session breakdown table, auto-refresh |
|
||||||
|
|||||||
@@ -5480,6 +5480,19 @@
|
|||||||
"docs/plans/state.json"
|
"docs/plans/state.json"
|
||||||
],
|
],
|
||||||
"test_status": "pnpm typecheck passing"
|
"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": {
|
"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) {
|
function updateAssistantHealth(configData) {
|
||||||
const el = document.getElementById('ops-assistant-health');
|
const el = document.getElementById('ops-assistant-health');
|
||||||
if (!el) {return;}
|
if (!el) {return;}
|
||||||
@@ -468,6 +501,17 @@ function updateAssistantHealth(configData) {
|
|||||||
const memoryProactive = Boolean(memory.proactive_extract?.enabled);
|
const memoryProactive = Boolean(memory.proactive_extract?.enabled);
|
||||||
const proactiveThreshold = Number(memory.proactive_extract?.min_tool_calls ?? 1);
|
const proactiveThreshold = Number(memory.proactive_extract?.min_tool_calls ?? 1);
|
||||||
const ttsEnabled = Boolean(tts.enabled);
|
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) => `
|
const chip = (label, value) => `
|
||||||
<div class="assistant-chip">
|
<div class="assistant-chip">
|
||||||
@@ -505,6 +549,23 @@ function updateAssistantHealth(configData) {
|
|||||||
${ttsEnabled ? 'Disable TTS' : 'Enable TTS'}
|
${ttsEnabled ? 'Disable TTS' : 'Enable TTS'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
<div id="ops-assistant-status" class="text-sm text-muted"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -524,6 +585,9 @@ function updateAssistantHealth(configData) {
|
|||||||
patches = { 'memory.proactive_extract.enabled': !memoryProactive };
|
patches = { 'memory.proactive_extract.enabled': !memoryProactive };
|
||||||
} else if (action === 'toggle-tts') {
|
} else if (action === 'toggle-tts') {
|
||||||
patches = { 'tts.enabled': !ttsEnabled };
|
patches = { 'tts.enabled': !ttsEnabled };
|
||||||
|
} else if (action === 'test-daily-briefing') {
|
||||||
|
await triggerDailyBriefingTest(briefingName, statusEl);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (!patches) {return;}
|
if (!patches) {return;}
|
||||||
await applyAssistantPatch(patches, statusEl);
|
await applyAssistantPatch(patches, statusEl);
|
||||||
|
|||||||
@@ -1571,6 +1571,47 @@ tr:hover td {
|
|||||||
color: var(--text-primary);
|
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 ─────────────────────────────────────── */
|
/* ── Responsive: Mobile ─────────────────────────────────────── */
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|||||||
Reference in New Issue
Block a user