1
0
This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/src/infrastructure/notifications/push.py

150 lines
4.8 KiB
Python
Raw Normal View History

"""Push notification system for swarm events.
Collects notifications from swarm events (task completed, agent joined,
auction won, etc.) and makes them available to the dashboard via polling
or WebSocket. On macOS, can optionally trigger native notifications
via osascript.
No cloud push services everything stays local.
"""
import logging
import platform
import subprocess
from collections import deque
ruff (#169) * polish: streamline nav, extract inline styles, improve tablet UX - Restructure desktop nav from 8+ flat links + overflow dropdown into 5 grouped dropdowns (Core, Agents, Intel, System, More) matching the mobile menu structure to reduce decision fatigue - Extract all inline styles from mission_control.html and base.html notification elements into mission-control.css with semantic classes - Replace JS-built innerHTML with secure DOM construction in notification loader and chat history - Add CONNECTING state to connection indicator (amber) instead of showing OFFLINE before WebSocket connects - Add tablet breakpoint (1024px) with larger touch targets for Apple Pencil / stylus use and safe-area padding for iPad toolbar - Add active-link highlighting in desktop dropdown menus - Rename "Mission Control" page title to "System Overview" to disambiguate from the chat home page - Add "Home — Timmy Time" page title to index.html https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * fix(security): move auth-gate credentials to environment variables Hardcoded username, password, and HMAC secret in auth-gate.py replaced with os.environ lookups. Startup now refuses to run if any variable is unset. Added AUTH_GATE_SECRET/USER/PASS to .env.example. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * refactor(tooling): migrate from black+isort+bandit to ruff Replace three separate linting/formatting tools with a single ruff invocation. Updates tox.ini (lint, format, pre-push, pre-commit envs), .pre-commit-config.yaml, and CI workflow. Fixes all ruff errors including unused imports, missing raise-from, and undefined names. Ruff config maps existing bandit skips to equivalent S-rules. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 12:23:35 -04:00
from collections.abc import Callable
from dataclasses import dataclass, field
ruff (#169) * polish: streamline nav, extract inline styles, improve tablet UX - Restructure desktop nav from 8+ flat links + overflow dropdown into 5 grouped dropdowns (Core, Agents, Intel, System, More) matching the mobile menu structure to reduce decision fatigue - Extract all inline styles from mission_control.html and base.html notification elements into mission-control.css with semantic classes - Replace JS-built innerHTML with secure DOM construction in notification loader and chat history - Add CONNECTING state to connection indicator (amber) instead of showing OFFLINE before WebSocket connects - Add tablet breakpoint (1024px) with larger touch targets for Apple Pencil / stylus use and safe-area padding for iPad toolbar - Add active-link highlighting in desktop dropdown menus - Rename "Mission Control" page title to "System Overview" to disambiguate from the chat home page - Add "Home — Timmy Time" page title to index.html https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * fix(security): move auth-gate credentials to environment variables Hardcoded username, password, and HMAC secret in auth-gate.py replaced with os.environ lookups. Startup now refuses to run if any variable is unset. Added AUTH_GATE_SECRET/USER/PASS to .env.example. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * refactor(tooling): migrate from black+isort+bandit to ruff Replace three separate linting/formatting tools with a single ruff invocation. Updates tox.ini (lint, format, pre-push, pre-commit envs), .pre-commit-config.yaml, and CI workflow. Fixes all ruff errors including unused imports, missing raise-from, and undefined names. Ruff config maps existing bandit skips to equivalent S-rules. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 12:23:35 -04:00
from datetime import UTC, datetime
logger = logging.getLogger(__name__)
@dataclass
class Notification:
id: int
title: str
message: str
category: str # swarm | task | agent | system | payment
ruff (#169) * polish: streamline nav, extract inline styles, improve tablet UX - Restructure desktop nav from 8+ flat links + overflow dropdown into 5 grouped dropdowns (Core, Agents, Intel, System, More) matching the mobile menu structure to reduce decision fatigue - Extract all inline styles from mission_control.html and base.html notification elements into mission-control.css with semantic classes - Replace JS-built innerHTML with secure DOM construction in notification loader and chat history - Add CONNECTING state to connection indicator (amber) instead of showing OFFLINE before WebSocket connects - Add tablet breakpoint (1024px) with larger touch targets for Apple Pencil / stylus use and safe-area padding for iPad toolbar - Add active-link highlighting in desktop dropdown menus - Rename "Mission Control" page title to "System Overview" to disambiguate from the chat home page - Add "Home — Timmy Time" page title to index.html https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * fix(security): move auth-gate credentials to environment variables Hardcoded username, password, and HMAC secret in auth-gate.py replaced with os.environ lookups. Startup now refuses to run if any variable is unset. Added AUTH_GATE_SECRET/USER/PASS to .env.example. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * refactor(tooling): migrate from black+isort+bandit to ruff Replace three separate linting/formatting tools with a single ruff invocation. Updates tox.ini (lint, format, pre-push, pre-commit envs), .pre-commit-config.yaml, and CI workflow. Fixes all ruff errors including unused imports, missing raise-from, and undefined names. Ruff config maps existing bandit skips to equivalent S-rules. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 12:23:35 -04:00
timestamp: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
read: bool = False
class PushNotifier:
"""Local push notification manager."""
def __init__(self, max_history: int = 200, native_enabled: bool = True) -> None:
self._notifications: deque[Notification] = deque(maxlen=max_history)
self._counter = 0
self._native_enabled = native_enabled and platform.system() == "Darwin"
self._listeners: list = []
def notify(
self,
title: str,
message: str,
category: str = "system",
native: bool = False,
) -> Notification:
"""Create and store a notification."""
self._counter += 1
notif = Notification(
id=self._counter,
title=title,
message=message,
category=category,
)
self._notifications.appendleft(notif)
logger.info("Notification [%s]: %s%s", category, title, message[:60])
# Trigger native macOS notification if requested
if native and self._native_enabled:
self._native_notify(title, message)
# Notify listeners (for WebSocket push)
for listener in self._listeners:
try:
listener(notif)
except Exception as exc:
logger.error("Notification listener error: %s", exc)
return notif
def _native_notify(self, title: str, message: str) -> None:
"""Send a native macOS notification via osascript."""
try:
safe_message = message.replace("\\", "\\\\").replace('"', '\\"')
safe_title = title.replace("\\", "\\\\").replace('"', '\\"')
script = (
f'display notification "{safe_message}" '
f'with title "Agent Dashboard" subtitle "{safe_title}"'
)
subprocess.Popen(
["osascript", "-e", script],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except Exception as exc:
logger.debug("Native notification failed: %s", exc)
ruff (#169) * polish: streamline nav, extract inline styles, improve tablet UX - Restructure desktop nav from 8+ flat links + overflow dropdown into 5 grouped dropdowns (Core, Agents, Intel, System, More) matching the mobile menu structure to reduce decision fatigue - Extract all inline styles from mission_control.html and base.html notification elements into mission-control.css with semantic classes - Replace JS-built innerHTML with secure DOM construction in notification loader and chat history - Add CONNECTING state to connection indicator (amber) instead of showing OFFLINE before WebSocket connects - Add tablet breakpoint (1024px) with larger touch targets for Apple Pencil / stylus use and safe-area padding for iPad toolbar - Add active-link highlighting in desktop dropdown menus - Rename "Mission Control" page title to "System Overview" to disambiguate from the chat home page - Add "Home — Timmy Time" page title to index.html https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * fix(security): move auth-gate credentials to environment variables Hardcoded username, password, and HMAC secret in auth-gate.py replaced with os.environ lookups. Startup now refuses to run if any variable is unset. Added AUTH_GATE_SECRET/USER/PASS to .env.example. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * refactor(tooling): migrate from black+isort+bandit to ruff Replace three separate linting/formatting tools with a single ruff invocation. Updates tox.ini (lint, format, pre-push, pre-commit envs), .pre-commit-config.yaml, and CI workflow. Fixes all ruff errors including unused imports, missing raise-from, and undefined names. Ruff config maps existing bandit skips to equivalent S-rules. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 12:23:35 -04:00
def recent(self, limit: int = 20, category: str | None = None) -> list[Notification]:
"""Get recent notifications, optionally filtered by category."""
notifs = list(self._notifications)
if category:
notifs = [n for n in notifs if n.category == category]
return notifs[:limit]
def unread_count(self) -> int:
return sum(1 for n in self._notifications if not n.read)
def mark_read(self, notification_id: int) -> bool:
for n in self._notifications:
if n.id == notification_id:
n.read = True
return True
return False
def mark_all_read(self) -> int:
count = 0
for n in self._notifications:
if not n.read:
n.read = True
count += 1
return count
def clear(self) -> None:
self._notifications.clear()
ruff (#169) * polish: streamline nav, extract inline styles, improve tablet UX - Restructure desktop nav from 8+ flat links + overflow dropdown into 5 grouped dropdowns (Core, Agents, Intel, System, More) matching the mobile menu structure to reduce decision fatigue - Extract all inline styles from mission_control.html and base.html notification elements into mission-control.css with semantic classes - Replace JS-built innerHTML with secure DOM construction in notification loader and chat history - Add CONNECTING state to connection indicator (amber) instead of showing OFFLINE before WebSocket connects - Add tablet breakpoint (1024px) with larger touch targets for Apple Pencil / stylus use and safe-area padding for iPad toolbar - Add active-link highlighting in desktop dropdown menus - Rename "Mission Control" page title to "System Overview" to disambiguate from the chat home page - Add "Home — Timmy Time" page title to index.html https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * fix(security): move auth-gate credentials to environment variables Hardcoded username, password, and HMAC secret in auth-gate.py replaced with os.environ lookups. Startup now refuses to run if any variable is unset. Added AUTH_GATE_SECRET/USER/PASS to .env.example. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * refactor(tooling): migrate from black+isort+bandit to ruff Replace three separate linting/formatting tools with a single ruff invocation. Updates tox.ini (lint, format, pre-push, pre-commit envs), .pre-commit-config.yaml, and CI workflow. Fixes all ruff errors including unused imports, missing raise-from, and undefined names. Ruff config maps existing bandit skips to equivalent S-rules. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 12:23:35 -04:00
def add_listener(self, callback: Callable[[Notification], None]) -> None:
"""Register a callback for real-time notification delivery."""
self._listeners.append(callback)
# Module-level singleton
notifier = PushNotifier()
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
2026-02-22 14:04:20 +00:00
async def notify_briefing_ready(briefing) -> None:
"""Notify the owner that a new morning briefing is ready.
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
2026-02-22 14:04:20 +00:00
Only triggers a native macOS popup when there are pending approval items.
Briefings with 0 approvals are still logged but don't interrupt the user
with a notification that leads to an empty-looking page.
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
2026-02-22 14:04:20 +00:00
Args:
briefing: A timmy.briefing.Briefing instance.
"""
n_approvals = len(briefing.approval_items) if briefing.approval_items else 0
if n_approvals == 0:
logger.info("Briefing ready but no pending approvals — skipping native notification")
return
ruff (#169) * polish: streamline nav, extract inline styles, improve tablet UX - Restructure desktop nav from 8+ flat links + overflow dropdown into 5 grouped dropdowns (Core, Agents, Intel, System, More) matching the mobile menu structure to reduce decision fatigue - Extract all inline styles from mission_control.html and base.html notification elements into mission-control.css with semantic classes - Replace JS-built innerHTML with secure DOM construction in notification loader and chat history - Add CONNECTING state to connection indicator (amber) instead of showing OFFLINE before WebSocket connects - Add tablet breakpoint (1024px) with larger touch targets for Apple Pencil / stylus use and safe-area padding for iPad toolbar - Add active-link highlighting in desktop dropdown menus - Rename "Mission Control" page title to "System Overview" to disambiguate from the chat home page - Add "Home — Timmy Time" page title to index.html https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * fix(security): move auth-gate credentials to environment variables Hardcoded username, password, and HMAC secret in auth-gate.py replaced with os.environ lookups. Startup now refuses to run if any variable is unset. Added AUTH_GATE_SECRET/USER/PASS to .env.example. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * refactor(tooling): migrate from black+isort+bandit to ruff Replace three separate linting/formatting tools with a single ruff invocation. Updates tox.ini (lint, format, pre-push, pre-commit envs), .pre-commit-config.yaml, and CI workflow. Fixes all ruff errors including unused imports, missing raise-from, and undefined names. Ruff config maps existing bandit skips to equivalent S-rules. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 12:23:35 -04:00
message = f"Your morning briefing is ready. {n_approvals} item(s) await your approval."
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
2026-02-22 14:04:20 +00:00
notifier.notify(
title="Morning Briefing Ready",
message=message,
category="briefing",
native=True,
)
logger.info("Briefing push notification dispatched (%d approval(s))", n_approvals)