Add task count to task list panel header

Show 'X todo / Y done' in the Tasks panel title bar.

Also includes prior uncommitted work:
- Pagination in fetch_tasks (maxResults=100 + pageToken loop)
- fetch_tasks_since for incremental pull sync
- SyncStats struct with version/last_sync/last_pull/changed counts
- Periodic push (30s) and pull (5min) sync engine
- event::poll(100ms) for non-blocking UI refresh
- Ctrl+R full sync (push + pull)
- refresh_if_needed() to reload data after background sync
- Retry mechanism (MAX_SYNC_RETRIES=3) for sync queue items
- HTTP status code checks in fetch_lists/fetch_tasks/fetch_tasks_since
- Fix move_task URL to use reqwest query()
- Remove CASCADE via replace_all_lists (use insert_list instead)
- has_pending_sync() to prevent pull during pending push
This commit is contained in:
Ruben Rosario
2026-06-21 14:21:14 +01:00
parent ae9910bcbc
commit 6eee90f128
7 changed files with 394 additions and 69 deletions
+38
View File
@@ -10,6 +10,15 @@ use crate::infrastructure::api::ApiClient;
use crate::infrastructure::db::Db;
use crate::ui::{Focus, NetworkStatus, Popup};
#[derive(Debug, Clone, Default)]
pub struct SyncStats {
pub last_sync_time: Option<chrono::NaiveDateTime>,
pub last_pull_time: Option<chrono::NaiveDateTime>,
pub lists_changed: usize,
pub tasks_changed: usize,
pub version: u64,
}
pub struct App {
pub lists: Vec<TaskList>,
pub tasks: Vec<Task>,
@@ -29,6 +38,8 @@ pub struct App {
pub api_client: Arc<ApiClient>,
pub needs_auth: bool,
pub auth_error: Option<String>,
pub sync_stats: SyncStats,
last_sync_version: u64,
auth_tx: std_mpsc::Sender<AuthEvent>,
auth_rx: std_mpsc::Receiver<AuthEvent>,
sync_tx: mpsc::Sender<SyncCommand>,
@@ -42,6 +53,7 @@ enum AuthEvent {
#[allow(dead_code)]
pub enum SyncCommand {
TriggerSync,
FullSync,
InitialSync,
Shutdown,
}
@@ -86,6 +98,8 @@ impl App {
api_client,
needs_auth: !has_token,
auth_error: None,
sync_stats: SyncStats::default(),
last_sync_version: 0,
auth_tx,
auth_rx,
sync_tx,
@@ -142,10 +156,31 @@ impl App {
}
}
pub fn refresh_if_needed(&mut self) {
if self.sync_stats.version != self.last_sync_version {
self.last_sync_version = self.sync_stats.version;
self.load_lists();
if !self.lists.is_empty() && self.selected_list < self.lists.len() {
self.tasks = self.db.get_tasks(&self.lists[self.selected_list].id);
} else {
self.tasks.clear();
}
if self.selected_task >= self.tasks.len() && !self.tasks.is_empty() {
self.selected_task = self.tasks.len() - 1;
} else if self.tasks.is_empty() {
self.selected_task = 0;
}
}
}
fn trigger_sync(&self) {
let _ = self.sync_tx.try_send(SyncCommand::TriggerSync);
}
fn trigger_full_sync(&self) {
let _ = self.sync_tx.try_send(SyncCommand::FullSync);
}
pub fn handle_key(&mut self, key: KeyEvent) {
if let Some(ref popup) = self.show_popup.clone() {
self.handle_popup_key(key, popup);
@@ -238,6 +273,9 @@ impl App {
KeyCode::Esc => {
self.show_popup = None;
}
KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.trigger_full_sync();
}
KeyCode::Char('q') | KeyCode::Char('Q') => {
self.should_quit = true;
}