dashboard sync app

This commit is contained in:
Tim Bendt
2025-12-16 17:13:26 -05:00
parent 73079f743a
commit d7c82a0da0
25 changed files with 4181 additions and 69 deletions

View File

@@ -1,6 +1,8 @@
import click
import asyncio
import os
import sys
import signal
import json
import time
from datetime import datetime, timedelta
@@ -31,7 +33,7 @@ from src.services.godspeed.sync import GodspeedSync
# Timing state management
def get_sync_state_file():
"""Get the path to the sync state file."""
return os.path.expanduser("~/.local/share/gtd-terminal-tools/sync_state.json")
return os.path.expanduser("~/.local/share/luk/sync_state.json")
def load_sync_state():
@@ -97,7 +99,7 @@ def get_godspeed_sync_directory():
return docs_dir
# Fall back to data directory
data_dir = home / ".local" / "share" / "gtd-terminal-tools" / "godspeed"
data_dir = home / ".local" / "share" / "luk" / "godspeed"
return data_dir
@@ -265,11 +267,12 @@ async def fetch_calendar_async(
# Update progress bar with total events
progress.update(task_id, total=total_events)
# Define org_vdir_path up front if vdir_path is specified
org_vdir_path = os.path.join(vdir_path, org_name) if vdir_path else None
# Save events to appropriate format
if not dry_run:
if vdir_path:
# Create org-specific directory within vdir path
org_vdir_path = os.path.join(vdir_path, org_name)
if vdir_path and org_vdir_path:
progress.console.print(
f"[cyan]Saving events to vdir: {org_vdir_path}[/cyan]"
)
@@ -342,7 +345,7 @@ async def fetch_calendar_async(
progress.update(task_id, total=next_total_events)
if not dry_run:
if vdir_path:
if vdir_path and org_vdir_path:
save_events_to_vdir(
next_events, org_vdir_path, progress, task_id, dry_run
)
@@ -494,9 +497,9 @@ async def _sync_outlook_data(
os.getenv("MAILDIR_PATH", os.path.expanduser("~/Mail")) + f"/{org}"
)
messages_before = 0
new_dir = os.path.join(maildir_path, "new")
cur_dir = os.path.join(maildir_path, "cur")
if notify:
new_dir = os.path.join(maildir_path, "new")
cur_dir = os.path.join(maildir_path, "cur")
if os.path.exists(new_dir):
messages_before += len([f for f in os.listdir(new_dir) if ".eml" in f])
if os.path.exists(cur_dir):
@@ -572,7 +575,51 @@ async def _sync_outlook_data(
click.echo("Sync complete.")
@click.command()
@click.group()
def sync():
"""Email and calendar synchronization."""
pass
def daemonize():
"""Properly daemonize the process for Unix systems."""
# First fork
try:
pid = os.fork()
if pid > 0:
# Parent exits
sys.exit(0)
except OSError as e:
sys.stderr.write(f"Fork #1 failed: {e}\n")
sys.exit(1)
# Decouple from parent environment
os.chdir("/")
os.setsid()
os.umask(0)
# Second fork
try:
pid = os.fork()
if pid > 0:
# Parent exits
sys.exit(0)
except OSError as e:
sys.stderr.write(f"Fork #2 failed: {e}\n")
sys.exit(1)
# Redirect standard file descriptors
sys.stdout.flush()
sys.stderr.flush()
si = open(os.devnull, "r")
so = open(os.devnull, "a+")
se = open(os.devnull, "a+")
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
@sync.command()
@click.option(
"--dry-run",
is_flag=True,
@@ -628,13 +675,19 @@ async def _sync_outlook_data(
help="Run in daemon mode.",
default=False,
)
@click.option(
"--dashboard",
is_flag=True,
help="Run with TUI dashboard.",
default=False,
)
@click.option(
"--notify",
is_flag=True,
help="Send macOS notifications for new email messages",
default=False,
)
def sync(
def run(
dry_run,
vdir,
icsfile,
@@ -645,23 +698,31 @@ def sync(
download_attachments,
two_way_calendar,
daemon,
dashboard,
notify,
):
if daemon:
asyncio.run(
daemon_mode(
dry_run,
vdir,
icsfile,
org,
days_back,
days_forward,
continue_iteration,
download_attachments,
two_way_calendar,
notify,
)
if dashboard:
from .sync_dashboard import run_dashboard_sync
asyncio.run(run_dashboard_sync())
elif daemon:
from .sync_daemon import create_daemon_config, SyncDaemon
config = create_daemon_config(
dry_run=dry_run,
vdir=vdir,
icsfile=icsfile,
org=org,
days_back=days_back,
days_forward=days_forward,
continue_iteration=continue_iteration,
download_attachments=download_attachments,
two_way_calendar=two_way_calendar,
notify=notify,
)
daemon_instance = SyncDaemon(config)
daemon_instance.start()
else:
asyncio.run(
_sync_outlook_data(
@@ -679,6 +740,55 @@ def sync(
)
@sync.command()
def stop():
"""Stop the sync daemon."""
pid_file = os.path.expanduser("~/.config/luk/luk.pid")
if not os.path.exists(pid_file):
click.echo("Daemon is not running (no PID file found)")
return
try:
with open(pid_file, "r") as f:
pid = int(f.read().strip())
# Send SIGTERM to process
os.kill(pid, signal.SIGTERM)
# Remove PID file
os.unlink(pid_file)
click.echo(f"Daemon stopped (PID {pid})")
except (ValueError, ProcessLookupError, OSError) as e:
click.echo(f"Error stopping daemon: {e}")
# Clean up stale PID file
if os.path.exists(pid_file):
os.unlink(pid_file)
@sync.command()
def status():
"""Check the status of the sync daemon."""
pid_file = os.path.expanduser("~/.config/luk/luk.pid")
if not os.path.exists(pid_file):
click.echo("Daemon is not running")
return
try:
with open(pid_file, "r") as f:
pid = int(f.read().strip())
# Check if process exists
os.kill(pid, 0) # Send signal 0 to check if process exists
click.echo(f"Daemon is running (PID {pid})")
except (ValueError, ProcessLookupError, OSError):
click.echo("Daemon is not running (stale PID file)")
# Clean up stale PID file
os.unlink(pid_file)
def check_calendar_changes(vdir_path, org):
"""
Check if there are local calendar changes that need syncing.