add calendar change detection to daemon mode

Enhances the sync daemon to monitor both email and calendar changes, automatically triggering syncs when local calendar events are added or deleted in VDIR format.

🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
This commit is contained in:
Tim Bendt
2025-07-16 09:46:31 -04:00
parent 1f306fffd7
commit fc7d07ae6b

View File

@@ -10,6 +10,8 @@ from src.utils.calendar_utils import save_events_to_vdir, save_events_to_file
from src.services.microsoft_graph.calendar import ( from src.services.microsoft_graph.calendar import (
fetch_calendar_events, fetch_calendar_events,
sync_local_calendar_changes, sync_local_calendar_changes,
get_last_sync_time,
detect_deleted_events,
) )
from src.services.microsoft_graph.mail import ( from src.services.microsoft_graph.mail import (
fetch_mail_async, fetch_mail_async,
@@ -420,6 +422,72 @@ def sync(
) )
def check_calendar_changes(vdir_path, org):
"""
Check if there are local calendar changes that need syncing.
Args:
vdir_path (str): Base vdir path
org (str): Organization name
Returns:
tuple: (has_changes, change_description)
"""
if not vdir_path:
return False, "No vdir path configured"
org_vdir_path = os.path.join(os.path.expanduser(vdir_path), org)
if not os.path.exists(org_vdir_path):
return False, "Calendar directory does not exist"
try:
# Get last sync time
last_sync_time = get_last_sync_time(org_vdir_path)
# Check if vdir directory has been modified since last sync
vdir_mtime = os.path.getmtime(org_vdir_path)
if vdir_mtime > last_sync_time:
# Check for specific types of changes
deleted_events = detect_deleted_events(org_vdir_path)
# Count .ics files to detect new events
import glob
ics_files = glob.glob(os.path.join(org_vdir_path, "*.ics"))
# Load previous state to compare
state_file = os.path.join(org_vdir_path, ".sync_state.json")
previous_state = {}
if os.path.exists(state_file):
try:
import json
with open(state_file, "r") as f:
previous_state = json.load(f)
except Exception:
pass
new_event_count = len(ics_files) - len(previous_state) + len(deleted_events)
if deleted_events or new_event_count > 0:
changes = []
if new_event_count > 0:
changes.append(f"{new_event_count} new events")
if deleted_events:
changes.append(f"{len(deleted_events)} deleted events")
return True, ", ".join(changes)
else:
return True, "directory modified"
return False, "no changes detected"
except Exception as e:
return False, f"error checking calendar: {str(e)}"
async def daemon_mode( async def daemon_mode(
dry_run, dry_run,
vdir, vdir,
@@ -432,7 +500,7 @@ async def daemon_mode(
two_way_calendar, two_way_calendar,
): ):
""" """
Run the script in daemon mode, periodically syncing emails. Run the script in daemon mode, periodically syncing emails and calendar.
""" """
from src.services.microsoft_graph.mail import get_inbox_count_async from src.services.microsoft_graph.mail import get_inbox_count_async
from rich.console import Console from rich.console import Console
@@ -455,7 +523,10 @@ async def daemon_mode(
content.append(status_text, style=status_color) content.append(status_text, style=status_color)
return Panel( return Panel(
content, title="📧 Email Sync Daemon", border_style="blue", padding=(0, 1) content,
title="📧 Email & Calendar Sync Daemon",
border_style="blue",
padding=(0, 1),
) )
# Initial display # Initial display
@@ -486,10 +557,10 @@ async def daemon_mode(
else: else:
# Show checking status # Show checking status
console.clear() console.clear()
console.print(create_status_display("Checking for new messages...", "cyan")) console.print(create_status_display("Checking for changes...", "cyan"))
try: try:
# Authenticate and get access token # Authenticate and get access token for mail check
scopes = ["https://graph.microsoft.com/Mail.Read"] scopes = ["https://graph.microsoft.com/Mail.Read"]
access_token, headers = get_access_token(scopes) access_token, headers = get_access_token(scopes)
remote_message_count = await get_inbox_count_async(headers) remote_message_count = await get_inbox_count_async(headers)
@@ -509,14 +580,41 @@ async def daemon_mode(
[f for f in os.listdir(cur_dir) if ".eml" in f] [f for f in os.listdir(cur_dir) if ".eml" in f]
) )
if remote_message_count != local_message_count: mail_changes = remote_message_count != local_message_count
# Check for calendar changes if two-way sync is enabled
calendar_changes = False
calendar_change_desc = ""
if two_way_calendar and vdir:
calendar_changes, calendar_change_desc = check_calendar_changes(
vdir, org
)
# Determine what changed and show appropriate status
if mail_changes and calendar_changes:
console.print(
create_status_display(
f"Changes detected! Mail: Remote {remote_message_count}, Local {local_message_count} | Calendar: {calendar_change_desc}. Starting sync...",
"yellow",
)
)
elif mail_changes:
console.print( console.print(
create_status_display( create_status_display(
f"New messages detected! Remote: {remote_message_count}, Local: {local_message_count}. Starting sync...", f"New messages detected! Remote: {remote_message_count}, Local: {local_message_count}. Starting sync...",
"yellow", "yellow",
) )
) )
elif calendar_changes:
console.print(
create_status_display(
f"Calendar changes detected! {calendar_change_desc}. Starting sync...",
"yellow",
)
)
# Sync if any changes detected
if mail_changes or calendar_changes:
await _sync_outlook_data( await _sync_outlook_data(
dry_run, dry_run,
vdir, vdir,
@@ -531,9 +629,15 @@ async def daemon_mode(
last_sync_time = time.time() last_sync_time = time.time()
console.print(create_status_display("Sync completed ✅", "green")) console.print(create_status_display("Sync completed ✅", "green"))
else: else:
status_parts = [
f"Mail: Remote {remote_message_count}, Local {local_message_count}"
]
if two_way_calendar:
status_parts.append(f"Calendar: {calendar_change_desc}")
console.print( console.print(
create_status_display( create_status_display(
f"No new messages (Remote: {remote_message_count}, Local: {local_message_count})", f"No changes detected ({', '.join(status_parts)})",
"green", "green",
) )
) )