feat(timetracking): Implement automatic time tracking for dev sessions
This commit introduces a time tracking feature for development sessions managed by . Key changes: - **Extended Notion Helpers:** Added new functions (get_page_number_property, get_page_date_property) to read numerical and date values from Notion. Refactored the status update function to a generic to handle various field types. - **Session Start Time:** A timestamp (session_start_time) is now saved in the file when a session starts. - **Automatic Time Calculation:** The time elapsed since is calculated when the command is executed. - **Incremental Duration:** The calculated duration is added to the existing 'Total Duration h' property of the Notion task, and the result is written back to Notion. - **Continuous Tracking:** The is reset to the current time after each report to accurately track the next work unit in the same session. Fixes [2f488f42]
This commit is contained in:
@@ -1 +1 @@
|
|||||||
{"task_id": "2f488f42-8544-819a-8407-f29748b3e0b8", "token": "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"}
|
{"task_id": "2f488f42-8544-81ac-a9f8-e373c4c18115", "token": "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", "session_start_time": "2026-01-26T18:39:11.157549"}
|
||||||
170
dev_session.py
170
dev_session.py
@@ -1,10 +1,14 @@
|
|||||||
|
import re
|
||||||
|
from typing import List, Dict, Optional, Tuple, Any
|
||||||
|
from getpass import getpass
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime, timedelta
|
||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
import re
|
import argparse
|
||||||
from typing import List, Dict, Optional, Tuple
|
import shutil
|
||||||
from getpass import getpass
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@@ -83,18 +87,48 @@ def get_page_title(page: Dict) -> str:
|
|||||||
return title_parts[0].get("plain_text", "Unbenannt")
|
return title_parts[0].get("plain_text", "Unbenannt")
|
||||||
return "Unbenannt"
|
return "Unbenannt"
|
||||||
|
|
||||||
def get_page_property(page: Dict, prop_name: str, prop_type: str = "rich_text") -> Optional[str]:
|
def get_page_rich_text_property(page: Dict, prop_name: str) -> Optional[str]:
|
||||||
"""Extrahiert den Inhalt einer bestimmten Eigenschaft (Property) von einer Seite."""
|
"""Extrahiert den Inhalt einer bestimmten Eigenschaft (Property) von einer Seite."""
|
||||||
prop = page.get("properties", {}).get(prop_name)
|
prop = page.get("properties", {}).get(prop_name)
|
||||||
if not prop:
|
if not prop:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if prop_type == "rich_text" and prop.get("type") == "rich_text":
|
if prop.get("type") == "rich_text":
|
||||||
text_parts = prop.get("rich_text", [])
|
text_parts = prop.get("rich_text", [])
|
||||||
if text_parts:
|
if text_parts:
|
||||||
return text_parts[0].get("plain_text")
|
return text_parts[0].get("plain_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 Inhalt einer Number-Eigenschaft von einer Seite."""
|
||||||
|
prop = page.get("properties", {}).get(prop_name)
|
||||||
|
if not prop:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if prop.get("type") == "number":
|
||||||
|
return prop.get("number")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_page_date_property(page: Dict, prop_name: str) -> Optional[datetime]:
|
||||||
|
"""Extrahiert den Inhalt einer Date-Eigenschaft von einer Seite."""
|
||||||
|
prop = page.get("properties", {}).get(prop_name)
|
||||||
|
if not prop:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if prop.get("type") == "date":
|
||||||
|
date_info = prop.get("date")
|
||||||
|
if date_info and date_info.get("start"):
|
||||||
|
try:
|
||||||
|
# Notion gibt Datumszeiten im ISO 8601 Format zurück (z.B. '2023-10-27T10:00:00.000+00:00')
|
||||||
|
# oder nur Datum (z.B. '2023-10-27')
|
||||||
|
# datetime.fromisoformat kann beides verarbeiten, aber Zeitzonen können komplex sein.
|
||||||
|
# Für unsere Zwecke reicht es, wenn es als UTC betrachtet wird und wir die Dauer berechnen.
|
||||||
|
return datetime.fromisoformat(date_info["start"].replace('Z', '+00:00'))
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_page_content(token: str, page_id: str) -> str:
|
def get_page_content(token: str, page_id: str) -> str:
|
||||||
@@ -193,32 +227,42 @@ def get_database_status_options(token: str, db_id: str) -> List[str]:
|
|||||||
print(f"Fehler beim Abrufen der Datenbank-Eigenschaften: {e}")
|
print(f"Fehler beim Abrufen der Datenbank-Eigenschaften: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def update_notion_task_status(token: str, task_id: str, status_value: str = "Doing") -> bool:
|
def update_notion_task_property(token: str, task_id: str, prop_name: str, prop_value: Any, prop_type: str) -> bool:
|
||||||
"""Aktualisiert den Status eines Notion-Tasks."""
|
"""Aktualisiert eine bestimmte Eigenschaft (Property) eines Notion-Tasks."""
|
||||||
print(f"\n--- Aktualisiere Status von Task '{task_id}' auf '{status_value}'... ---")
|
print(f"\n--- Aktualisiere Task '{task_id}', Eigenschaft '{prop_name}' ({prop_type}) mit Wert '{prop_value}'... ---")
|
||||||
url = f"https://api.notion.com/v1/pages/{task_id}"
|
url = f"https://api.notion.com/v1/pages/{task_id}"
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {token}",
|
"Authorization": f"Bearer {token}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Notion-Version": "2022-06-28"
|
"Notion-Version": "2022-06-28"
|
||||||
}
|
}
|
||||||
payload = {
|
|
||||||
"properties": {
|
payload_properties = {}
|
||||||
"Status": {
|
if prop_type == "status":
|
||||||
"status": {
|
payload_properties[prop_name] = {"status": {"name": prop_value}}
|
||||||
"name": status_value
|
elif prop_type == "number":
|
||||||
}
|
payload_properties[prop_name] = {"number": prop_value}
|
||||||
}
|
elif prop_type == "date":
|
||||||
}
|
# Notion erwartet Datum im ISO 8601 Format
|
||||||
}
|
if isinstance(prop_value, datetime):
|
||||||
|
payload_properties[prop_name] = {"date": {"start": prop_value.isoformat()}}
|
||||||
|
else:
|
||||||
|
print(f"❌ FEHLER: Ungültiges Datumformat für '{prop_name}'. Erwartet datetime-Objekt.")
|
||||||
|
return False
|
||||||
|
# Weitere Typen können hier hinzugefügt werden (z.B. rich_text, multi_select etc.)
|
||||||
|
else:
|
||||||
|
print(f"❌ FEHLER: Nicht unterstützter Eigenschaftstyp '{prop_type}' für Update.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
payload = {"properties": payload_properties}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.patch(url, headers=headers, json=payload)
|
response = requests.patch(url, headers=headers, json=payload)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
print(f"✅ Task-Status erfolgreich auf '{status_value}' aktualisiert.")
|
print(f"✅ Task-Eigenschaft '{prop_name}' erfolgreich aktualisiert.")
|
||||||
return True
|
return True
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
print(f"❌ FEHLER beim Aktualisieren des Task-Status: {e}")
|
print(f"❌ FEHLER beim Aktualisieren der Task-Eigenschaft '{prop_name}': {e}")
|
||||||
try:
|
try:
|
||||||
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
|
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
|
||||||
except:
|
except:
|
||||||
@@ -395,7 +439,7 @@ def select_project(token: str) -> Optional[Tuple[Dict, Optional[str]]]:
|
|||||||
choice = int(input("Bitte wähle eine Nummer: "))
|
choice = int(input("Bitte wähle eine Nummer: "))
|
||||||
if 1 <= choice <= len(projects):
|
if 1 <= choice <= len(projects):
|
||||||
selected_project = projects[choice - 1]
|
selected_project = projects[choice - 1]
|
||||||
readme_path = get_page_property(selected_project, "Readme Path")
|
readme_path = get_page_rich_text_property(selected_project, "Readme Path")
|
||||||
return selected_project, readme_path
|
return selected_project, readme_path
|
||||||
else:
|
else:
|
||||||
print("Ungültige Auswahl.")
|
print("Ungültige Auswahl.")
|
||||||
@@ -491,13 +535,68 @@ def report_status_to_notion(
|
|||||||
session_data = json.load(f)
|
session_data = json.load(f)
|
||||||
task_id = session_data.get("task_id")
|
task_id = session_data.get("task_id")
|
||||||
token = session_data.get("token")
|
token = session_data.get("token")
|
||||||
|
session_start_time_str = session_data.get("session_start_time")
|
||||||
|
|
||||||
if not (task_id and token):
|
if not (task_id and token and session_start_time_str):
|
||||||
print("❌ FEHLER: Session-Daten unvollständig. Kann keinen Statusbericht erstellen.")
|
print("❌ FEHLER: Session-Daten unvollständig oder Startzeit fehlt. Kann keinen Statusbericht erstellen.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
session_start_time = datetime.fromisoformat(session_start_time_str)
|
||||||
|
except ValueError:
|
||||||
|
print(f"❌ FEHLER: Ungültiges Startzeitformat in Session-Daten: {session_start_time_str}")
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f"--- Erstelle Statusbericht für Task {task_id} ---")
|
print(f"--- Erstelle Statusbericht für Task {task_id} ---")
|
||||||
|
|
||||||
|
tasks_db_id = find_database_by_title(token, "Tasks [UT]")
|
||||||
|
if not tasks_db_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 1. Aktuelles Task-Objekt abrufen, um die vorhandene Dauer zu lesen
|
||||||
|
page_url = f"https://api.notion.com/v1/pages/{task_id}"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Notion-Version": "2022-06-28"
|
||||||
|
}
|
||||||
|
current_task_page = None
|
||||||
|
try:
|
||||||
|
response = requests.get(page_url, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
current_task_page = response.json()
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"❌ FEHLER beim Abrufen der Task-Seite {task_id}: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not current_task_page:
|
||||||
|
print(f"❌ FEHLER: Konnte den Task {task_id} in Notion nicht finden. Zeiterfassung wird übersprungen.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. Bestehende Total Duration (h) abrufen
|
||||||
|
existing_duration = get_page_number_property(current_task_page, "Total Duration (h)")
|
||||||
|
if existing_duration is None:
|
||||||
|
existing_duration = 0.0
|
||||||
|
|
||||||
|
# 3. Dauer der aktuellen Arbeitseinheit berechnen
|
||||||
|
current_end_time = datetime.now()
|
||||||
|
time_spent = (current_end_time - session_start_time).total_seconds() / 3600.0 # Dauer in Stunden
|
||||||
|
|
||||||
|
# 4. Neue Gesamtdauer berechnen
|
||||||
|
new_total_duration = existing_duration + time_spent
|
||||||
|
|
||||||
|
# 5. Total Duration (h) in Notion aktualisieren
|
||||||
|
update_notion_task_property(token, task_id, "Total Duration (h)", round(new_total_duration, 2), "number")
|
||||||
|
|
||||||
|
# 6. session_start_time in SESSION_INFO für die nächste Arbeitseinheit aktualisieren
|
||||||
|
# Hier speichern wir die Endzeit als neue Startzeit für die nächste mögliche Einheit
|
||||||
|
# oder löschen sie, wenn die Session als beendet betrachtet wird.
|
||||||
|
# Entsprechend des Plans setzen wir sie zurück (auf die aktuelle Endzeit der Arbeitseinheit).
|
||||||
|
session_data["session_start_time"] = current_end_time.isoformat()
|
||||||
|
with open(SESSION_FILE_PATH, "w") as f:
|
||||||
|
json.dump(session_data, f)
|
||||||
|
|
||||||
|
|
||||||
# Git-Zusammenfassung generieren (immer, wenn nicht explizit überschrieben)
|
# Git-Zusammenfassung generieren (immer, wenn nicht explizit überschrieben)
|
||||||
actual_git_changes = git_changes_override
|
actual_git_changes = git_changes_override
|
||||||
actual_commit_messages = commit_messages_override
|
actual_commit_messages = commit_messages_override
|
||||||
@@ -601,7 +700,7 @@ def report_status_to_notion(
|
|||||||
|
|
||||||
# Notion aktualisieren
|
# Notion aktualisieren
|
||||||
append_blocks_to_notion_page(token, task_id, notion_blocks)
|
append_blocks_to_notion_page(token, task_id, notion_blocks)
|
||||||
update_notion_task_status(token, task_id, actual_status)
|
update_notion_task_property(token, task_id, "Status", actual_status, "status")
|
||||||
|
|
||||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||||
print(f"❌ FEHLER beim Lesen der Session-Informationen für Statusbericht: {e}")
|
print(f"❌ FEHLER beim Lesen der Session-Informationen für Statusbericht: {e}")
|
||||||
@@ -641,20 +740,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.
|
# Die start_gemini_cli Funktion wird entfernt, da das aufrufende Skript jetzt die Gemini CLI startet.
|
||||||
|
|
||||||
import shutil
|
def save_session_info(task_id: str, token: str, session_start_time: datetime):
|
||||||
import argparse
|
"""Speichert die Task-ID, den Token und die Startzeit der Session."""
|
||||||
|
|
||||||
# --- 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."""
|
|
||||||
os.makedirs(SESSION_DIR, exist_ok=True)
|
os.makedirs(SESSION_DIR, exist_ok=True)
|
||||||
session_data = {
|
session_data = {
|
||||||
"task_id": task_id,
|
"task_id": task_id,
|
||||||
"token": token
|
"token": token,
|
||||||
|
"session_start_time": session_start_time.isoformat() # Speichern als ISO-Format String
|
||||||
}
|
}
|
||||||
with open(SESSION_FILE_PATH, "w") as f:
|
with open(SESSION_FILE_PATH, "w") as f:
|
||||||
json.dump(session_data, f)
|
json.dump(session_data, f)
|
||||||
@@ -713,7 +805,7 @@ def complete_session():
|
|||||||
status_options = get_database_status_options(token, tasks_db_id)
|
status_options = get_database_status_options(token, tasks_db_id)
|
||||||
if status_options:
|
if status_options:
|
||||||
done_status = status_options[-1]
|
done_status = status_options[-1]
|
||||||
update_notion_task_status(token, task_id, done_status)
|
update_notion_task_property(token, task_id, "Status", done_status, "status")
|
||||||
|
|
||||||
except (FileNotFoundError, json.JSONDecodeError):
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
print("Fehler beim Lesen der Session-Informationen.")
|
print("Fehler beim Lesen der Session-Informationen.")
|
||||||
@@ -771,7 +863,7 @@ def start_interactive_session():
|
|||||||
task_description = get_page_content(token, task_id)
|
task_description = get_page_content(token, task_id)
|
||||||
|
|
||||||
# Session-Informationen für den Git-Hook speichern
|
# Session-Informationen für den Git-Hook speichern
|
||||||
save_session_info(task_id, token)
|
save_session_info(task_id, token, datetime.now())
|
||||||
|
|
||||||
# Git-Hook installieren, der die Session-Infos nutzt
|
# Git-Hook installieren, der die Session-Infos nutzt
|
||||||
install_git_hook()
|
install_git_hook()
|
||||||
@@ -782,7 +874,7 @@ def start_interactive_session():
|
|||||||
|
|
||||||
suggested_branch_name = f"feature/task-{task_id.split('-')[0]}-{title_slug}"
|
suggested_branch_name = f"feature/task-{task_id.split('-')[0]}-{title_slug}"
|
||||||
|
|
||||||
status_updated = update_notion_task_status(token, task_id, "Doing")
|
status_updated = update_notion_task_property(token, task_id, "Status", "Doing", "status")
|
||||||
if not status_updated:
|
if not status_updated:
|
||||||
print("Warnung: Notion-Task-Status konnte nicht aktualisiert werden.")
|
print("Warnung: Notion-Task-Status konnte nicht aktualisiert werden.")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user