Files
sysops 1fedd683e0 Initial commit – TimeMaster Zeiterfassung & HR-Tool
Stand: agent-06 (Audit-Log), agent-05 (Krankmeldung), agent-07 Phase 1 (Personalnummer),
Busylight-Pull-Integration, TOTP/2FA, Abwesenheiten, Zeiterfassung, Kiosk-Grundgerüst.
Migrations 0001–0023 deployed auf 192.168.1.137 + .164.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:27 +02:00

195 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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}")