Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)
Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
200 lines
6.1 KiB
Python
200 lines
6.1 KiB
Python
"""Daily Digest and Weekly Retrospective generation."""
|
|
|
|
from datetime import datetime, timedelta
|
|
from zoneinfo import ZoneInfo
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import func
|
|
from app.models import Task, Event, CallItem
|
|
|
|
BERLIN_TZ = ZoneInfo("Europe/Berlin")
|
|
|
|
|
|
def generate_daily_digest(db: Session) -> dict:
|
|
"""Generate a structured daily digest.
|
|
|
|
Returns a dict with sections: greeting, today_events, top_tasks,
|
|
overdue, due_today, new_since_yesterday, stats.
|
|
"""
|
|
now = datetime.now(BERLIN_TZ)
|
|
today_start = datetime.combine(now.date(), datetime.min.time()).replace(tzinfo=BERLIN_TZ)
|
|
today_end = datetime.combine(now.date(), datetime.max.time()).replace(tzinfo=BERLIN_TZ)
|
|
yesterday = today_start - timedelta(days=1)
|
|
|
|
# Greeting based on time of day
|
|
hour = now.hour
|
|
if hour < 12:
|
|
greeting = "Good morning"
|
|
elif hour < 17:
|
|
greeting = "Good afternoon"
|
|
else:
|
|
greeting = "Good evening"
|
|
|
|
# Today's events
|
|
todays_events = db.query(Event).filter(
|
|
Event.start_time >= today_start,
|
|
Event.start_time <= today_end
|
|
).order_by(Event.start_time).all()
|
|
|
|
# Top 5 priority tasks
|
|
top_tasks = db.query(Task).filter(
|
|
Task.status == "open"
|
|
).order_by(Task.priority, Task.position).limit(5).all()
|
|
|
|
# Overdue tasks
|
|
overdue = db.query(Task).filter(
|
|
Task.status == "open",
|
|
Task.due_date != None,
|
|
Task.due_date < today_start
|
|
).order_by(Task.due_date).all()
|
|
|
|
# Due today
|
|
due_today = db.query(Task).filter(
|
|
Task.status == "open",
|
|
Task.due_date != None,
|
|
Task.due_date >= today_start,
|
|
Task.due_date <= today_end
|
|
).all()
|
|
|
|
# New tasks since yesterday
|
|
new_tasks = db.query(Task).filter(
|
|
Task.status == "open",
|
|
Task.created_at >= yesterday
|
|
).count()
|
|
|
|
# Total open
|
|
total_open = db.query(Task).filter(Task.status == "open").count()
|
|
|
|
# Pending calls
|
|
pending_calls = db.query(CallItem).filter(
|
|
CallItem.status == "pending"
|
|
).all()
|
|
|
|
priority_labels = {1: "Very High", 2: "High", 3: "Medium", 4: "Low"}
|
|
|
|
result = {
|
|
"greeting": greeting,
|
|
"date": now.strftime("%A, %B %d, %Y"),
|
|
"total_open": total_open,
|
|
"events_today": [
|
|
{
|
|
"title": e.title,
|
|
"time": f"{e.start_time.astimezone(BERLIN_TZ).strftime('%H:%M')} - {e.end_time.astimezone(BERLIN_TZ).strftime('%H:%M')}"
|
|
}
|
|
for e in todays_events
|
|
],
|
|
"top_tasks": [
|
|
{"id": t.id, "title": t.title, "priority": priority_labels.get(t.priority, "Low")}
|
|
for t in top_tasks
|
|
],
|
|
"overdue": [
|
|
{
|
|
"id": t.id,
|
|
"title": t.title,
|
|
"days_overdue": (now.date() - t.due_date.date()).days
|
|
}
|
|
for t in overdue
|
|
],
|
|
"due_today": [
|
|
{"id": t.id, "title": t.title}
|
|
for t in due_today
|
|
],
|
|
"new_since_yesterday": new_tasks,
|
|
"stats": {
|
|
"events_count": len(todays_events),
|
|
"overdue_count": len(overdue),
|
|
"due_today_count": len(due_today),
|
|
}
|
|
}
|
|
|
|
# Only include pending_calls section if there are pending calls
|
|
if pending_calls:
|
|
result["pending_calls"] = [
|
|
{"person": c.person, "reason": c.reason}
|
|
for c in pending_calls
|
|
]
|
|
|
|
return result
|
|
|
|
|
|
def generate_weekly_retro(db: Session) -> dict:
|
|
"""Generate a weekly retrospective summary.
|
|
|
|
Looks at the past 7 days: what was completed, what slipped,
|
|
what's still open, and workload trends.
|
|
"""
|
|
now = datetime.now(BERLIN_TZ)
|
|
week_ago = now - timedelta(days=7)
|
|
today_start = datetime.combine(now.date(), datetime.min.time()).replace(tzinfo=BERLIN_TZ)
|
|
|
|
# Tasks completed this week (check for status change isn't tracked,
|
|
# so we look at tasks with status=completed that exist)
|
|
completed_this_week = db.query(Task).filter(
|
|
Task.status == "completed"
|
|
).count()
|
|
|
|
# Tasks still open
|
|
total_open = db.query(Task).filter(Task.status == "open").count()
|
|
|
|
# Tasks in review
|
|
in_review = db.query(Task).filter(Task.status == "review").count()
|
|
|
|
# Overdue tasks
|
|
overdue = db.query(Task).filter(
|
|
Task.status == "open",
|
|
Task.due_date != None,
|
|
Task.due_date < today_start
|
|
).order_by(Task.due_date).all()
|
|
|
|
# Tasks created this week
|
|
created_this_week = db.query(Task).filter(
|
|
Task.created_at >= week_ago,
|
|
Task.status.notin_(["dismissed"])
|
|
).count()
|
|
|
|
# Tasks by priority
|
|
priority_breakdown = {}
|
|
for prio in [1, 2, 3, 4]:
|
|
count = db.query(Task).filter(
|
|
Task.status == "open", Task.priority == prio
|
|
).count()
|
|
priority_breakdown[prio] = count
|
|
|
|
# Tasks by origin
|
|
origin_breakdown = db.query(
|
|
Task.origin, func.count(Task.id)
|
|
).filter(Task.status == "open").group_by(Task.origin).all()
|
|
|
|
# Events this week
|
|
events_this_week = db.query(Event).filter(
|
|
Event.start_time >= week_ago,
|
|
Event.start_time <= now
|
|
).count()
|
|
|
|
priority_labels = {1: "Very High", 2: "High", 3: "Medium", 4: "Low"}
|
|
|
|
return {
|
|
"period": f"{week_ago.strftime('%b %d')} - {now.strftime('%b %d, %Y')}",
|
|
"completed": completed_this_week,
|
|
"created": created_this_week,
|
|
"total_open": total_open,
|
|
"in_review": in_review,
|
|
"overdue": [
|
|
{
|
|
"id": t.id,
|
|
"title": t.title,
|
|
"days_overdue": (now.date() - t.due_date.date()).days
|
|
}
|
|
for t in overdue
|
|
],
|
|
"priority_breakdown": {
|
|
priority_labels[k]: v for k, v in priority_breakdown.items()
|
|
},
|
|
"origin_breakdown": {
|
|
(origin or "unknown"): count for origin, count in origin_breakdown
|
|
},
|
|
"events_attended": events_this_week,
|
|
"net_change": created_this_week - completed_this_week,
|
|
"health": "growing" if created_this_week > completed_this_week + 5 else
|
|
"shrinking" if completed_this_week > created_this_week + 3 else "stable",
|
|
}
|