#!/usr/bin/env python3 """ dev_weekly.py – Wochenbericht: Gesamtstunden der aktuellen Woche + letzte DEVLOG-Einträge. Aufruf: python3 dev_weekly.py """ import json import os import re from datetime import datetime, timezone, timedelta BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_FILE = os.path.expanduser("~/.claude/timetrack.json") DEVLOG = os.path.join(BASE_DIR, "DEVLOG.md") # ANSI colors RESET = "\033[0m" BOLD = "\033[1m" DIM = "\033[2m" RED = "\033[91m" GREEN = "\033[92m" YELLOW = "\033[93m" BLUE = "\033[94m" MAGENTA = "\033[95m" CYAN = "\033[96m" WHITE = "\033[97m" def c(*codes: str) -> str: """Return ANSI prefix string.""" return "".join(codes) def colorize(text: str, *codes: str) -> str: return "".join(codes) + text + RESET def parse_iso(s: str) -> datetime: if s.endswith("Z"): s = s[:-1] + "+00:00" return datetime.fromisoformat(s) def format_duration(seconds: float) -> str: seconds = int(seconds) h = seconds // 3600 m = (seconds % 3600) // 60 if h > 0: return f"{h}h {m:02d}m" return f"{m}m" def current_week_range() -> tuple[datetime, datetime]: """Return (monday_00:00 local, sunday_23:59 local) for the current ISO week.""" now = datetime.now(timezone.utc).astimezone() monday = (now - timedelta(days=now.weekday())).replace( hour=0, minute=0, second=0, microsecond=0 ) sunday_end = monday + timedelta(days=7) return monday, sunday_end def weekly_stats(data: dict) -> tuple[float, int, dict[str, float]]: """Return (total_seconds, session_count, {date: seconds}) for this week.""" week_start, week_end = current_week_range() total = 0.0 count = 0 per_day: dict[str, float] = {} for s in data.get("sessions", []): start_dt = parse_iso(s["start"]) loc = start_dt.astimezone() if not (week_start <= loc < week_end): continue if s.get("end") is not None: end_dt = parse_iso(s["end"]) elapsed = (end_dt - start_dt).total_seconds() else: elapsed = (datetime.now(timezone.utc) - start_dt).total_seconds() day_key = loc.strftime("%Y-%m-%d") per_day[day_key] = per_day.get(day_key, 0.0) + elapsed total += elapsed count += 1 return total, count, per_day def get_last_devlog_entries(n: int = 5) -> list[str]: """ Parse DEVLOG.md and return the last n section blocks (everything between ## headings). """ if not os.path.exists(DEVLOG): return [] with open(DEVLOG, "r", encoding="utf-8") as f: content = f.read() # Split on ## headings (session entries) parts = re.split(r"(?=^## )", content, flags=re.MULTILINE) # Filter out non-entry parts (header, blanks) entries = [p.strip() for p in parts if p.strip().startswith("## ")] return entries[-n:] def print_separator(char: str = "─", width: int = 60) -> None: print(colorize(char * width, DIM)) def print_weekly_report() -> None: if not os.path.exists(DATA_FILE): print(colorize("Keine Timetrack-Daten gefunden.", DIM)) return with open(DATA_FILE, "r", encoding="utf-8") as f: data = json.load(f) total_sec, count, per_day = weekly_stats(data) week_start, _ = current_week_range() week_num = week_start.isocalendar()[1] week_label = ( f"KW {week_num} " f"({week_start.strftime('%d.%m.')} – " f"{(week_start + timedelta(days=6)).strftime('%d.%m.%Y')})" ) print() print(colorize(f" TimeMaster – Wochenbericht {week_label}", BOLD, BLUE)) print_separator() if not per_day: print(colorize(" Keine Sessions diese Woche.", DIM)) else: # Day-by-day breakdown DAYS_DE = { 0: "Mo", 1: "Di", 2: "Mi", 3: "Do", 4: "Fr", 5: "Sa", 6: "So" } for day_str in sorted(per_day): dt = datetime.fromisoformat(day_str) day_name = DAYS_DE.get(dt.weekday(), "??") dur = format_duration(per_day[day_str]) print( f" {colorize(day_name, WHITE, BOLD)} " f"{colorize(day_str, DIM)} " f"{colorize(dur, YELLOW)}" ) print_separator() print( f" {'Gesamt':.<20} " + colorize(format_duration(total_sec), CYAN, BOLD) + colorize(f" ({count} Sessions)", DIM) ) print() print_separator("═") print(colorize(" Letzte Einträge im Dev Log", BOLD, MAGENTA)) print_separator("═") entries = get_last_devlog_entries(5) if not entries: print(colorize(" Keine Einträge in DEVLOG.md.", DIM)) else: for entry in entries: lines = entry.splitlines() if not lines: continue # Header line (## date time – time (dur)) header = lines[0] print() print(colorize(f" {header}", BOLD, GREEN)) # Print remaining lines, indented, max 8 lines to keep it compact body_lines = [l for l in lines[1:] if l.strip()][:8] for bl in body_lines: # Dim section headers, normal for bullets if bl.startswith("###"): print(colorize(f" {bl}", DIM)) elif bl.startswith("-"): print(f" {colorize(bl, WHITE)}") elif bl.startswith("**"): print(f" {colorize(bl, CYAN)}") else: print(f" {colorize(bl, DIM)}") print() print_separator() print() if __name__ == "__main__": try: print_weekly_report() except Exception as e: print(f"Fehler: {e}")