dashboard sync app
This commit is contained in:
680
src/cli/sync_dashboard.py
Normal file
680
src/cli/sync_dashboard.py
Normal file
@@ -0,0 +1,680 @@
|
||||
"""TUI dashboard for sync progress with scrollable logs."""
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Container, Horizontal, Vertical
|
||||
from textual.widgets import (
|
||||
Header,
|
||||
Footer,
|
||||
Static,
|
||||
ProgressBar,
|
||||
Log,
|
||||
ListView,
|
||||
ListItem,
|
||||
Label,
|
||||
)
|
||||
from textual.reactive import reactive
|
||||
from textual.binding import Binding
|
||||
from rich.text import Text
|
||||
from datetime import datetime, timedelta
|
||||
import asyncio
|
||||
from typing import Dict, Any, Optional, List, Callable
|
||||
|
||||
# Default sync interval in seconds (5 minutes)
|
||||
DEFAULT_SYNC_INTERVAL = 300
|
||||
|
||||
# Futuristic spinner frames
|
||||
# SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
||||
# Alternative spinners you could use:
|
||||
# SPINNER_FRAMES = ["◢", "◣", "◤", "◥"] # Rotating triangle
|
||||
SPINNER_FRAMES = ["▰▱▱▱▱", "▰▰▱▱▱", "▰▰▰▱▱", "▰▰▰▰▱", "▰▰▰▰▰", "▱▰▰▰▰", "▱▱▰▰▰", "▱▱▱▰▰", "▱▱▱▱▰"] # Loading bar
|
||||
# SPINNER_FRAMES = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"] # Braille dots
|
||||
# SPINNER_FRAMES = ["◐", "◓", "◑", "◒"] # Circle quarters
|
||||
# SPINNER_FRAMES = ["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"] # Braille orbit
|
||||
|
||||
|
||||
class TaskStatus:
|
||||
"""Status constants for tasks."""
|
||||
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
ERROR = "error"
|
||||
|
||||
|
||||
class TaskListItem(ListItem):
|
||||
"""A list item representing a sync task."""
|
||||
|
||||
def __init__(self, task_id: str, task_name: str, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.task_id = task_id
|
||||
self.task_name = task_name
|
||||
self.status = TaskStatus.PENDING
|
||||
self.progress = 0
|
||||
self.total = 100
|
||||
self.spinner_frame = 0
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the task item layout."""
|
||||
yield Static(self._build_content_text(), id=f"task-content-{self.task_id}")
|
||||
|
||||
def _get_status_icon(self) -> str:
|
||||
"""Get icon based on status."""
|
||||
if self.status == TaskStatus.RUNNING:
|
||||
return SPINNER_FRAMES[self.spinner_frame % len(SPINNER_FRAMES)]
|
||||
icons = {
|
||||
TaskStatus.PENDING: "○",
|
||||
TaskStatus.COMPLETED: "✓",
|
||||
TaskStatus.ERROR: "✗",
|
||||
}
|
||||
return icons.get(self.status, "○")
|
||||
|
||||
def advance_spinner(self) -> None:
|
||||
"""Advance the spinner to the next frame."""
|
||||
self.spinner_frame = (self.spinner_frame + 1) % len(SPINNER_FRAMES)
|
||||
|
||||
def _get_status_color(self) -> str:
|
||||
"""Get color based on status."""
|
||||
colors = {
|
||||
TaskStatus.PENDING: "dim",
|
||||
TaskStatus.RUNNING: "cyan",
|
||||
TaskStatus.COMPLETED: "bright_white",
|
||||
TaskStatus.ERROR: "red",
|
||||
}
|
||||
return colors.get(self.status, "white")
|
||||
|
||||
def _build_content_text(self) -> Text:
|
||||
"""Build the task content text."""
|
||||
icon = self._get_status_icon()
|
||||
color = self._get_status_color()
|
||||
|
||||
# Use green checkmark for completed, but white text for readability
|
||||
if self.status == TaskStatus.RUNNING:
|
||||
progress_pct = (
|
||||
int((self.progress / self.total) * 100) if self.total > 0 else 0
|
||||
)
|
||||
text = Text()
|
||||
text.append(f"{icon} ", style="cyan")
|
||||
text.append(f"{self.task_name} [{progress_pct}%]", style=color)
|
||||
return text
|
||||
elif self.status == TaskStatus.COMPLETED:
|
||||
text = Text()
|
||||
text.append(f"{icon} ", style="green") # Green checkmark
|
||||
text.append(f"{self.task_name} [Done]", style=color)
|
||||
return text
|
||||
elif self.status == TaskStatus.ERROR:
|
||||
text = Text()
|
||||
text.append(f"{icon} ", style="red")
|
||||
text.append(f"{self.task_name} [Error]", style=color)
|
||||
return text
|
||||
else:
|
||||
return Text(f"{icon} {self.task_name}", style=color)
|
||||
|
||||
def update_display(self) -> None:
|
||||
"""Update the display of this item."""
|
||||
try:
|
||||
content = self.query_one(f"#task-content-{self.task_id}", Static)
|
||||
content.update(self._build_content_text())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class SyncDashboard(App):
|
||||
"""TUI dashboard for sync operations."""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("q", "quit", "Quit"),
|
||||
Binding("ctrl+c", "quit", "Quit"),
|
||||
Binding("s", "sync_now", "Sync Now"),
|
||||
Binding("r", "refresh", "Refresh"),
|
||||
Binding("+", "increase_interval", "+Interval"),
|
||||
Binding("-", "decrease_interval", "-Interval"),
|
||||
Binding("up", "cursor_up", "Up", show=False),
|
||||
Binding("down", "cursor_down", "Down", show=False),
|
||||
]
|
||||
|
||||
CSS = """
|
||||
.dashboard {
|
||||
height: 100%;
|
||||
layout: horizontal;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 30;
|
||||
height: 100%;
|
||||
border: solid $primary;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
text-style: bold;
|
||||
padding: 1;
|
||||
background: $primary-darken-2;
|
||||
}
|
||||
|
||||
.countdown-container {
|
||||
height: 3;
|
||||
padding: 0 1;
|
||||
border-top: solid $primary;
|
||||
background: $surface;
|
||||
}
|
||||
|
||||
.countdown-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.main-panel {
|
||||
width: 1fr;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.task-header {
|
||||
height: 5;
|
||||
padding: 1;
|
||||
border-bottom: solid $primary;
|
||||
}
|
||||
|
||||
.task-name {
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
.progress-row {
|
||||
height: 3;
|
||||
padding: 0 1;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
height: 1fr;
|
||||
border: solid $primary;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.log-title {
|
||||
padding: 0 1;
|
||||
background: $primary-darken-2;
|
||||
}
|
||||
|
||||
ListView {
|
||||
height: 1fr;
|
||||
}
|
||||
|
||||
ListItem {
|
||||
padding: 0 1;
|
||||
}
|
||||
|
||||
ListItem:hover {
|
||||
background: $primary-darken-1;
|
||||
}
|
||||
|
||||
Log {
|
||||
height: 1fr;
|
||||
border: none;
|
||||
}
|
||||
|
||||
ProgressBar {
|
||||
width: 1fr;
|
||||
padding: 0 1;
|
||||
}
|
||||
"""
|
||||
|
||||
selected_task: reactive[str] = reactive("archive")
|
||||
sync_interval: reactive[int] = reactive(DEFAULT_SYNC_INTERVAL)
|
||||
next_sync_time: reactive[float] = reactive(0.0)
|
||||
|
||||
def __init__(self, sync_interval: int = DEFAULT_SYNC_INTERVAL):
|
||||
super().__init__()
|
||||
self._mounted: asyncio.Event = asyncio.Event()
|
||||
self._task_logs: Dict[str, List[str]] = {}
|
||||
self._task_items: Dict[str, TaskListItem] = {}
|
||||
self._sync_callback: Optional[Callable] = None
|
||||
self._countdown_task: Optional[asyncio.Task] = None
|
||||
self._spinner_task: Optional[asyncio.Task] = None
|
||||
self._initial_sync_interval = sync_interval
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the dashboard layout."""
|
||||
yield Header()
|
||||
|
||||
with Horizontal(classes="dashboard"):
|
||||
# Sidebar with task list
|
||||
with Vertical(classes="sidebar"):
|
||||
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"),
|
||||
# Stage 2: Fetch from server
|
||||
TaskListItem("inbox", "Inbox Sync", id="task-inbox"),
|
||||
TaskListItem("calendar", "Calendar Sync", id="task-calendar"),
|
||||
# Stage 3: Task management
|
||||
TaskListItem("godspeed", "Godspeed Sync", id="task-godspeed"),
|
||||
TaskListItem("sweep", "Task Sweep", id="task-sweep"),
|
||||
id="task-list",
|
||||
)
|
||||
# Countdown timer at bottom of sidebar
|
||||
with Vertical(classes="countdown-container"):
|
||||
yield Static(
|
||||
"Next sync: --:--", id="countdown", classes="countdown-text"
|
||||
)
|
||||
|
||||
# Main panel with selected task details
|
||||
with Vertical(classes="main-panel"):
|
||||
# Task header with name and progress
|
||||
with Vertical(classes="task-header"):
|
||||
yield Static(
|
||||
"Archive Mail", id="selected-task-name", classes="task-name"
|
||||
)
|
||||
with Horizontal(classes="progress-row"):
|
||||
yield Static("Progress:", id="progress-label")
|
||||
yield ProgressBar(total=100, id="task-progress")
|
||||
yield Static("0%", id="progress-percent")
|
||||
|
||||
# Log for selected task
|
||||
with Vertical(classes="log-container"):
|
||||
yield Static("Activity Log", classes="log-title")
|
||||
yield Log(id="task-log")
|
||||
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Initialize the dashboard."""
|
||||
# Store references to task items
|
||||
task_list = self.query_one("#task-list", ListView)
|
||||
for item in task_list.children:
|
||||
if isinstance(item, TaskListItem):
|
||||
self._task_items[item.task_id] = item
|
||||
self._task_logs[item.task_id] = []
|
||||
|
||||
# Initialize sync interval
|
||||
self.sync_interval = self._initial_sync_interval
|
||||
self.schedule_next_sync()
|
||||
|
||||
# Start countdown timer and spinner animation
|
||||
self._countdown_task = asyncio.create_task(self._update_countdown())
|
||||
self._spinner_task = asyncio.create_task(self._animate_spinners())
|
||||
|
||||
self._log_to_task("archive", "Dashboard initialized. Waiting to start sync...")
|
||||
self._mounted.set()
|
||||
|
||||
def on_list_view_selected(self, event: ListView.Selected) -> None:
|
||||
"""Handle task selection from the list."""
|
||||
if isinstance(event.item, TaskListItem):
|
||||
self.selected_task = event.item.task_id
|
||||
self._update_main_panel()
|
||||
|
||||
def on_list_view_highlighted(self, event: ListView.Highlighted) -> None:
|
||||
"""Handle task highlight from the list."""
|
||||
if isinstance(event.item, TaskListItem):
|
||||
self.selected_task = event.item.task_id
|
||||
self._update_main_panel()
|
||||
|
||||
def _update_main_panel(self) -> None:
|
||||
"""Update the main panel to show selected task details."""
|
||||
task_item = self._task_items.get(self.selected_task)
|
||||
if not task_item:
|
||||
return
|
||||
|
||||
# Update task name
|
||||
try:
|
||||
name_widget = self.query_one("#selected-task-name", Static)
|
||||
name_widget.update(Text(task_item.task_name, style="bold"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Update progress bar
|
||||
try:
|
||||
progress_bar = self.query_one("#task-progress", ProgressBar)
|
||||
progress_bar.total = task_item.total
|
||||
progress_bar.progress = task_item.progress
|
||||
|
||||
percent_widget = self.query_one("#progress-percent", Static)
|
||||
pct = (
|
||||
int((task_item.progress / task_item.total) * 100)
|
||||
if task_item.total > 0
|
||||
else 0
|
||||
)
|
||||
percent_widget.update(f"{pct}%")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Update log with task-specific logs
|
||||
try:
|
||||
log_widget = self.query_one("#task-log", Log)
|
||||
log_widget.clear()
|
||||
for entry in self._task_logs.get(self.selected_task, []):
|
||||
log_widget.write_line(entry)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _log_to_task(self, task_id: str, message: str, level: str = "INFO") -> None:
|
||||
"""Add a log entry to a specific task."""
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
formatted = f"[{timestamp}] {level}: {message}"
|
||||
|
||||
if task_id not in self._task_logs:
|
||||
self._task_logs[task_id] = []
|
||||
self._task_logs[task_id].append(formatted)
|
||||
|
||||
# If this is the selected task, also write to the visible log
|
||||
if task_id == self.selected_task:
|
||||
try:
|
||||
log_widget = self.query_one("#task-log", Log)
|
||||
log_widget.write_line(formatted)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def start_task(self, task_id: str, total: int = 100) -> None:
|
||||
"""Start a task."""
|
||||
if task_id in self._task_items:
|
||||
item = self._task_items[task_id]
|
||||
item.status = TaskStatus.RUNNING
|
||||
item.progress = 0
|
||||
item.total = total
|
||||
item.update_display()
|
||||
self._log_to_task(task_id, f"Starting {item.task_name}...")
|
||||
if task_id == self.selected_task:
|
||||
self._update_main_panel()
|
||||
|
||||
def update_task(self, task_id: str, progress: int, message: str = "") -> None:
|
||||
"""Update task progress."""
|
||||
if task_id in self._task_items:
|
||||
item = self._task_items[task_id]
|
||||
item.progress = progress
|
||||
item.update_display()
|
||||
if message:
|
||||
self._log_to_task(task_id, message)
|
||||
if task_id == self.selected_task:
|
||||
self._update_main_panel()
|
||||
|
||||
def complete_task(self, task_id: str, message: str = "") -> None:
|
||||
"""Mark a task as complete."""
|
||||
if task_id in self._task_items:
|
||||
item = self._task_items[task_id]
|
||||
item.status = TaskStatus.COMPLETED
|
||||
item.progress = item.total
|
||||
item.update_display()
|
||||
self._log_to_task(
|
||||
task_id,
|
||||
f"Completed: {message}" if message else "Completed successfully",
|
||||
)
|
||||
if task_id == self.selected_task:
|
||||
self._update_main_panel()
|
||||
|
||||
def error_task(self, task_id: str, error: str) -> None:
|
||||
"""Mark a task as errored."""
|
||||
if task_id in self._task_items:
|
||||
item = self._task_items[task_id]
|
||||
item.status = TaskStatus.ERROR
|
||||
item.update_display()
|
||||
self._log_to_task(task_id, f"ERROR: {error}", "ERROR")
|
||||
if task_id == self.selected_task:
|
||||
self._update_main_panel()
|
||||
|
||||
def skip_task(self, task_id: str, reason: str = "") -> None:
|
||||
"""Mark a task as skipped (completed with no work)."""
|
||||
if task_id in self._task_items:
|
||||
item = self._task_items[task_id]
|
||||
item.status = TaskStatus.COMPLETED
|
||||
item.update_display()
|
||||
self._log_to_task(task_id, f"Skipped: {reason}" if reason else "Skipped")
|
||||
if task_id == self.selected_task:
|
||||
self._update_main_panel()
|
||||
|
||||
def action_refresh(self) -> None:
|
||||
"""Refresh the dashboard."""
|
||||
self._update_main_panel()
|
||||
|
||||
def action_cursor_up(self) -> None:
|
||||
"""Move cursor up in task list."""
|
||||
task_list = self.query_one("#task-list", ListView)
|
||||
task_list.action_cursor_up()
|
||||
|
||||
def action_cursor_down(self) -> None:
|
||||
"""Move cursor down in task list."""
|
||||
task_list = self.query_one("#task-list", ListView)
|
||||
task_list.action_cursor_down()
|
||||
|
||||
def action_sync_now(self) -> None:
|
||||
"""Trigger an immediate sync."""
|
||||
if self._sync_callback:
|
||||
asyncio.create_task(self._run_sync_callback())
|
||||
else:
|
||||
self._log_to_task("archive", "No sync callback configured")
|
||||
|
||||
async def _run_sync_callback(self) -> None:
|
||||
"""Run the sync callback if set."""
|
||||
if self._sync_callback:
|
||||
if asyncio.iscoroutinefunction(self._sync_callback):
|
||||
await self._sync_callback()
|
||||
else:
|
||||
self._sync_callback()
|
||||
|
||||
def action_increase_interval(self) -> None:
|
||||
"""Increase sync interval by 1 minute."""
|
||||
self.sync_interval = min(self.sync_interval + 60, 3600) # Max 1 hour
|
||||
self._update_countdown_display()
|
||||
self._log_to_task(
|
||||
self.selected_task,
|
||||
f"Sync interval: {self.sync_interval // 60} min",
|
||||
)
|
||||
|
||||
def action_decrease_interval(self) -> None:
|
||||
"""Decrease sync interval by 1 minute."""
|
||||
self.sync_interval = max(self.sync_interval - 60, 60) # Min 1 minute
|
||||
self._update_countdown_display()
|
||||
self._log_to_task(
|
||||
self.selected_task,
|
||||
f"Sync interval: {self.sync_interval // 60} min",
|
||||
)
|
||||
|
||||
def set_sync_callback(self, callback: Callable) -> None:
|
||||
"""Set the callback to run when sync is triggered."""
|
||||
self._sync_callback = callback
|
||||
|
||||
def schedule_next_sync(self) -> None:
|
||||
"""Schedule the next sync time."""
|
||||
import time
|
||||
|
||||
self.next_sync_time = time.time() + self.sync_interval
|
||||
|
||||
def reset_all_tasks(self) -> None:
|
||||
"""Reset all tasks to pending state."""
|
||||
for task_id, item in self._task_items.items():
|
||||
item.status = TaskStatus.PENDING
|
||||
item.progress = 0
|
||||
item.update_display()
|
||||
self._update_main_panel()
|
||||
|
||||
async def _update_countdown(self) -> None:
|
||||
"""Update the countdown timer every second."""
|
||||
import time
|
||||
|
||||
while True:
|
||||
try:
|
||||
self._update_countdown_display()
|
||||
await asyncio.sleep(1)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
def _update_countdown_display(self) -> None:
|
||||
"""Update the countdown display widget."""
|
||||
import time
|
||||
|
||||
try:
|
||||
countdown_widget = self.query_one("#countdown", Static)
|
||||
remaining = max(0, self.next_sync_time - time.time())
|
||||
|
||||
if remaining <= 0:
|
||||
countdown_widget.update(f"Syncing... ({self.sync_interval // 60}m)")
|
||||
else:
|
||||
minutes = int(remaining // 60)
|
||||
seconds = int(remaining % 60)
|
||||
countdown_widget.update(
|
||||
f"Next: {minutes:02d}:{seconds:02d} ({self.sync_interval // 60}m)"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def _animate_spinners(self) -> None:
|
||||
"""Animate spinners for running tasks."""
|
||||
while True:
|
||||
try:
|
||||
# Update all running task spinners
|
||||
for task_id, item in self._task_items.items():
|
||||
if item.status == TaskStatus.RUNNING:
|
||||
item.advance_spinner()
|
||||
item.update_display()
|
||||
await asyncio.sleep(0.08) # ~12 FPS for smooth animation
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception:
|
||||
await asyncio.sleep(0.08)
|
||||
|
||||
|
||||
class SyncProgressTracker:
|
||||
"""Track sync progress and update the dashboard."""
|
||||
|
||||
def __init__(self, dashboard: SyncDashboard):
|
||||
self.dashboard = dashboard
|
||||
|
||||
def start_task(self, task_id: str, total: int = 100) -> None:
|
||||
"""Start tracking a task."""
|
||||
self.dashboard.start_task(task_id, total)
|
||||
|
||||
def update_task(self, task_id: str, progress: int, message: str = "") -> None:
|
||||
"""Update task progress."""
|
||||
self.dashboard.update_task(task_id, progress, message)
|
||||
|
||||
def complete_task(self, task_id: str, message: str = "") -> None:
|
||||
"""Mark a task as complete."""
|
||||
self.dashboard.complete_task(task_id, message)
|
||||
|
||||
def error_task(self, task_id: str, error: str) -> None:
|
||||
"""Mark a task as failed."""
|
||||
self.dashboard.error_task(task_id, error)
|
||||
|
||||
def skip_task(self, task_id: str, reason: str = "") -> None:
|
||||
"""Mark a task as skipped."""
|
||||
self.dashboard.skip_task(task_id, reason)
|
||||
|
||||
|
||||
# Global dashboard instance
|
||||
_dashboard_instance: Optional[SyncDashboard] = None
|
||||
_progress_tracker: Optional[SyncProgressTracker] = None
|
||||
|
||||
|
||||
def get_dashboard() -> Optional[SyncDashboard]:
|
||||
"""Get the global dashboard instance."""
|
||||
global _dashboard_instance
|
||||
return _dashboard_instance
|
||||
|
||||
|
||||
def get_progress_tracker() -> Optional[SyncProgressTracker]:
|
||||
"""Get the global progress_tracker"""
|
||||
global _progress_tracker
|
||||
return _progress_tracker
|
||||
|
||||
|
||||
async def run_dashboard_sync():
|
||||
"""Run sync with dashboard UI."""
|
||||
global _dashboard_instance, _progress_tracker
|
||||
|
||||
dashboard = SyncDashboard()
|
||||
tracker = SyncProgressTracker(dashboard)
|
||||
|
||||
_dashboard_instance = dashboard
|
||||
_progress_tracker = tracker
|
||||
|
||||
async def do_sync():
|
||||
"""Run the actual sync process."""
|
||||
try:
|
||||
# Reset all tasks before starting
|
||||
dashboard.reset_all_tasks()
|
||||
|
||||
# Simulate sync progress for demo (replace with actual sync calls)
|
||||
|
||||
# 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")
|
||||
|
||||
# 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")
|
||||
|
||||
# Stage 2: Fetch from server
|
||||
|
||||
# Inbox sync
|
||||
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)
|
||||
tracker.complete_task("inbox", "150 emails processed")
|
||||
|
||||
# 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")
|
||||
|
||||
# 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")
|
||||
|
||||
# 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")
|
||||
|
||||
# Schedule next sync
|
||||
dashboard.schedule_next_sync()
|
||||
|
||||
except Exception as e:
|
||||
tracker.error_task("archive", str(e))
|
||||
|
||||
# Set the sync callback so 's' key triggers it
|
||||
dashboard.set_sync_callback(do_sync)
|
||||
|
||||
async def sync_loop():
|
||||
"""Run sync on interval."""
|
||||
import time
|
||||
|
||||
# Wait for the dashboard to be mounted before updating widgets
|
||||
await dashboard._mounted.wait()
|
||||
|
||||
# Run initial sync
|
||||
await do_sync()
|
||||
|
||||
# Then loop waiting for next sync time
|
||||
while True:
|
||||
try:
|
||||
remaining = dashboard.next_sync_time - time.time()
|
||||
if remaining <= 0:
|
||||
await do_sync()
|
||||
else:
|
||||
await asyncio.sleep(1)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Run dashboard and sync loop concurrently
|
||||
await asyncio.gather(dashboard.run_async(), sync_loop())
|
||||
Reference in New Issue
Block a user