feat(timetracking): Complete and verify time tracking implementation [2f488f42]

Implemented a full time tracking feature. The system now displays the previously recorded time in hh:mm format when a session starts. When a work unit is completed, the invested time is automatically calculated, added to the total in Notion, and included in the status report. Various bugs were fixed during this process.
This commit is contained in:
2026-01-27 08:24:44 +00:00
parent 5f391a2caa
commit fff89ff012

View File

@@ -5,6 +5,8 @@ import re
from typing import List, Dict, Optional, Tuple
from getpass import getpass
from dotenv import load_dotenv
import argparse
import shutil
load_dotenv()
@@ -43,6 +45,7 @@ def find_database_by_title(token: str, title: str) -> Optional[str]:
except requests.exceptions.RequestException as e:
print(f"Fehler bei der Suche nach der Notion-Datenbank '{title}': {e}")
if e.response is not None:
try:
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
except:
@@ -68,6 +71,7 @@ def query_notion_database(token: str, database_id: str, filter_payload: Dict = N
return response.json().get("results", [])
except requests.exceptions.RequestException as e:
print(f"Fehler bei der Abfrage der Notion-Datenbank {database_id}: {e}")
if e.response is not None:
try:
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
except:
@@ -97,6 +101,22 @@ def get_page_property(page: Dict, prop_name: str, prop_type: str = "rich_text")
# Hier könnten weitere Typen wie 'select', 'number' etc. behandelt werden
return None
def get_page_number_property(page: Dict, prop_name: str) -> Optional[float]:
"""Extrahiert den Wert einer 'number'-Eigenschaft von einer Seite."""
prop = page.get("properties", {}).get(prop_name)
if not prop or prop.get("type") != "number":
return None
return prop.get("number")
def decimal_hours_to_hhmm(decimal_hours: float) -> str:
"""Wandelt Dezimalstunden in das Format 'HH:MM' um."""
if decimal_hours is None:
return "00:00"
hours = int(decimal_hours)
minutes = int((decimal_hours * 60) % 60)
return f"{hours:02d}:{minutes:02d}"
def get_page_content(token: str, page_id: str) -> str:
"""Ruft den gesamten Textinhalt einer Notion-Seite ab, indem es die Blöcke zusammenfügt, mit Paginierung."""
url = f"https://api.notion.com/v1/blocks/{page_id}/children"
@@ -165,6 +185,7 @@ def get_page_content(token: str, page_id: str) -> str:
return "\n".join(full_text)
except requests.exceptions.RequestException as e:
print(f"Fehler beim Abrufen des Seiteninhalts für Page-ID {page_id}: {e}")
if e.response is not None:
try:
if e.response: # Wenn eine Antwort vorhanden ist
error_details = e.response.json()
@@ -193,32 +214,26 @@ def get_database_status_options(token: str, db_id: str) -> List[str]:
print(f"Fehler beim Abrufen der Datenbank-Eigenschaften: {e}")
return []
def update_notion_task_status(token: str, task_id: str, status_value: str = "Doing") -> bool:
"""Aktualisiert den Status eines Notion-Tasks."""
print(f"\n--- Aktualisiere Status von Task '{task_id}' auf '{status_value}'... ---")
def update_notion_task_property(token: str, task_id: str, payload: Dict) -> bool:
"""Aktualisiert eine beliebige Eigenschaft eines Notion-Tasks."""
print(f"\n--- Aktualisiere Eigenschaft von Task '{task_id}'... ---")
url = f"https://api.notion.com/v1/pages/{task_id}"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Notion-Version": "2022-06-28"
}
payload = {
"properties": {
"Status": {
"status": {
"name": status_value
}
}
}
}
update_payload = {"properties": payload}
try:
response = requests.patch(url, headers=headers, json=payload)
response = requests.patch(url, headers=headers, json=update_payload)
response.raise_for_status()
print(f"✅ Task-Status erfolgreich auf '{status_value}' aktualisiert.")
print(f"✅ Task-Eigenschaft erfolgreich aktualisiert.")
return True
except requests.exceptions.RequestException as e:
print(f"❌ FEHLER beim Aktualisieren des Task-Status: {e}")
print(f"❌ FEHLER beim Aktualisieren der Task-Eigenschaft: {e}")
if e.response is not None:
try:
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
except:
@@ -269,6 +284,7 @@ def create_new_notion_task(token: str, project_id: str, tasks_db_id: str) -> Opt
return new_task
except requests.exceptions.RequestException as e:
print(f"❌ FEHLER beim Erstellen des Tasks: {e}")
if e.response is not None:
try:
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
except:
@@ -298,6 +314,7 @@ def add_comment_to_notion_task(token: str, task_id: str, comment: str) -> bool:
return True
except requests.exceptions.RequestException as e:
print(f"❌ FEHLER beim Hinzufügen des Kommentars zum Notion-Task: {e}")
if e.response is not None:
try:
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
except:
@@ -320,6 +337,7 @@ def append_blocks_to_notion_page(token: str, page_id: str, blocks: List[Dict]) -
return True
except requests.exceptions.RequestException as e:
print(f"❌ FEHLER beim Anhängen des Statusberichts an die Notion-Seite: {e}")
if e.response is not None:
try:
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
except:
@@ -496,6 +514,44 @@ def report_status_to_notion(
print("❌ FEHLER: Session-Daten unvollständig. Kann keinen Statusbericht erstellen.")
return
# Time tracking logic
session_start_time_str = session_data.get("session_start_time")
if session_start_time_str:
session_start_time = datetime.fromisoformat(session_start_time_str)
elapsed_time = datetime.now() - session_start_time
elapsed_hours = elapsed_time.total_seconds() / 3600
# Get current task page to read existing duration
# Note: This is a simplified way. A more robust solution might query the DB
# to get the page object without a separate API call if we already have it.
# For now, a direct API call is clear and ensures we have the latest data.
task_page_url = f"https://api.notion.com/v1/pages/{task_id}"
headers = {
"Authorization": f"Bearer {token}",
"Notion-Version": "2022-06-28"
}
try:
page_response = requests.get(task_page_url, headers=headers)
page_response.raise_for_status()
task_page = page_response.json()
current_duration = get_page_number_property(task_page, "Total Duration (h)") or 0.0
new_total_duration = current_duration + elapsed_hours
duration_payload = {
"Total Duration (h)": {
"number": new_total_duration
}
}
update_notion_task_property(token, task_id, duration_payload)
print(f"✅ Zeiterfassung: {elapsed_hours:.2f} Stunden zum Task hinzugefügt. Neue Gesamtdauer: {new_total_duration:.2f} Stunden.")
# Reset session start time for the next interval
save_session_info(task_id, token)
except requests.exceptions.RequestException as e:
print(f"❌ FEHLER beim Abrufen der Task-Details für die Zeiterfassung: {e}")
print(f"--- Erstelle Statusbericht für Task {task_id} ---")
# Git-Zusammenfassung generieren (immer, wenn nicht explizit überschrieben)
@@ -558,6 +614,12 @@ def report_status_to_notion(
# Kommentar zusammenstellen
report_lines = []
# Diese Zeilen werden jetzt innerhalb des Code-Blocks formatiert
# Add invested time to the report if available
if 'elapsed_hours' in locals():
elapsed_hhmm = decimal_hours_to_hhmm(elapsed_hours)
report_lines.append(f"Investierte Zeit in dieser Session: {elapsed_hhmm}")
report_lines.append(f"Neuer Status: {actual_status}")
if actual_summary:
@@ -601,7 +663,8 @@ def report_status_to_notion(
# Notion aktualisieren
append_blocks_to_notion_page(token, task_id, notion_blocks)
update_notion_task_status(token, task_id, actual_status)
status_payload = {"Status": {"status": {"name": actual_status}}}
update_notion_task_property(token, task_id, status_payload)
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"❌ FEHLER beim Lesen der Session-Informationen für Statusbericht: {e}")
@@ -612,7 +675,7 @@ def report_status_to_notion(
# --- Context Generation ---
def generate_cli_context(project_title: str, task_title: str, task_id: str, readme_path: Optional[str], task_description: Optional[str]) -> str:
def generate_cli_context(project_title: str, task_title: str, task_id: str, readme_path: Optional[str], task_description: Optional[str], total_duration: float) -> str:
"""Erstellt den reinen Kontext-String für die Gemini CLI."""
# Fallback, falls kein Pfad in Notion gesetzt ist
@@ -626,9 +689,13 @@ def generate_cli_context(project_title: str, task_title: str, task_id: str, read
f"```\n{task_description}\n```\n"
)
duration_hhmm = decimal_hours_to_hhmm(total_duration)
duration_part = f"Bisher erfasster Zeitaufwand (Notion): {duration_hhmm} Stunden.\n"
context = (
f"Ich arbeite jetzt am Projekt '{project_title}'. Der Fokus liegt auf dem Task '{task_title}'.\n"
f"{description_part}\n"
f"{duration_part}"
"Die relevanten Dateien für dieses Projekt sind wahrscheinlich:\n"
"- Die primäre Projektdokumentation: @readme.md\n"
f"- Die spezifische Dokumentation für dieses Modul: @{readme_path}\n\n"
@@ -641,20 +708,13 @@ def generate_cli_context(project_title: str, task_title: str, task_id: str, read
# Die start_gemini_cli Funktion wird entfernt, da das aufrufende Skript jetzt die Gemini CLI startet.
import shutil
import argparse
# --- Session Management ---
SESSION_DIR = ".dev_session"
SESSION_FILE_PATH = os.path.join(SESSION_DIR, "SESSION_INFO")
def save_session_info(task_id: str, token: str):
"""Speichert die Task-ID und den Token für den Git-Hook."""
"""Speichert die Task-ID, den Token und den Startzeitpunkt für den Git-Hook."""
os.makedirs(SESSION_DIR, exist_ok=True)
session_data = {
"task_id": task_id,
"token": token
"token": token,
"session_start_time": datetime.now().isoformat()
}
with open(SESSION_FILE_PATH, "w") as f:
json.dump(session_data, f)
@@ -713,7 +773,8 @@ def complete_session():
status_options = get_database_status_options(token, tasks_db_id)
if status_options:
done_status = status_options[-1]
update_notion_task_status(token, task_id, done_status)
status_payload = {"Status": {"status": {"name": done_status}}}
update_notion_task_property(token, task_id, status_payload)
except (FileNotFoundError, json.JSONDecodeError):
print("Fehler beim Lesen der Session-Informationen.")
@@ -767,8 +828,11 @@ def start_interactive_session():
task_id = selected_task["id"]
print(f"\nTask '{task_title}' ausgewählt.")
# NEU: Lade die Task-Beschreibung
# NEU: Lade die Task-Beschreibung und die bisherige Dauer
task_description = get_page_content(token, task_id)
total_duration_decimal = get_page_number_property(selected_task, "Total Duration (h)") or 0.0
total_duration_hhmm = decimal_hours_to_hhmm(total_duration_decimal)
print(f"> Bisher für diesen Task erfasst: {total_duration_hhmm} Stunden.")
# Session-Informationen für den Git-Hook speichern
save_session_info(task_id, token)
@@ -782,7 +846,8 @@ def start_interactive_session():
suggested_branch_name = f"feature/task-{task_id.split('-')[0]}-{title_slug}"
status_updated = update_notion_task_status(token, task_id, "Doing")
status_payload = {"Status": {"status": {"name": "Doing"}}}
status_updated = update_notion_task_property(token, task_id, status_payload)
if not status_updated:
print("Warnung: Notion-Task-Status konnte nicht aktualisiert werden.")
@@ -795,7 +860,7 @@ def start_interactive_session():
print("------------------------------------------------------------------")
# CLI-Kontext generieren und an stdout ausgeben, damit das Startskript ihn aufgreifen kann
cli_context = generate_cli_context(project_title, task_title, task_id, readme_path, task_description)
cli_context = generate_cli_context(project_title, task_title, task_id, readme_path, task_description, total_duration_decimal)
print("\n---GEMINI_CLI_CONTEXT_START---")
print(cli_context)
print("---GEMINI_CLI_CONTEXT_END---")