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
+176
View File
@@ -0,0 +1,176 @@
#!/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