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>
This commit is contained in:
sysops
2026-05-23 20:03:27 +02:00
commit 1fedd683e0
178 changed files with 29896 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
#!/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}")