WIP
This commit is contained in:
@@ -1,8 +1,32 @@
|
||||
"""Calendar CLI commands."""
|
||||
|
||||
import click
|
||||
import subprocess
|
||||
|
||||
|
||||
@click.command()
|
||||
def calendar():
|
||||
"""Open the calendar (khal interactive)."""
|
||||
click.echo("Opening calendar...")
|
||||
subprocess.run(["khal", "interactive"])
|
||||
@click.option("--interactive", "-i", is_flag=True, help="Use khal interactive mode")
|
||||
@click.option("--weekdays", "-w", is_flag=True, help="Show only weekdays (Mon-Fri)")
|
||||
def calendar(interactive: bool, weekdays: bool):
|
||||
"""Open the calendar TUI.
|
||||
|
||||
Displays a week view of calendar events from khal.
|
||||
|
||||
Navigation:
|
||||
j/k - Move up/down (time)
|
||||
h/l - Move left/right (day)
|
||||
H/L - Previous/Next week
|
||||
g - Go to today
|
||||
w - Toggle weekends
|
||||
Enter - View event details
|
||||
q - Quit
|
||||
"""
|
||||
if interactive:
|
||||
# Fallback to khal interactive mode
|
||||
import subprocess
|
||||
|
||||
click.echo("Opening khal interactive...")
|
||||
subprocess.run(["khal", "interactive"])
|
||||
else:
|
||||
from src.calendar import run_app
|
||||
|
||||
run_app()
|
||||
|
||||
@@ -20,6 +20,7 @@ from src.services.microsoft_graph.calendar import (
|
||||
)
|
||||
from src.services.microsoft_graph.mail import (
|
||||
fetch_mail_async,
|
||||
fetch_archive_mail_async,
|
||||
archive_mail_async,
|
||||
delete_mail_async,
|
||||
synchronize_maildir_async,
|
||||
@@ -216,7 +217,9 @@ def create_maildir_structure(base_path):
|
||||
ensure_directory_exists(os.path.join(base_path, "cur"))
|
||||
ensure_directory_exists(os.path.join(base_path, "new"))
|
||||
ensure_directory_exists(os.path.join(base_path, "tmp"))
|
||||
ensure_directory_exists(os.path.join(base_path, ".Archives"))
|
||||
ensure_directory_exists(os.path.join(base_path, ".Archive", "cur"))
|
||||
ensure_directory_exists(os.path.join(base_path, ".Archive", "new"))
|
||||
ensure_directory_exists(os.path.join(base_path, ".Archive", "tmp"))
|
||||
ensure_directory_exists(os.path.join(base_path, ".Trash", "cur"))
|
||||
# Create outbox structure for sending emails
|
||||
ensure_directory_exists(os.path.join(base_path, "outbox", "new"))
|
||||
@@ -436,6 +439,7 @@ async def _sync_outlook_data(
|
||||
|
||||
with progress:
|
||||
task_fetch = progress.add_task("[green]Syncing Inbox...", total=0)
|
||||
task_fetch_archive = progress.add_task("[green]Syncing Archive...", total=0)
|
||||
task_calendar = progress.add_task("[cyan]Fetching calendar...", total=0)
|
||||
task_local_calendar = progress.add_task(
|
||||
"[magenta]Syncing local calendar...", total=0
|
||||
@@ -515,6 +519,15 @@ async def _sync_outlook_data(
|
||||
dry_run,
|
||||
download_attachments,
|
||||
),
|
||||
fetch_archive_mail_async(
|
||||
maildir_path,
|
||||
attachments_dir,
|
||||
headers,
|
||||
progress,
|
||||
task_fetch_archive,
|
||||
dry_run,
|
||||
download_attachments,
|
||||
),
|
||||
fetch_calendar_async(
|
||||
headers,
|
||||
progress,
|
||||
|
||||
@@ -17,15 +17,45 @@ from textual.binding import Binding
|
||||
from rich.text import Text
|
||||
from datetime import datetime, timedelta
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from typing import Dict, Any, Optional, List, Callable
|
||||
from pathlib import Path
|
||||
|
||||
from src.utils.shared_config import get_theme_name
|
||||
|
||||
# Default sync interval in seconds (5 minutes)
|
||||
DEFAULT_SYNC_INTERVAL = 300
|
||||
|
||||
# Sync tasks config file path
|
||||
SYNC_TASKS_CONFIG_PATH = os.path.expanduser("~/.config/luk/sync_tasks.json")
|
||||
|
||||
|
||||
def load_sync_tasks_config() -> Dict[str, bool]:
|
||||
"""Load sync tasks enabled/disabled state from config file.
|
||||
|
||||
Returns:
|
||||
Dict mapping task_id to enabled state (True = enabled, False = disabled)
|
||||
"""
|
||||
if os.path.exists(SYNC_TASKS_CONFIG_PATH):
|
||||
try:
|
||||
with open(SYNC_TASKS_CONFIG_PATH, "r") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
# Default: all tasks enabled
|
||||
return {}
|
||||
|
||||
|
||||
def save_sync_tasks_config(config: Dict[str, bool]) -> None:
|
||||
"""Save sync tasks enabled/disabled state to config file."""
|
||||
os.makedirs(os.path.dirname(SYNC_TASKS_CONFIG_PATH), exist_ok=True)
|
||||
with open(SYNC_TASKS_CONFIG_PATH, "w") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
|
||||
# Futuristic spinner frames
|
||||
# SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
||||
# Alternative spinners you could use:
|
||||
@@ -73,7 +103,9 @@ class TaskStatus:
|
||||
class TaskListItem(ListItem):
|
||||
"""A list item representing a sync task."""
|
||||
|
||||
def __init__(self, task_id: str, task_name: str, *args, **kwargs):
|
||||
def __init__(
|
||||
self, task_id: str, task_name: str, enabled: bool = True, *args, **kwargs
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.task_id = task_id
|
||||
self.task_name = task_name
|
||||
@@ -81,6 +113,7 @@ class TaskListItem(ListItem):
|
||||
self.progress = 0
|
||||
self.total = 100
|
||||
self.spinner_frame = 0
|
||||
self.enabled = enabled
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the task item layout."""
|
||||
@@ -88,6 +121,8 @@ class TaskListItem(ListItem):
|
||||
|
||||
def _get_status_icon(self) -> str:
|
||||
"""Get icon based on status."""
|
||||
if not self.enabled:
|
||||
return "⊘" # Disabled icon
|
||||
if self.status == TaskStatus.RUNNING:
|
||||
return SPINNER_FRAMES[self.spinner_frame % len(SPINNER_FRAMES)]
|
||||
icons = {
|
||||
@@ -103,6 +138,8 @@ class TaskListItem(ListItem):
|
||||
|
||||
def _get_status_color(self) -> str:
|
||||
"""Get color based on status."""
|
||||
if not self.enabled:
|
||||
return "dim italic"
|
||||
colors = {
|
||||
TaskStatus.PENDING: "dim",
|
||||
TaskStatus.RUNNING: "cyan",
|
||||
@@ -116,6 +153,13 @@ class TaskListItem(ListItem):
|
||||
icon = self._get_status_icon()
|
||||
color = self._get_status_color()
|
||||
|
||||
# Disabled tasks show differently
|
||||
if not self.enabled:
|
||||
text = Text()
|
||||
text.append(f"{icon} ", style="dim")
|
||||
text.append(f"{self.task_name} [Disabled]", style=color)
|
||||
return text
|
||||
|
||||
# Use green checkmark for completed, but white text for readability
|
||||
if self.status == TaskStatus.RUNNING:
|
||||
progress_pct = (
|
||||
@@ -156,6 +200,7 @@ class SyncDashboard(App):
|
||||
Binding("s", "sync_now", "Sync Now"),
|
||||
Binding("d", "daemonize", "Daemonize"),
|
||||
Binding("r", "refresh", "Refresh"),
|
||||
Binding("t", "toggle_task", "Toggle"),
|
||||
Binding("+", "increase_interval", "+Interval"),
|
||||
Binding("-", "decrease_interval", "-Interval"),
|
||||
Binding("up", "cursor_up", "Up", show=False),
|
||||
@@ -277,10 +322,16 @@ class SyncDashboard(App):
|
||||
self._initial_sync_interval = sync_interval
|
||||
self._notify = notify
|
||||
self._demo_mode = demo_mode
|
||||
# Load task enabled/disabled config
|
||||
self._tasks_config = load_sync_tasks_config()
|
||||
# Merge provided config with defaults
|
||||
self._sync_config = {**DEFAULT_SYNC_CONFIG, **(sync_config or {})}
|
||||
self._sync_config["notify"] = notify
|
||||
|
||||
def _is_task_enabled(self, task_id: str) -> bool:
|
||||
"""Check if a task is enabled (default: True)."""
|
||||
return self._tasks_config.get(task_id, True)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the dashboard layout."""
|
||||
yield Header()
|
||||
@@ -291,15 +342,56 @@ class SyncDashboard(App):
|
||||
yield Static("Tasks", classes="sidebar-title")
|
||||
yield ListView(
|
||||
# Stage 1: Sync local changes to server
|
||||
TaskListItem("archive", "Archive Mail", id="task-archive"),
|
||||
TaskListItem("outbox", "Outbox Send", id="task-outbox"),
|
||||
TaskListItem(
|
||||
"archive",
|
||||
"Archive Mail",
|
||||
enabled=self._is_task_enabled("archive"),
|
||||
id="task-archive",
|
||||
),
|
||||
TaskListItem(
|
||||
"outbox",
|
||||
"Outbox Send",
|
||||
enabled=self._is_task_enabled("outbox"),
|
||||
id="task-outbox",
|
||||
),
|
||||
# Stage 2: Fetch from server
|
||||
TaskListItem("inbox", "Inbox Sync", id="task-inbox"),
|
||||
TaskListItem("calendar", "Calendar Sync", id="task-calendar"),
|
||||
TaskListItem(
|
||||
"inbox",
|
||||
"Inbox Sync",
|
||||
enabled=self._is_task_enabled("inbox"),
|
||||
id="task-inbox",
|
||||
),
|
||||
TaskListItem(
|
||||
"archive-fetch",
|
||||
"Archive Sync",
|
||||
enabled=self._is_task_enabled("archive-fetch"),
|
||||
id="task-archive-fetch",
|
||||
),
|
||||
TaskListItem(
|
||||
"calendar",
|
||||
"Calendar Sync",
|
||||
enabled=self._is_task_enabled("calendar"),
|
||||
id="task-calendar",
|
||||
),
|
||||
# Stage 3: Task management
|
||||
TaskListItem("godspeed", "Godspeed Sync", id="task-godspeed"),
|
||||
TaskListItem("dstask", "dstask Sync", id="task-dstask"),
|
||||
TaskListItem("sweep", "Task Sweep", id="task-sweep"),
|
||||
TaskListItem(
|
||||
"godspeed",
|
||||
"Godspeed Sync",
|
||||
enabled=self._is_task_enabled("godspeed"),
|
||||
id="task-godspeed",
|
||||
),
|
||||
TaskListItem(
|
||||
"dstask",
|
||||
"dstask Sync",
|
||||
enabled=self._is_task_enabled("dstask"),
|
||||
id="task-dstask",
|
||||
),
|
||||
TaskListItem(
|
||||
"sweep",
|
||||
"Task Sweep",
|
||||
enabled=self._is_task_enabled("sweep"),
|
||||
id="task-sweep",
|
||||
),
|
||||
id="task-list",
|
||||
)
|
||||
# Countdown timer at bottom of sidebar
|
||||
@@ -332,6 +424,9 @@ class SyncDashboard(App):
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Initialize the dashboard."""
|
||||
# Set theme from shared config
|
||||
self.theme = get_theme_name()
|
||||
|
||||
# Store references to task items
|
||||
task_list = self.query_one("#task-list", ListView)
|
||||
for item in task_list.children:
|
||||
@@ -436,6 +531,12 @@ class SyncDashboard(App):
|
||||
if task_id == self.selected_task:
|
||||
self._update_main_panel()
|
||||
|
||||
def is_task_enabled(self, task_id: str) -> bool:
|
||||
"""Check if a task is enabled."""
|
||||
if task_id in self._task_items:
|
||||
return self._task_items[task_id].enabled
|
||||
return self._is_task_enabled(task_id)
|
||||
|
||||
def update_task(self, task_id: str, progress: int, message: str = "") -> None:
|
||||
"""Update task progress."""
|
||||
if task_id in self._task_items:
|
||||
@@ -495,6 +596,25 @@ class SyncDashboard(App):
|
||||
task_list = self.query_one("#task-list", ListView)
|
||||
task_list.action_cursor_down()
|
||||
|
||||
def action_toggle_task(self) -> None:
|
||||
"""Toggle the selected task enabled/disabled state."""
|
||||
if self.selected_task not in self._task_items:
|
||||
return
|
||||
|
||||
item = self._task_items[self.selected_task]
|
||||
# Toggle enabled state
|
||||
item.enabled = not item.enabled
|
||||
item.update_display()
|
||||
|
||||
# Update config and save
|
||||
self._tasks_config[self.selected_task] = item.enabled
|
||||
save_sync_tasks_config(self._tasks_config)
|
||||
|
||||
# Log the change
|
||||
state = "enabled" if item.enabled else "disabled"
|
||||
self._log_to_task(self.selected_task, f"Task {state}")
|
||||
self._update_main_panel()
|
||||
|
||||
def action_sync_now(self) -> None:
|
||||
"""Trigger an immediate sync."""
|
||||
if self._sync_callback:
|
||||
@@ -770,6 +890,10 @@ class SyncProgressTracker:
|
||||
"""Mark a task as skipped."""
|
||||
self.dashboard.skip_task(task_id, reason)
|
||||
|
||||
def is_task_enabled(self, task_id: str) -> bool:
|
||||
"""Check if a task is enabled."""
|
||||
return self.dashboard.is_task_enabled(task_id)
|
||||
|
||||
|
||||
# Global dashboard instance
|
||||
_dashboard_instance: Optional[SyncDashboard] = None
|
||||
@@ -823,68 +947,99 @@ async def run_dashboard_sync(
|
||||
# Stage 1: Sync local changes to server
|
||||
|
||||
# Archive mail
|
||||
tracker.start_task("archive", 100)
|
||||
tracker.update_task("archive", 50, "Scanning for archived messages...")
|
||||
await asyncio.sleep(0.3)
|
||||
tracker.update_task("archive", 100, "Moving 3 messages to archive...")
|
||||
await asyncio.sleep(0.2)
|
||||
tracker.complete_task("archive", "3 messages archived")
|
||||
if tracker.is_task_enabled("archive"):
|
||||
tracker.start_task("archive", 100)
|
||||
tracker.update_task("archive", 50, "Scanning for archived messages...")
|
||||
await asyncio.sleep(0.3)
|
||||
tracker.update_task("archive", 100, "Moving 3 messages to archive...")
|
||||
await asyncio.sleep(0.2)
|
||||
tracker.complete_task("archive", "3 messages archived")
|
||||
else:
|
||||
tracker.skip_task("archive", "Disabled")
|
||||
|
||||
# Outbox
|
||||
tracker.start_task("outbox", 100)
|
||||
tracker.update_task("outbox", 50, "Checking outbox...")
|
||||
await asyncio.sleep(0.2)
|
||||
tracker.complete_task("outbox", "No pending emails")
|
||||
if tracker.is_task_enabled("outbox"):
|
||||
tracker.start_task("outbox", 100)
|
||||
tracker.update_task("outbox", 50, "Checking outbox...")
|
||||
await asyncio.sleep(0.2)
|
||||
tracker.complete_task("outbox", "No pending emails")
|
||||
else:
|
||||
tracker.skip_task("outbox", "Disabled")
|
||||
|
||||
# Stage 2: Fetch from server
|
||||
|
||||
# Inbox sync - simulate finding new messages
|
||||
tracker.start_task("inbox", 100)
|
||||
for i in range(0, 101, 20):
|
||||
tracker.update_task("inbox", i, f"Fetching emails... {i}%")
|
||||
await asyncio.sleep(0.3)
|
||||
if tracker.is_task_enabled("inbox"):
|
||||
tracker.start_task("inbox", 100)
|
||||
for i in range(0, 101, 20):
|
||||
tracker.update_task("inbox", i, f"Fetching emails... {i}%")
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
new_message_count = random.randint(0, 5)
|
||||
if new_message_count > 0:
|
||||
tracker.complete_task("inbox", f"{new_message_count} new emails")
|
||||
if dashboard._notify:
|
||||
from src.utils.notifications import notify_new_emails
|
||||
new_message_count = random.randint(0, 5)
|
||||
if new_message_count > 0:
|
||||
tracker.complete_task("inbox", f"{new_message_count} new emails")
|
||||
if dashboard._notify:
|
||||
from src.utils.notifications import notify_new_emails
|
||||
|
||||
notify_new_emails(new_message_count, "")
|
||||
notify_new_emails(new_message_count, "")
|
||||
else:
|
||||
tracker.complete_task("inbox", "No new emails")
|
||||
else:
|
||||
tracker.complete_task("inbox", "No new emails")
|
||||
tracker.skip_task("inbox", "Disabled")
|
||||
|
||||
# Archive fetch
|
||||
if tracker.is_task_enabled("archive-fetch"):
|
||||
tracker.start_task("archive-fetch", 100)
|
||||
for i in range(0, 101, 25):
|
||||
tracker.update_task("archive-fetch", i, f"Fetching archive... {i}%")
|
||||
await asyncio.sleep(0.2)
|
||||
tracker.complete_task("archive-fetch", "Archive synced")
|
||||
else:
|
||||
tracker.skip_task("archive-fetch", "Disabled")
|
||||
|
||||
# Calendar sync
|
||||
tracker.start_task("calendar", 100)
|
||||
for i in range(0, 101, 25):
|
||||
tracker.update_task("calendar", i, f"Syncing events... {i}%")
|
||||
await asyncio.sleep(0.3)
|
||||
tracker.complete_task("calendar", "25 events synced")
|
||||
if tracker.is_task_enabled("calendar"):
|
||||
tracker.start_task("calendar", 100)
|
||||
for i in range(0, 101, 25):
|
||||
tracker.update_task("calendar", i, f"Syncing events... {i}%")
|
||||
await asyncio.sleep(0.3)
|
||||
tracker.complete_task("calendar", "25 events synced")
|
||||
else:
|
||||
tracker.skip_task("calendar", "Disabled")
|
||||
|
||||
# Stage 3: Task management
|
||||
|
||||
# Godspeed sync
|
||||
tracker.start_task("godspeed", 100)
|
||||
for i in range(0, 101, 33):
|
||||
tracker.update_task(
|
||||
"godspeed", min(i, 100), f"Syncing tasks... {min(i, 100)}%"
|
||||
)
|
||||
await asyncio.sleep(0.3)
|
||||
tracker.complete_task("godspeed", "42 tasks synced")
|
||||
if tracker.is_task_enabled("godspeed"):
|
||||
tracker.start_task("godspeed", 100)
|
||||
for i in range(0, 101, 33):
|
||||
tracker.update_task(
|
||||
"godspeed", min(i, 100), f"Syncing tasks... {min(i, 100)}%"
|
||||
)
|
||||
await asyncio.sleep(0.3)
|
||||
tracker.complete_task("godspeed", "42 tasks synced")
|
||||
else:
|
||||
tracker.skip_task("godspeed", "Disabled")
|
||||
|
||||
# dstask sync
|
||||
tracker.start_task("dstask", 100)
|
||||
tracker.update_task("dstask", 30, "Running dstask sync...")
|
||||
await asyncio.sleep(0.3)
|
||||
tracker.update_task("dstask", 70, "Pushing changes...")
|
||||
await asyncio.sleep(0.2)
|
||||
tracker.complete_task("dstask", "Sync completed")
|
||||
if tracker.is_task_enabled("dstask"):
|
||||
tracker.start_task("dstask", 100)
|
||||
tracker.update_task("dstask", 30, "Running dstask sync...")
|
||||
await asyncio.sleep(0.3)
|
||||
tracker.update_task("dstask", 70, "Pushing changes...")
|
||||
await asyncio.sleep(0.2)
|
||||
tracker.complete_task("dstask", "Sync completed")
|
||||
else:
|
||||
tracker.skip_task("dstask", "Disabled")
|
||||
|
||||
# Task sweep
|
||||
tracker.start_task("sweep")
|
||||
tracker.update_task("sweep", 50, "Scanning notes directory...")
|
||||
await asyncio.sleep(0.2)
|
||||
tracker.skip_task("sweep", "Before 6 PM, skipping daily sweep")
|
||||
if tracker.is_task_enabled("sweep"):
|
||||
tracker.start_task("sweep")
|
||||
tracker.update_task("sweep", 50, "Scanning notes directory...")
|
||||
await asyncio.sleep(0.2)
|
||||
tracker.skip_task("sweep", "Before 6 PM, skipping daily sweep")
|
||||
else:
|
||||
tracker.skip_task("sweep", "Disabled")
|
||||
|
||||
# Schedule next sync
|
||||
dashboard.schedule_next_sync()
|
||||
@@ -902,6 +1057,7 @@ async def run_dashboard_sync(
|
||||
synchronize_maildir_async,
|
||||
process_outbox_async,
|
||||
fetch_mail_async,
|
||||
fetch_archive_mail_async,
|
||||
)
|
||||
from src.services.microsoft_graph.calendar import (
|
||||
fetch_calendar_events,
|
||||
@@ -955,32 +1111,46 @@ async def run_dashboard_sync(
|
||||
# ===== STAGE 1: Sync local changes to server =====
|
||||
|
||||
# Archive mail
|
||||
tracker.start_task("archive", 100)
|
||||
tracker.update_task("archive", 10, "Checking for archived messages...")
|
||||
try:
|
||||
archive_progress = DashboardProgressAdapter(tracker, "archive")
|
||||
await archive_mail_async(
|
||||
maildir_path, headers, archive_progress, None, dry_run
|
||||
)
|
||||
tracker.complete_task("archive", "Archive sync complete")
|
||||
except Exception as e:
|
||||
tracker.error_task("archive", str(e))
|
||||
if tracker.is_task_enabled("archive"):
|
||||
tracker.start_task("archive", 100)
|
||||
tracker.update_task("archive", 10, "Checking for archived messages...")
|
||||
try:
|
||||
archive_progress = DashboardProgressAdapter(tracker, "archive")
|
||||
await archive_mail_async(
|
||||
maildir_path,
|
||||
headers,
|
||||
archive_progress,
|
||||
None,
|
||||
dry_run,
|
||||
is_cancelled=lambda: not tracker.is_task_enabled("archive"),
|
||||
)
|
||||
if tracker.is_task_enabled("archive"):
|
||||
tracker.complete_task("archive", "Archive sync complete")
|
||||
else:
|
||||
tracker.skip_task("archive", "Cancelled")
|
||||
except Exception as e:
|
||||
tracker.error_task("archive", str(e))
|
||||
else:
|
||||
tracker.skip_task("archive", "Disabled")
|
||||
|
||||
# Process outbox (send pending emails)
|
||||
tracker.start_task("outbox", 100)
|
||||
tracker.update_task("outbox", 10, "Checking outbox...")
|
||||
try:
|
||||
outbox_progress = DashboardProgressAdapter(tracker, "outbox")
|
||||
result = await process_outbox_async(
|
||||
base_maildir_path, org, headers, outbox_progress, None, dry_run
|
||||
)
|
||||
sent_count, failed_count = result if result else (0, 0)
|
||||
if sent_count > 0:
|
||||
tracker.complete_task("outbox", f"{sent_count} emails sent")
|
||||
else:
|
||||
tracker.complete_task("outbox", "No pending emails")
|
||||
except Exception as e:
|
||||
tracker.error_task("outbox", str(e))
|
||||
if tracker.is_task_enabled("outbox"):
|
||||
tracker.start_task("outbox", 100)
|
||||
tracker.update_task("outbox", 10, "Checking outbox...")
|
||||
try:
|
||||
outbox_progress = DashboardProgressAdapter(tracker, "outbox")
|
||||
result = await process_outbox_async(
|
||||
base_maildir_path, org, headers, outbox_progress, None, dry_run
|
||||
)
|
||||
sent_count, failed_count = result if result else (0, 0)
|
||||
if sent_count > 0:
|
||||
tracker.complete_task("outbox", f"{sent_count} emails sent")
|
||||
else:
|
||||
tracker.complete_task("outbox", "No pending emails")
|
||||
except Exception as e:
|
||||
tracker.error_task("outbox", str(e))
|
||||
else:
|
||||
tracker.skip_task("outbox", "Disabled")
|
||||
|
||||
# ===== STAGE 2: Fetch from server =====
|
||||
|
||||
@@ -994,160 +1164,216 @@ async def run_dashboard_sync(
|
||||
messages_before += len([f for f in os.listdir(cur_dir) if ".eml" in f])
|
||||
|
||||
# Inbox sync
|
||||
tracker.start_task("inbox", 100)
|
||||
tracker.update_task("inbox", 10, "Fetching emails from server...")
|
||||
try:
|
||||
inbox_progress = DashboardProgressAdapter(tracker, "inbox")
|
||||
await fetch_mail_async(
|
||||
maildir_path,
|
||||
attachments_dir,
|
||||
headers,
|
||||
inbox_progress,
|
||||
None,
|
||||
dry_run,
|
||||
download_attachments,
|
||||
)
|
||||
tracker.update_task("inbox", 80, "Processing messages...")
|
||||
|
||||
# Count new messages
|
||||
messages_after = 0
|
||||
if os.path.exists(new_dir):
|
||||
messages_after += len(
|
||||
[f for f in os.listdir(new_dir) if ".eml" in f]
|
||||
)
|
||||
if os.path.exists(cur_dir):
|
||||
messages_after += len(
|
||||
[f for f in os.listdir(cur_dir) if ".eml" in f]
|
||||
if tracker.is_task_enabled("inbox"):
|
||||
tracker.start_task("inbox", 100)
|
||||
tracker.update_task("inbox", 10, "Fetching emails from server...")
|
||||
try:
|
||||
inbox_progress = DashboardProgressAdapter(tracker, "inbox")
|
||||
await fetch_mail_async(
|
||||
maildir_path,
|
||||
attachments_dir,
|
||||
headers,
|
||||
inbox_progress,
|
||||
None,
|
||||
dry_run,
|
||||
download_attachments,
|
||||
is_cancelled=lambda: not tracker.is_task_enabled("inbox"),
|
||||
)
|
||||
|
||||
new_message_count = messages_after - messages_before
|
||||
# Check if cancelled before completing
|
||||
if not tracker.is_task_enabled("inbox"):
|
||||
tracker.skip_task("inbox", "Cancelled")
|
||||
else:
|
||||
tracker.update_task("inbox", 80, "Processing messages...")
|
||||
|
||||
if new_message_count > 0:
|
||||
tracker.complete_task("inbox", f"{new_message_count} new emails")
|
||||
if dashboard._notify and not dry_run:
|
||||
notify_new_emails(new_message_count, org)
|
||||
else:
|
||||
tracker.complete_task("inbox", "No new emails")
|
||||
except Exception as e:
|
||||
tracker.error_task("inbox", str(e))
|
||||
# Count new messages
|
||||
messages_after = 0
|
||||
if os.path.exists(new_dir):
|
||||
messages_after += len(
|
||||
[f for f in os.listdir(new_dir) if ".eml" in f]
|
||||
)
|
||||
if os.path.exists(cur_dir):
|
||||
messages_after += len(
|
||||
[f for f in os.listdir(cur_dir) if ".eml" in f]
|
||||
)
|
||||
|
||||
new_message_count = messages_after - messages_before
|
||||
|
||||
if new_message_count > 0:
|
||||
tracker.complete_task(
|
||||
"inbox", f"{new_message_count} new emails"
|
||||
)
|
||||
if dashboard._notify and not dry_run:
|
||||
notify_new_emails(new_message_count, org)
|
||||
else:
|
||||
tracker.complete_task("inbox", "No new emails")
|
||||
except Exception as e:
|
||||
tracker.error_task("inbox", str(e))
|
||||
else:
|
||||
tracker.skip_task("inbox", "Disabled")
|
||||
|
||||
# Archive sync (fetch archived messages from server)
|
||||
if tracker.is_task_enabled("archive-fetch"):
|
||||
tracker.start_task("archive-fetch", 100)
|
||||
tracker.update_task("archive-fetch", 10, "Fetching archived emails...")
|
||||
try:
|
||||
archive_progress = DashboardProgressAdapter(
|
||||
tracker, "archive-fetch"
|
||||
)
|
||||
await fetch_archive_mail_async(
|
||||
maildir_path,
|
||||
attachments_dir,
|
||||
headers,
|
||||
archive_progress,
|
||||
None,
|
||||
dry_run,
|
||||
download_attachments,
|
||||
is_cancelled=lambda: not tracker.is_task_enabled(
|
||||
"archive-fetch"
|
||||
),
|
||||
)
|
||||
if tracker.is_task_enabled("archive-fetch"):
|
||||
tracker.complete_task("archive-fetch", "Archive synced")
|
||||
else:
|
||||
tracker.skip_task("archive-fetch", "Cancelled")
|
||||
except Exception as e:
|
||||
tracker.error_task("archive-fetch", str(e))
|
||||
else:
|
||||
tracker.skip_task("archive-fetch", "Disabled")
|
||||
|
||||
# Calendar sync
|
||||
tracker.start_task("calendar", 100)
|
||||
tracker.update_task("calendar", 10, "Fetching calendar events...")
|
||||
try:
|
||||
events, total_events = await fetch_calendar_events(
|
||||
headers=headers, days_back=days_back, days_forward=days_forward
|
||||
)
|
||||
tracker.update_task(
|
||||
"calendar", 50, f"Processing {len(events)} events..."
|
||||
)
|
||||
if tracker.is_task_enabled("calendar"):
|
||||
tracker.start_task("calendar", 100)
|
||||
tracker.update_task("calendar", 10, "Fetching calendar events...")
|
||||
try:
|
||||
events, total_events = await fetch_calendar_events(
|
||||
headers=headers, days_back=days_back, days_forward=days_forward
|
||||
)
|
||||
tracker.update_task(
|
||||
"calendar", 50, f"Processing {len(events)} events..."
|
||||
)
|
||||
|
||||
if not dry_run:
|
||||
calendar_progress = DashboardProgressAdapter(tracker, "calendar")
|
||||
org_vdir_path = os.path.join(vdir, org) if vdir else None
|
||||
if vdir and org_vdir_path:
|
||||
save_events_to_vdir(
|
||||
events, org_vdir_path, calendar_progress, None, dry_run
|
||||
)
|
||||
elif icsfile:
|
||||
save_events_to_file(
|
||||
events,
|
||||
f"{icsfile}/events_latest.ics",
|
||||
calendar_progress,
|
||||
None,
|
||||
dry_run,
|
||||
if not dry_run:
|
||||
calendar_progress = DashboardProgressAdapter(
|
||||
tracker, "calendar"
|
||||
)
|
||||
org_vdir_path = os.path.join(vdir, org) if vdir else None
|
||||
if vdir and org_vdir_path:
|
||||
save_events_to_vdir(
|
||||
events, org_vdir_path, calendar_progress, None, dry_run
|
||||
)
|
||||
elif icsfile:
|
||||
save_events_to_file(
|
||||
events,
|
||||
f"{icsfile}/events_latest.ics",
|
||||
calendar_progress,
|
||||
None,
|
||||
dry_run,
|
||||
)
|
||||
|
||||
tracker.complete_task("calendar", f"{len(events)} events synced")
|
||||
except Exception as e:
|
||||
tracker.error_task("calendar", str(e))
|
||||
tracker.complete_task("calendar", f"{len(events)} events synced")
|
||||
except Exception as e:
|
||||
tracker.error_task("calendar", str(e))
|
||||
else:
|
||||
tracker.skip_task("calendar", "Disabled")
|
||||
|
||||
# ===== STAGE 3: Godspeed operations =====
|
||||
|
||||
# Godspeed sync (runs every 15 minutes)
|
||||
tracker.start_task("godspeed", 100)
|
||||
if should_run_godspeed_sync():
|
||||
tracker.update_task("godspeed", 10, "Syncing with Godspeed...")
|
||||
try:
|
||||
email, password, token = get_godspeed_credentials()
|
||||
if token or (email and password):
|
||||
from src.services.godspeed.client import GodspeedClient
|
||||
from src.services.godspeed.sync import GodspeedSync
|
||||
if tracker.is_task_enabled("godspeed"):
|
||||
tracker.start_task("godspeed", 100)
|
||||
if should_run_godspeed_sync():
|
||||
tracker.update_task("godspeed", 10, "Syncing with Godspeed...")
|
||||
try:
|
||||
email, password, token = get_godspeed_credentials()
|
||||
if token or (email and password):
|
||||
from src.services.godspeed.client import GodspeedClient
|
||||
from src.services.godspeed.sync import GodspeedSync
|
||||
|
||||
sync_dir = get_godspeed_sync_directory()
|
||||
client = GodspeedClient(
|
||||
email=email, password=password, token=token
|
||||
)
|
||||
sync_engine = GodspeedSync(client, sync_dir)
|
||||
sync_engine.sync_bidirectional()
|
||||
sync_dir = get_godspeed_sync_directory()
|
||||
client = GodspeedClient(
|
||||
email=email, password=password, token=token
|
||||
)
|
||||
sync_engine = GodspeedSync(client, sync_dir)
|
||||
sync_engine.sync_bidirectional()
|
||||
|
||||
state = load_sync_state()
|
||||
state["last_godspeed_sync"] = time.time()
|
||||
save_sync_state(state)
|
||||
state = load_sync_state()
|
||||
state["last_godspeed_sync"] = time.time()
|
||||
save_sync_state(state)
|
||||
|
||||
tracker.complete_task("godspeed", "Sync completed")
|
||||
else:
|
||||
tracker.skip_task("godspeed", "No credentials configured")
|
||||
except Exception as e:
|
||||
tracker.error_task("godspeed", str(e))
|
||||
tracker.complete_task("godspeed", "Sync completed")
|
||||
else:
|
||||
tracker.skip_task("godspeed", "No credentials configured")
|
||||
except Exception as e:
|
||||
tracker.error_task("godspeed", str(e))
|
||||
else:
|
||||
tracker.skip_task("godspeed", "Not due yet (every 15 min)")
|
||||
else:
|
||||
tracker.skip_task("godspeed", "Not due yet (every 15 min)")
|
||||
tracker.skip_task("godspeed", "Disabled")
|
||||
|
||||
# dstask sync
|
||||
tracker.start_task("dstask", 100)
|
||||
try:
|
||||
from src.services.dstask.client import DstaskClient
|
||||
if tracker.is_task_enabled("dstask"):
|
||||
tracker.start_task("dstask", 100)
|
||||
try:
|
||||
from src.services.dstask.client import DstaskClient
|
||||
|
||||
dstask_client = DstaskClient()
|
||||
if dstask_client.is_available():
|
||||
tracker.update_task("dstask", 30, "Running dstask sync...")
|
||||
success = dstask_client.sync()
|
||||
if success:
|
||||
tracker.complete_task("dstask", "Sync completed")
|
||||
dstask_client = DstaskClient()
|
||||
if dstask_client.is_available():
|
||||
tracker.update_task("dstask", 30, "Running dstask sync...")
|
||||
success = dstask_client.sync()
|
||||
if success:
|
||||
tracker.complete_task("dstask", "Sync completed")
|
||||
else:
|
||||
tracker.error_task("dstask", "Sync failed")
|
||||
else:
|
||||
tracker.error_task("dstask", "Sync failed")
|
||||
else:
|
||||
tracker.skip_task("dstask", "dstask not installed")
|
||||
except Exception as e:
|
||||
tracker.error_task("dstask", str(e))
|
||||
tracker.skip_task("dstask", "dstask not installed")
|
||||
except Exception as e:
|
||||
tracker.error_task("dstask", str(e))
|
||||
else:
|
||||
tracker.skip_task("dstask", "Disabled")
|
||||
|
||||
# Task sweep (runs once daily after 6 PM)
|
||||
tracker.start_task("sweep", 100)
|
||||
if should_run_sweep():
|
||||
tracker.update_task("sweep", 10, "Sweeping tasks from notes...")
|
||||
try:
|
||||
from src.cli.godspeed import TaskSweeper
|
||||
if tracker.is_task_enabled("sweep"):
|
||||
tracker.start_task("sweep", 100)
|
||||
if should_run_sweep():
|
||||
tracker.update_task("sweep", 10, "Sweeping tasks from notes...")
|
||||
try:
|
||||
from src.cli.godspeed import TaskSweeper
|
||||
from datetime import datetime
|
||||
|
||||
notes_dir_env = os.getenv("NOTES_DIR")
|
||||
if notes_dir_env and Path(notes_dir_env).exists():
|
||||
godspeed_dir = get_godspeed_sync_directory()
|
||||
sweeper = TaskSweeper(
|
||||
Path(notes_dir_env), godspeed_dir, dry_run=dry_run
|
||||
)
|
||||
result = sweeper.sweep_tasks()
|
||||
|
||||
state = load_sync_state()
|
||||
state["last_sweep_date"] = datetime.now().strftime(
|
||||
"%Y-%m-%d"
|
||||
)
|
||||
save_sync_state(state)
|
||||
|
||||
swept = result.get("swept_tasks", 0)
|
||||
if swept > 0:
|
||||
tracker.complete_task("sweep", f"{swept} tasks swept")
|
||||
else:
|
||||
tracker.complete_task("sweep", "No tasks to sweep")
|
||||
else:
|
||||
tracker.skip_task("sweep", "$NOTES_DIR not configured")
|
||||
except Exception as e:
|
||||
tracker.error_task("sweep", str(e))
|
||||
else:
|
||||
from datetime import datetime
|
||||
|
||||
notes_dir_env = os.getenv("NOTES_DIR")
|
||||
if notes_dir_env and Path(notes_dir_env).exists():
|
||||
godspeed_dir = get_godspeed_sync_directory()
|
||||
sweeper = TaskSweeper(
|
||||
Path(notes_dir_env), godspeed_dir, dry_run=dry_run
|
||||
)
|
||||
result = sweeper.sweep_tasks()
|
||||
|
||||
state = load_sync_state()
|
||||
state["last_sweep_date"] = datetime.now().strftime("%Y-%m-%d")
|
||||
save_sync_state(state)
|
||||
|
||||
swept = result.get("swept_tasks", 0)
|
||||
if swept > 0:
|
||||
tracker.complete_task("sweep", f"{swept} tasks swept")
|
||||
else:
|
||||
tracker.complete_task("sweep", "No tasks to sweep")
|
||||
current_hour = datetime.now().hour
|
||||
if current_hour < 18:
|
||||
tracker.skip_task("sweep", "Before 6 PM")
|
||||
else:
|
||||
tracker.skip_task("sweep", "$NOTES_DIR not configured")
|
||||
except Exception as e:
|
||||
tracker.error_task("sweep", str(e))
|
||||
tracker.skip_task("sweep", "Already completed today")
|
||||
else:
|
||||
from datetime import datetime
|
||||
|
||||
current_hour = datetime.now().hour
|
||||
if current_hour < 18:
|
||||
tracker.skip_task("sweep", "Before 6 PM")
|
||||
else:
|
||||
tracker.skip_task("sweep", "Already completed today")
|
||||
tracker.skip_task("sweep", "Disabled")
|
||||
|
||||
# Schedule next sync
|
||||
dashboard.schedule_next_sync()
|
||||
|
||||
Reference in New Issue
Block a user