forked from Rockachopa/Timmy-time-dashboard
feat(briefing): morning briefing + approval queue
Implements the Morning Briefing and Approval Queue feature — the first step
from tool to companion. Timmy now shows up before the owner asks.
New modules
-----------
• src/timmy/approvals.py — ApprovalItem dataclass, GOLDEN_TIMMY governance
constant, full SQLite CRUD (create / list / approve / reject / expire).
Items auto-expire after 7 days if not actioned.
• src/timmy/briefing.py — BriefingEngine that queries swarm activity and
chat history, calls Timmy's Agno agent for a prose summary, and caches
the result in SQLite (~/.timmy/briefings.db). get_or_generate() skips
regeneration if a fresh briefing (< 30 min) already exists.
New routes (src/dashboard/routes/briefing.py)
----------------------------------------------
GET /briefing — full briefing page
GET /briefing/approvals — HTMX partial: pending approval cards
POST /briefing/approvals/{id}/approve — approve via HTMX (no page reload)
POST /briefing/approvals/{id}/reject — reject via HTMX (no page reload)
New templates
-------------
• briefing.html — clean, mobile-first prose layout (max 680px)
• partials/approval_cards.html — list of approval cards
• partials/approval_card_single.html — single approval card with
Approve/Reject HTMX buttons
App wiring (src/dashboard/app.py)
----------------------------------
• Added asynccontextmanager lifespan with _briefing_scheduler background task.
Generates a briefing at startup and every 6 hours; skips if fresh.
Push notification hook (src/notifications/push.py)
---------------------------------------------------
• notify_briefing_ready(briefing) — logs + triggers local notifier.
Placeholder for APNs/Pushover wiring later.
Navigation
----------
• Added BRIEFING link to the header nav in base.html.
Tests
-----
• tests/test_approvals.py — 17 tests: GOLDEN_TIMMY, CRUD, expiry, ordering
• tests/test_briefing.py — 22 tests: dataclass, freshness, cache round-trip,
generate/get_or_generate, push notification hook
354 tests, 354 passing.
https://claude.ai/code/session_01D7p5w91KX3grBeioGiiGy8
This commit is contained in:
208
src/dashboard/templates/briefing.html
Normal file
208
src/dashboard/templates/briefing.html
Normal file
@@ -0,0 +1,208 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Timmy Time — Morning Briefing{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container briefing-container py-4">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="briefing-header mb-4">
|
||||
<div class="briefing-greeting">Good morning.</div>
|
||||
<div class="briefing-timestamp">
|
||||
Briefing generated
|
||||
<span class="briefing-ts-val">{{ briefing.generated_at.strftime('%Y-%m-%d %H:%M UTC') }}</span>
|
||||
— covering
|
||||
<span class="briefing-ts-val">{{ briefing.period_start.strftime('%H:%M') }}</span>
|
||||
to
|
||||
<span class="briefing-ts-val">{{ briefing.period_end.strftime('%H:%M UTC') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="card mc-panel briefing-summary mb-5">
|
||||
<div class="card-header mc-panel-header">// TIMMY’S REPORT</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="briefing-prose">{{ briefing.summary }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Approval Queue -->
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header d-flex justify-content-between align-items-center">
|
||||
<span>// APPROVAL QUEUE</span>
|
||||
<span class="badge bg-warning text-dark"
|
||||
id="approval-count">{{ briefing.approval_items | length }} pending</span>
|
||||
</div>
|
||||
<div class="card-body p-3"
|
||||
id="approval-queue"
|
||||
hx-get="/briefing/approvals"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<!-- HTMX fills this on load with live data -->
|
||||
<div class="text-center text-muted py-3">Loading approval items…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Briefing-specific styles — mobile-first */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
.briefing-container {
|
||||
max-width: 680px;
|
||||
}
|
||||
|
||||
.briefing-header {
|
||||
border-left: 3px solid var(--mc-amber, #ffc107);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.briefing-greeting {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
color: var(--mc-amber, #ffc107);
|
||||
letter-spacing: 0.04em;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.briefing-timestamp {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.briefing-ts-val {
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.briefing-prose {
|
||||
font-size: 1rem;
|
||||
line-height: 1.75;
|
||||
color: #dee2e6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Approval cards */
|
||||
.approval-card {
|
||||
border: 1px solid #2a3a4a;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
background: #0d1b2a;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.approval-card.approved {
|
||||
border-color: #198754;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.approval-card.rejected {
|
||||
border-color: #dc3545;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.approval-card-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: #f8f9fa;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.approval-card-desc {
|
||||
font-size: 0.85rem;
|
||||
color: #adb5bd;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.approval-card-action {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
margin-bottom: 0.75rem;
|
||||
border-left: 2px solid #495057;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.impact-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.2em 0.5em;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.impact-low { background: #198754; color: #fff; }
|
||||
.impact-medium { background: #fd7e14; color: #fff; }
|
||||
.impact-high { background: #dc3545; color: #fff; }
|
||||
|
||||
.approval-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-approve {
|
||||
background: #198754;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem 0.9rem;
|
||||
font-size: 0.82rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
cursor: pointer;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.btn-approve:hover { background: #157347; }
|
||||
|
||||
.btn-reject {
|
||||
background: transparent;
|
||||
color: #dc3545;
|
||||
border: 1px solid #dc3545;
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem 0.9rem;
|
||||
font-size: 0.82rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
cursor: pointer;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.btn-reject:hover { background: #dc354520; }
|
||||
|
||||
.no-approvals {
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
padding: 2rem 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Refresh button */
|
||||
.btn-refresh {
|
||||
background: transparent;
|
||||
color: #adb5bd;
|
||||
border: 1px solid #495057;
|
||||
border-radius: 4px;
|
||||
padding: 0.3rem 0.7rem;
|
||||
font-size: 0.75rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-refresh:hover {
|
||||
color: #f8f9fa;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.briefing-greeting { font-size: 1.3rem; }
|
||||
.briefing-prose { font-size: 0.95rem; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user