Files
timemaster/dev_session_doc.py
T
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

177 lines
5.2 KiB
Python
Raw 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_session_doc.py Dokumentiert die zuletzt abgeschlossene Timetrack-Session in DEVLOG.md.
Wird automatisch vom Claude Code Stop-Hook aufgerufen.
"""
import json
import os
import subprocess
from datetime import datetime, timezone
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")
BACKEND_DIR = os.path.join(BASE_DIR, "backend")
# ── Helpers ───────────────────────────────────────────────────────────────────
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 local_display(dt: datetime) -> str:
return dt.astimezone().strftime("%H:%M")
def local_date(dt: datetime) -> str:
return dt.astimezone().strftime("%Y-%m-%d")
def run(cmd: list[str]) -> str:
"""Run a command and return stdout; return '' on any error."""
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=15,
)
return result.stdout.strip()
except Exception:
return ""
# ── Core logic ────────────────────────────────────────────────────────────────
def get_last_completed_session(data: dict) -> dict | None:
"""Return the most recently *completed* session (end != None)."""
completed = [s for s in data.get("sessions", []) if s.get("end") is not None]
if not completed:
return None
return max(completed, key=lambda s: s["end"])
def get_commits(start_iso: str, end_iso: str) -> list[str]:
"""Return list of 'HASH message' strings for commits within the session window."""
start_dt = parse_iso(start_iso)
end_dt = parse_iso(end_iso)
# git log uses local times via --after/--before; pass UTC explicitly as ISO
after = start_dt.strftime("%Y-%m-%dT%H:%M:%SZ")
before = end_dt.strftime("%Y-%m-%dT%H:%M:%SZ")
raw = run([
"git", "-C", BACKEND_DIR,
"log", "--oneline",
f"--after={after}",
f"--before={before}",
])
if not raw:
return []
return [line for line in raw.splitlines() if line.strip()]
def get_diff_stat() -> list[str]:
"""Return lines from git diff --stat HEAD~1 HEAD."""
raw = run([
"git", "-C", BACKEND_DIR,
"diff", "--stat", "HEAD~1", "HEAD",
])
if not raw:
return []
lines = raw.splitlines()
# Drop the summary line (last line with "N files changed …")
return [l for l in lines if l.strip() and " | " in l]
def build_entry(session: dict) -> str:
start_dt = parse_iso(session["start"])
end_dt = parse_iso(session["end"])
elapsed = (end_dt - start_dt).total_seconds()
duration = format_duration(elapsed)
date_str = local_date(start_dt)
start_str = local_display(start_dt)
end_str = local_display(end_dt)
desc = session.get("description") or "Claude Code Session"
commits = get_commits(session["start"], session["end"])
diff_stat = get_diff_stat()
lines: list[str] = []
lines.append(f"## {date_str} {start_str} {end_str} ({duration})")
lines.append(f"**Beschreibung:** {desc}")
lines.append("")
# Commits section
lines.append("### Commits")
if commits:
for c in commits:
lines.append(f"- {c}")
else:
lines.append("Keine Commits in dieser Session.")
lines.append("")
# Changed files section
lines.append("### Geänderte Dateien")
if diff_stat:
for l in diff_stat:
# Normalize: " path/file | 5 ++-" → "- path/file (+5/-2)"
# Keep it simple and just relay the stat line
lines.append(f"- {l.strip()}")
else:
lines.append("Keine Änderungen ermittelbar.")
lines.append("")
lines.append("---")
lines.append("")
return "\n".join(lines)
def append_to_devlog(entry: str) -> None:
# Ensure file exists with a header
if not os.path.exists(DEVLOG):
with open(DEVLOG, "w", encoding="utf-8") as f:
f.write("# TimeMaster Dev Log\n\n")
with open(DEVLOG, "a", encoding="utf-8") as f:
f.write(entry)
# ── Entry point ───────────────────────────────────────────────────────────────
def main() -> None:
if not os.path.exists(DATA_FILE):
return # Nothing to document
with open(DATA_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
session = get_last_completed_session(data)
if not session:
return
entry = build_entry(session)
append_to_devlog(entry)
if __name__ == "__main__":
try:
main()
except Exception:
# Never crash Claude Code
pass