1
0

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:
Claude
2026-02-22 14:04:20 +00:00
parent 648305d65c
commit ce6077be0c
11 changed files with 1326 additions and 0 deletions

View 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>
&mdash; 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&rsquo;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&hellip;</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 %}