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:
187
dev_session.py
187
dev_session.py
@@ -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,10 +45,11 @@ 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}")
|
||||
try:
|
||||
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
|
||||
except:
|
||||
print(f"Antwort des Servers: {e.response.text}")
|
||||
if e.response is not None:
|
||||
try:
|
||||
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
|
||||
except:
|
||||
print(f"Antwort des Servers: {e.response.text}")
|
||||
return None
|
||||
|
||||
def query_notion_database(token: str, database_id: str, filter_payload: Dict = None) -> List[Dict]:
|
||||
@@ -68,10 +71,11 @@ 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}")
|
||||
try:
|
||||
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
|
||||
except:
|
||||
print(f"Antwort des Servers: {e.response.text}")
|
||||
if e.response is not None:
|
||||
try:
|
||||
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
|
||||
except:
|
||||
print(f"Antwort des Servers: {e.response.text}")
|
||||
return []
|
||||
|
||||
def get_page_title(page: Dict) -> str:
|
||||
@@ -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,12 +185,13 @@ 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}")
|
||||
try:
|
||||
if e.response: # Wenn eine Antwort vorhanden ist
|
||||
error_details = e.response.json()
|
||||
print(f"Notion API Fehlerdetails: {json.dumps(error_details, indent=2)}")
|
||||
except json.JSONDecodeError:
|
||||
print(f"Notion API Rohantwort: {e.response.text}")
|
||||
if e.response is not None:
|
||||
try:
|
||||
if e.response: # Wenn eine Antwort vorhanden ist
|
||||
error_details = e.response.json()
|
||||
print(f"Notion API Fehlerdetails: {json.dumps(error_details, indent=2)}")
|
||||
except json.JSONDecodeError:
|
||||
print(f"Notion API Rohantwort: {e.response.text}")
|
||||
return ""
|
||||
|
||||
|
||||
@@ -193,36 +214,30 @@ 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}")
|
||||
try:
|
||||
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
|
||||
except:
|
||||
print(f"Antwort des Servers: {e.response.text}")
|
||||
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:
|
||||
print(f"Antwort des Servers: {e.response.text}")
|
||||
return False
|
||||
|
||||
def create_new_notion_task(token: str, project_id: str, tasks_db_id: str) -> Optional[Dict]:
|
||||
@@ -269,10 +284,11 @@ 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}")
|
||||
try:
|
||||
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
|
||||
except:
|
||||
print(f"Antwort des Servers: {e.response.text}")
|
||||
if e.response is not None:
|
||||
try:
|
||||
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
|
||||
except:
|
||||
print(f"Antwort des Servers: {e.response.text}")
|
||||
return None
|
||||
|
||||
def add_comment_to_notion_task(token: str, task_id: str, comment: str) -> bool:
|
||||
@@ -298,10 +314,11 @@ 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}")
|
||||
try:
|
||||
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
|
||||
except:
|
||||
print(f"Antwort des Servers: {e.response.text}")
|
||||
if e.response is not None:
|
||||
try:
|
||||
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
|
||||
except:
|
||||
print(f"Antwort des Servers: {e.response.text}")
|
||||
return False
|
||||
|
||||
def append_blocks_to_notion_page(token: str, page_id: str, blocks: List[Dict]) -> bool:
|
||||
@@ -320,10 +337,11 @@ 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}")
|
||||
try:
|
||||
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
|
||||
except:
|
||||
print(f"Antwort des Servers: {e.response.text}")
|
||||
if e.response is not None:
|
||||
try:
|
||||
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
|
||||
except:
|
||||
print(f"Antwort des Servers: {e.response.text}")
|
||||
return False
|
||||
|
||||
# --- Session Management ---
|
||||
@@ -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---")
|
||||
|
||||
Reference in New Issue
Block a user