tempo_cli/ui/
dashboard.rs

1use anyhow::Result;
2use chrono::{Local, Timelike};
3use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
4use log::debug;
5use ratatui::{
6    backend::Backend,
7    buffer::Buffer,
8    layout::{Alignment, Constraint, Direction, Layout, Rect},
9    style::{Color, Modifier, Style},
10    text::{Line, Span},
11    widgets::{Block, Borders, Cell, List, ListItem, Paragraph, Row, Table, Widget},
12    Frame, Terminal,
13};
14use std::time::{Duration, Instant};
15
16use crate::{
17    models::{Project, Session},
18    ui::animations::{AnimatedSpinner, PulsingIndicator, ViewTransition, TransitionDirection},
19    ui::formatter::Formatter,
20    ui::widgets::{ColorScheme, Spinner},
21    utils::ipc::{
22        get_socket_path, is_daemon_running, IpcClient, IpcMessage, IpcResponse, ProjectWithStats,
23    },
24};
25
26#[derive(Clone, PartialEq)]
27pub enum DashboardView {
28    FocusedSession,
29    Overview,
30    History,
31    Projects,
32}
33
34#[derive(Clone)]
35pub struct SessionFilter {
36    pub start_date: Option<chrono::NaiveDate>,
37    pub end_date: Option<chrono::NaiveDate>,
38    pub project_filter: Option<String>,
39    pub duration_filter: Option<(i64, i64)>, // min, max seconds
40    pub search_text: String,
41}
42
43impl Default for SessionFilter {
44    fn default() -> Self {
45        Self {
46            start_date: None,
47            end_date: None,
48            project_filter: None,
49            duration_filter: None,
50            search_text: String::new(),
51        }
52    }
53}
54
55pub struct Dashboard {
56    client: IpcClient,
57    current_session: Option<Session>,
58    current_project: Option<Project>,
59    daily_stats: (i64, i64, i64),
60    weekly_stats: i64,
61    today_sessions: Vec<Session>,
62    recent_projects: Vec<ProjectWithStats>,
63    available_projects: Vec<Project>,
64    selected_project_index: usize,
65    show_project_switcher: bool,
66    current_view: DashboardView,
67
68    // History browser state
69    history_sessions: Vec<Session>,
70    selected_session_index: usize,
71    session_filter: SessionFilter,
72    filter_input_mode: bool,
73
74    // Project grid state
75    selected_project_row: usize,
76    selected_project_col: usize,
77    projects_per_row: usize,
78
79    spinner: Spinner,
80    last_update: Instant,
81
82    // Animation state
83    animated_spinner: AnimatedSpinner,
84    pulsing_indicator: PulsingIndicator,
85    view_transition: Option<ViewTransition>,
86    previous_view: DashboardView,
87    frame_count: u64,
88}
89
90impl Dashboard {
91    pub async fn new() -> Result<Self> {
92        let socket_path = get_socket_path()?;
93        let client = if socket_path.exists() && is_daemon_running() {
94            IpcClient::connect(&socket_path)
95                .await
96                .unwrap_or_else(|_| IpcClient::new().unwrap())
97        } else {
98            IpcClient::new()?
99        };
100        Ok(Self {
101            client,
102            current_session: None,
103            current_project: None,
104            daily_stats: (0, 0, 0),
105            weekly_stats: 0,
106            today_sessions: Vec::new(),
107            recent_projects: Vec::new(),
108            available_projects: Vec::new(),
109            selected_project_index: 0,
110            show_project_switcher: false,
111            current_view: DashboardView::FocusedSession,
112
113            // Initialize history browser state
114            history_sessions: Vec::new(),
115            selected_session_index: 0,
116            session_filter: SessionFilter::default(),
117            filter_input_mode: false,
118
119            // Initialize project grid state
120            selected_project_row: 0,
121            selected_project_col: 0,
122            projects_per_row: 3,
123
124            spinner: Spinner::new(),
125            last_update: Instant::now(),
126
127            // Initialize animation state
128            animated_spinner: AnimatedSpinner::braille(),
129            pulsing_indicator: PulsingIndicator::new(),
130            view_transition: None,
131            previous_view: DashboardView::FocusedSession,
132            frame_count: 0,
133        })
134    }
135
136    pub async fn run<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> Result<()> {
137        use tokio::time::{interval, Duration as TokioDuration};
138
139        // 60 FPS frame loop (16.67ms per frame)
140        let mut frame_interval = interval(TokioDuration::from_millis(16));
141
142        loop {
143            // Wait for next frame
144            frame_interval.tick().await;
145
146            // Update animations and state
147            self.update_animations();
148            self.update_state().await?;
149
150            // Render current frame
151            terminal.draw(|f| self.render_dashboard_sync(f))?;
152
153            // Poll for input events (non-blocking)
154            if event::poll(Duration::from_millis(0))? {
155                match event::read()? {
156                    Event::Key(key) if key.kind == KeyEventKind::Press => {
157                        if self.show_project_switcher {
158                            self.handle_project_switcher_input(key).await?;
159                        } else {
160                            // Handle global exit here
161                            match key.code {
162                                KeyCode::Char('q') => break,
163                                KeyCode::Esc => {
164                                    if self.current_view == DashboardView::FocusedSession {
165                                        self.transition_to_view(DashboardView::Overview);
166                                    } else {
167                                        break;
168                                    }
169                                }
170                                _ => self.handle_dashboard_input(key).await?,
171                            }
172                        }
173                    }
174                    _ => {}
175                }
176            }
177        }
178        Ok(())
179    }
180
181    /// Update all animation states (called every frame @ 60 FPS)
182    fn update_animations(&mut self) {
183        // Increment frame counter
184        self.frame_count += 1;
185
186        // Tick spinners
187        self.animated_spinner.tick();
188
189        // Update view transition if active
190        if let Some(transition) = &self.view_transition {
191            if transition.is_complete() {
192                self.view_transition = None;
193            }
194        }
195    }
196
197    async fn update_state(&mut self) -> Result<()> {
198        // Send activity heartbeat (throttled)
199        if self.last_update.elapsed() >= Duration::from_secs(3) {
200            if let Err(e) = self.send_activity_heartbeat().await {
201                debug!("Heartbeat error: {}", e);
202            }
203            self.last_update = Instant::now();
204        }
205
206        // Tick legacy spinner for compatibility
207        self.spinner.next();
208
209        // Get current status
210        self.current_session = self.get_current_session().await?;
211
212        // Clone session to avoid borrow conflict
213        let session_clone = self.current_session.clone();
214        if let Some(session) = session_clone {
215            self.current_project = self.get_project_by_session(&session).await?;
216        } else {
217            self.current_project = None;
218        }
219
220        self.daily_stats = self.get_today_stats().await.unwrap_or((0, 0, 0));
221        self.weekly_stats = self.get_weekly_stats().await.unwrap_or(0);
222        self.today_sessions = self.get_today_sessions().await.unwrap_or_default();
223        self.recent_projects = self.get_recent_projects().await.unwrap_or_default();
224
225        // Update history sessions if in history view
226        if self.current_view == DashboardView::History {
227            self.history_sessions = self.get_history_sessions().await.unwrap_or_default();
228        }
229
230        // Update project list if in project view
231        if self.current_view == DashboardView::Projects && self.available_projects.is_empty() {
232            if let Err(_) = self.refresh_projects().await {
233                // Ignore errors and use empty list
234            }
235        }
236
237        Ok(())
238    }
239
240    async fn get_weekly_stats(&mut self) -> Result<i64> {
241        match self.client.send_message(&IpcMessage::GetWeeklyStats).await {
242            Ok(IpcResponse::WeeklyStats { total_seconds }) => Ok(total_seconds),
243            Ok(response) => {
244                debug!("Unexpected response for GetWeeklyStats: {:?}", response);
245                Err(anyhow::anyhow!("Unexpected response"))
246            }
247            Err(e) => {
248                debug!("Failed to receive GetWeeklyStats response: {}", e);
249                Err(anyhow::anyhow!("Failed to receive response"))
250            }
251        }
252    }
253
254    async fn get_recent_projects(&mut self) -> Result<Vec<ProjectWithStats>> {
255        match self
256            .client
257            .send_message(&IpcMessage::GetRecentProjects)
258            .await
259        {
260            Ok(IpcResponse::RecentProjects(projects)) => Ok(projects),
261            Ok(response) => {
262                debug!("Unexpected response for GetRecentProjects: {:?}", response);
263                Err(anyhow::anyhow!("Unexpected response"))
264            }
265            Err(e) => {
266                debug!("Failed to receive GetRecentProjects response: {}", e);
267                Err(anyhow::anyhow!("Failed to receive response"))
268            }
269        }
270    }
271
272    /// Transition to a new view with animation
273    fn transition_to_view(&mut self, new_view: DashboardView) {
274        if self.current_view == new_view {
275            return;
276        }
277
278        // Determine transition direction based on view order
279        let direction = match (&self.current_view, &new_view) {
280            (DashboardView::FocusedSession, _) => TransitionDirection::SlideLeft,
281            (_, DashboardView::FocusedSession) => TransitionDirection::SlideRight,
282            _ => TransitionDirection::FadeIn,
283        };
284
285        // Create transition animation (200ms duration for smooth, subtle effect)
286        self.view_transition = Some(ViewTransition::new(direction, Duration::from_millis(200)));
287        self.previous_view = self.current_view.clone();
288        self.current_view = new_view;
289    }
290
291    async fn handle_dashboard_input(&mut self, key: KeyEvent) -> Result<()> {
292        // Handle view-specific inputs first
293        match self.current_view {
294            DashboardView::History => {
295                return self.handle_history_input(key).await;
296            }
297            DashboardView::Projects => {
298                return self.handle_project_grid_input(key).await;
299            }
300            _ => {}
301        }
302
303        // Handle global navigation
304        match key.code {
305            // View navigation with transitions
306            KeyCode::Char('1') => self.transition_to_view(DashboardView::FocusedSession),
307            KeyCode::Char('2') => self.transition_to_view(DashboardView::Overview),
308            KeyCode::Char('3') => self.transition_to_view(DashboardView::History),
309            KeyCode::Char('4') => self.transition_to_view(DashboardView::Projects),
310            KeyCode::Char('f') => self.transition_to_view(DashboardView::FocusedSession),
311            KeyCode::Tab => {
312                let next_view = match self.current_view {
313                    DashboardView::FocusedSession => DashboardView::Overview,
314                    DashboardView::Overview => DashboardView::History,
315                    DashboardView::History => DashboardView::Projects,
316                    DashboardView::Projects => DashboardView::FocusedSession,
317                };
318                self.transition_to_view(next_view);
319            }
320            // Project switcher (only in certain views)
321            KeyCode::Char('p') if self.current_view != DashboardView::Projects => {
322                self.refresh_projects().await?;
323                self.show_project_switcher = true;
324            }
325            _ => {}
326        }
327        Ok(())
328    }
329
330    async fn handle_history_input(&mut self, key: KeyEvent) -> Result<()> {
331        match key.code {
332            // Navigation in session list
333            KeyCode::Up | KeyCode::Char('k') => {
334                if !self.history_sessions.is_empty() && self.selected_session_index > 0 {
335                    self.selected_session_index -= 1;
336                }
337            }
338            KeyCode::Down | KeyCode::Char('j') => {
339                if self.selected_session_index < self.history_sessions.len().saturating_sub(1) {
340                    self.selected_session_index += 1;
341                }
342            }
343            // Search mode
344            KeyCode::Char('/') => {
345                self.filter_input_mode = true;
346            }
347            KeyCode::Enter if self.filter_input_mode => {
348                self.filter_input_mode = false;
349                self.history_sessions = self.get_history_sessions().await.unwrap_or_default();
350            }
351            // Character input in search mode
352            KeyCode::Char(c) if self.filter_input_mode => {
353                self.session_filter.search_text.push(c);
354            }
355            KeyCode::Backspace if self.filter_input_mode => {
356                self.session_filter.search_text.pop();
357            }
358            KeyCode::Esc if self.filter_input_mode => {
359                self.filter_input_mode = false;
360                self.session_filter.search_text.clear();
361            }
362            _ => {}
363        }
364        Ok(())
365    }
366
367    async fn handle_project_grid_input(&mut self, key: KeyEvent) -> Result<()> {
368        match key.code {
369            // Grid navigation
370            KeyCode::Up | KeyCode::Char('k') => {
371                if self.selected_project_row > 0 {
372                    self.selected_project_row -= 1;
373                }
374            }
375            KeyCode::Down | KeyCode::Char('j') => {
376                let total_projects = self.available_projects.len();
377                let total_rows =
378                    (total_projects + self.projects_per_row - 1) / self.projects_per_row;
379                if self.selected_project_row < total_rows.saturating_sub(1) {
380                    // Only move down if there's a project on the next row
381                    let next_row_first_index =
382                        (self.selected_project_row + 1) * self.projects_per_row;
383                    if next_row_first_index < total_projects {
384                        self.selected_project_row += 1;
385                    }
386                }
387            }
388            KeyCode::Left | KeyCode::Char('h') => {
389                if self.selected_project_col > 0 {
390                    self.selected_project_col -= 1;
391                }
392            }
393            KeyCode::Right | KeyCode::Char('l') => {
394                let row_start = self.selected_project_row * self.projects_per_row;
395                let row_end =
396                    (row_start + self.projects_per_row).min(self.available_projects.len());
397                let max_col = (row_end - row_start).saturating_sub(1);
398                if self.selected_project_col < max_col {
399                    self.selected_project_col += 1;
400                }
401            }
402            // Project selection
403            KeyCode::Enter => {
404                self.switch_to_grid_selected_project().await?;
405            }
406            _ => {}
407        }
408        Ok(())
409    }
410
411    async fn handle_project_switcher_input(&mut self, key: KeyEvent) -> Result<()> {
412        match key.code {
413            KeyCode::Esc => {
414                self.show_project_switcher = false;
415            }
416            KeyCode::Up | KeyCode::Char('k') => {
417                self.navigate_projects(-1);
418            }
419            KeyCode::Down | KeyCode::Char('j') => {
420                self.navigate_projects(1);
421            }
422            KeyCode::Enter => {
423                self.switch_to_selected_project().await?;
424            }
425            _ => {}
426        }
427        Ok(())
428    }
429
430    async fn ensure_connected(&mut self) -> Result<()> {
431        if !is_daemon_running() {
432            return Err(anyhow::anyhow!("Daemon is not running"));
433        }
434
435        // Test if we have a working connection
436        if self.client.stream.is_some() {
437            return Ok(());
438        }
439
440        // Reconnect if needed
441        let socket_path = get_socket_path()?;
442        if socket_path.exists() {
443            self.client = IpcClient::connect(&socket_path).await?;
444        }
445        Ok(())
446    }
447
448    async fn switch_to_grid_selected_project(&mut self) -> Result<()> {
449        let selected_index =
450            self.selected_project_row * self.projects_per_row + self.selected_project_col;
451        if let Some(selected_project) = self.available_projects.get(selected_index) {
452            let project_id = selected_project.id.unwrap_or(0);
453
454            self.ensure_connected().await?;
455
456            // Switch to the selected project
457            let response = self
458                .client
459                .send_message(&IpcMessage::SwitchProject(project_id))
460                .await?;
461            match response {
462                IpcResponse::Success => {
463                    // Switch to focused view after selection with transition
464                    self.transition_to_view(DashboardView::FocusedSession);
465                }
466                IpcResponse::Error(e) => {
467                    return Err(anyhow::anyhow!("Failed to switch project: {}", e))
468                }
469                _ => return Err(anyhow::anyhow!("Unexpected response")),
470            }
471        }
472        Ok(())
473    }
474
475    fn render_keyboard_hints(&self, area: Rect, buf: &mut Buffer) {
476        let hints = match self.current_view {
477            DashboardView::FocusedSession => vec![
478                ("Esc", "Exit Focus"),
479                ("Tab", "Next View"),
480                ("p", "Projects"),
481            ],
482            DashboardView::History => vec![
483                ("↑/↓", "Navigate"),
484                ("/", "Search"),
485                ("Tab", "Next View"),
486                ("q", "Quit"),
487            ],
488            DashboardView::Projects => vec![
489                ("↑/↓/←/→", "Navigate"),
490                ("Enter", "Select"),
491                ("Tab", "Next View"),
492                ("q", "Quit"),
493            ],
494            _ => vec![
495                ("q", "Quit"),
496                ("f", "Focus"),
497                ("Tab", "Next View"),
498                ("1-4", "View"),
499                ("p", "Projects"),
500            ],
501        };
502
503        let spans: Vec<Span> = hints
504            .iter()
505            .flat_map(|(key, desc)| {
506                vec![
507                    Span::styled(
508                        format!(" {} ", key),
509                        Style::default()
510                            .fg(Color::Yellow)
511                            .add_modifier(Modifier::BOLD),
512                    ),
513                    Span::styled(format!("{} ", desc), Style::default().fg(Color::DarkGray)),
514                ]
515            })
516            .collect();
517
518        let line = Line::from(spans);
519        let block = Block::default()
520            .borders(Borders::TOP)
521            .border_style(Style::default().fg(Color::DarkGray));
522        Paragraph::new(line).block(block).render(area, buf);
523    }
524
525    fn render_dashboard_sync(&mut self, f: &mut Frame) {
526        match self.current_view {
527            DashboardView::FocusedSession => self.render_focused_session_view(f),
528            DashboardView::Overview => self.render_overview_dashboard(f),
529            DashboardView::History => self.render_history_browser(f),
530            DashboardView::Projects => self.render_project_grid(f),
531        }
532
533        // Project switcher overlay (available on most views)
534        if self.show_project_switcher {
535            self.render_project_switcher(f, f.size());
536        }
537    }
538
539    fn render_focused_session_view(&mut self, f: &mut Frame) {
540        let chunks = Layout::default()
541            .direction(Direction::Vertical)
542            .constraints([
543                Constraint::Length(3), // Header with ESC hint
544                Constraint::Length(2), // Spacer
545                Constraint::Length(6), // Project info box
546                Constraint::Length(2), // Spacer
547                Constraint::Length(8), // Large timer box
548                Constraint::Length(2), // Spacer
549                Constraint::Length(8), // Session details
550                Constraint::Min(0),    // Bottom spacer
551                Constraint::Length(1), // Footer
552            ])
553            .split(f.size());
554
555        // Top header with ESC hint
556        let header_layout = Layout::default()
557            .direction(Direction::Horizontal)
558            .constraints([Constraint::Percentage(100)])
559            .split(chunks[0]);
560
561        f.render_widget(
562            Paragraph::new("Press ESC to exit focused mode.")
563                .alignment(Alignment::Center)
564                .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
565            header_layout[0],
566        );
567
568        if let (Some(session), Some(project)) = (&self.current_session, &self.current_project) {
569            // Project info box
570            let project_area = self.centered_rect(60, 20, chunks[2]);
571            let project_block = Block::default()
572                .borders(Borders::ALL)
573                .border_style(Style::default().fg(ColorScheme::GRAY_TEXT))
574                .style(Style::default().bg(ColorScheme::CLEAN_BG));
575
576            let project_layout = Layout::default()
577                .direction(Direction::Vertical)
578                .constraints([
579                    Constraint::Length(1),
580                    Constraint::Length(1),
581                    Constraint::Length(1),
582                    Constraint::Length(1),
583                ])
584                .margin(1)
585                .split(project_area);
586
587            f.render_widget(project_block, project_area);
588
589            // Project name
590            f.render_widget(
591                Paragraph::new(project.name.clone())
592                    .alignment(Alignment::Center)
593                    .style(
594                        Style::default()
595                            .fg(ColorScheme::WHITE_TEXT)
596                            .add_modifier(Modifier::BOLD),
597                    ),
598                project_layout[0],
599            );
600
601            // Project description or refactor info
602            let default_description = "Refactor authentication module".to_string();
603            let description = project.description.as_ref().unwrap_or(&default_description);
604            f.render_widget(
605                Paragraph::new(description.clone())
606                    .alignment(Alignment::Center)
607                    .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
608                project_layout[1],
609            );
610
611            // Large timer box with pulsing border (breathing effect)
612            let timer_area = self.centered_rect(40, 20, chunks[4]);
613
614            // Calculate pulsing border color for active session
615            use crate::ui::animations::pulse_color;
616            let pulse_start = ColorScheme::CLEAN_GREEN;
617            let pulse_end = ColorScheme::PRIMARY_FOCUS;
618            let border_color = pulse_color(
619                pulse_start,
620                pulse_end,
621                self.pulsing_indicator.start_time.elapsed(),
622                Duration::from_millis(2000),
623            );
624
625            let timer_block = Block::default()
626                .borders(Borders::ALL)
627                .border_style(Style::default().fg(border_color))
628                .style(Style::default().bg(Color::Black));
629
630            let timer_inner = timer_block.inner(timer_area);
631            f.render_widget(timer_block, timer_area);
632
633            // Calculate and display large timer
634            let now = Local::now();
635            let elapsed_seconds = (now.timestamp() - session.start_time.timestamp())
636                - session.paused_duration.num_seconds();
637            let duration_str = Formatter::format_duration_clock(elapsed_seconds);
638
639            f.render_widget(
640                Paragraph::new(duration_str)
641                    .alignment(Alignment::Center)
642                    .style(
643                        Style::default()
644                            .fg(ColorScheme::CLEAN_GREEN)
645                            .add_modifier(Modifier::BOLD),
646                    ),
647                timer_inner,
648            );
649
650            // Session details box
651            let details_area = self.centered_rect(60, 25, chunks[6]);
652            let details_block = Block::default()
653                .borders(Borders::ALL)
654                .border_style(Style::default().fg(ColorScheme::GRAY_TEXT))
655                .style(Style::default().bg(ColorScheme::CLEAN_BG));
656
657            let details_layout = Layout::default()
658                .direction(Direction::Vertical)
659                .constraints([
660                    Constraint::Length(2), // Start time
661                    Constraint::Length(2), // Session type
662                    Constraint::Length(2), // Tags
663                ])
664                .margin(1)
665                .split(details_area);
666
667            f.render_widget(details_block, details_area);
668
669            // Start time
670            let start_time_layout = Layout::default()
671                .direction(Direction::Horizontal)
672                .constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
673                .split(details_layout[0]);
674
675            f.render_widget(
676                Paragraph::new("Start Time").style(Style::default().fg(ColorScheme::GRAY_TEXT)),
677                start_time_layout[0],
678            );
679            f.render_widget(
680                Paragraph::new(
681                    session
682                        .start_time
683                        .with_timezone(&Local)
684                        .format("%H:%M")
685                        .to_string(),
686                )
687                .alignment(Alignment::Right)
688                .style(Style::default().fg(ColorScheme::WHITE_TEXT)),
689                start_time_layout[1],
690            );
691
692            // Session type
693            let session_type_layout = Layout::default()
694                .direction(Direction::Horizontal)
695                .constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
696                .split(details_layout[1]);
697
698            f.render_widget(
699                Paragraph::new("Session Type").style(Style::default().fg(ColorScheme::GRAY_TEXT)),
700                session_type_layout[0],
701            );
702            f.render_widget(
703                Paragraph::new("Deep Work")
704                    .alignment(Alignment::Right)
705                    .style(Style::default().fg(ColorScheme::WHITE_TEXT)),
706                session_type_layout[1],
707            );
708
709            // Tags
710            let tags_layout = Layout::default()
711                .direction(Direction::Horizontal)
712                .constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
713                .split(details_layout[2]);
714
715            f.render_widget(
716                Paragraph::new("Tags").style(Style::default().fg(ColorScheme::GRAY_TEXT)),
717                tags_layout[0],
718            );
719
720            // Create tag spans
721            let tag_spans = vec![
722                Span::styled(
723                    " Backend ",
724                    Style::default()
725                        .fg(ColorScheme::CLEAN_BG)
726                        .bg(ColorScheme::GRAY_TEXT),
727                ),
728                Span::raw(" "),
729                Span::styled(
730                    " Refactor ",
731                    Style::default()
732                        .fg(ColorScheme::CLEAN_BG)
733                        .bg(ColorScheme::GRAY_TEXT),
734                ),
735                Span::raw(" "),
736                Span::styled(
737                    " Security ",
738                    Style::default()
739                        .fg(ColorScheme::CLEAN_BG)
740                        .bg(ColorScheme::GRAY_TEXT),
741                ),
742            ];
743
744            f.render_widget(
745                Paragraph::new(Line::from(tag_spans)).alignment(Alignment::Right),
746                tags_layout[1],
747            );
748        } else {
749            // No active session - show idle state
750            let idle_area = self.centered_rect(50, 20, chunks[4]);
751            let idle_block = Block::default()
752                .borders(Borders::ALL)
753                .border_style(Style::default().fg(ColorScheme::GRAY_TEXT))
754                .style(Style::default().bg(ColorScheme::CLEAN_BG));
755
756            f.render_widget(idle_block.clone(), idle_area);
757
758            let idle_inner = idle_block.inner(idle_area);
759            f.render_widget(
760                Paragraph::new("No Active Session\n\nPress 's' to start tracking")
761                    .alignment(Alignment::Center)
762                    .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
763                idle_inner,
764            );
765        }
766    }
767
768    fn render_overview_dashboard(&mut self, f: &mut Frame) {
769        let chunks = Layout::default()
770            .direction(Direction::Vertical)
771            .constraints([
772                Constraint::Length(3), // Header
773                Constraint::Min(10),   // Main Content
774                Constraint::Length(1), // Bottom bar
775            ])
776            .split(f.size());
777
778        // Header
779        self.render_header(f, chunks[0]);
780
781        // Main Content Grid (Left: 8 cols, Right: 4 cols)
782        let grid_chunks = Layout::default()
783            .direction(Direction::Horizontal)
784            .constraints([
785                Constraint::Percentage(66), // Left Column
786                Constraint::Percentage(34), // Right Column
787            ])
788            .split(chunks[1]);
789
790        let left_col = grid_chunks[0];
791        let right_col = grid_chunks[1];
792
793        // Left Column Layout
794        let left_chunks = Layout::default()
795            .direction(Direction::Vertical)
796            .constraints([
797                Constraint::Length(12), // Active Session Panel
798                Constraint::Min(10),    // Project List Table
799            ])
800            .split(left_col);
801
802        let current_session = &self.current_session;
803        let current_project = &self.current_project;
804
805        // 1. Active Session Panel
806        self.render_active_session_panel(f, left_chunks[0], current_session, current_project);
807
808        // 2. Project List Table
809        self.render_projects_table(f, left_chunks[1]);
810
811        // Right Column Layout
812        let right_chunks = Layout::default()
813            .direction(Direction::Vertical)
814            .constraints([
815                Constraint::Length(10), // Quick Stats
816                Constraint::Min(10),    // Activity Timeline
817            ])
818            .split(right_col);
819
820        // 3. Quick Stats
821        let daily_stats = self.get_daily_stats();
822        self.render_quick_stats(f, right_chunks[0], daily_stats);
823
824        // 4. Activity Timeline
825        self.render_activity_timeline(f, right_chunks[1]);
826
827        // 5. Bottom Bar
828        self.render_keyboard_hints(chunks[2], f.buffer_mut());
829    }
830
831    fn render_history_browser(&mut self, f: &mut Frame) {
832        let chunks = Layout::default()
833            .direction(Direction::Horizontal)
834            .constraints([Constraint::Percentage(70), Constraint::Percentage(30)])
835            .split(f.size());
836
837        let left_chunks = Layout::default()
838            .direction(Direction::Vertical)
839            .constraints([
840                Constraint::Length(3), // Header
841                Constraint::Length(8), // Filters
842                Constraint::Min(10),   // Session list
843            ])
844            .split(chunks[0]);
845
846        let right_chunks = Layout::default()
847            .direction(Direction::Vertical)
848            .constraints([
849                Constraint::Percentage(60), // Session details
850                Constraint::Length(4),      // Action buttons
851                Constraint::Min(0),         // Summary
852            ])
853            .split(chunks[1]);
854
855        // Header
856        f.render_widget(
857            Paragraph::new("Tempo TUI :: History Browser")
858                .style(
859                    Style::default()
860                        .fg(ColorScheme::CLEAN_BLUE)
861                        .add_modifier(Modifier::BOLD),
862                )
863                .block(
864                    Block::default()
865                        .borders(Borders::BOTTOM)
866                        .border_style(Style::default().fg(ColorScheme::GRAY_TEXT)),
867                ),
868            left_chunks[0],
869        );
870
871        // Filters panel
872        self.render_history_filters(f, left_chunks[1]);
873
874        // Session list
875        self.render_session_list(f, left_chunks[2]);
876
877        // Session details
878        self.render_session_details(f, right_chunks[0]);
879
880        // Action buttons
881        self.render_session_actions(f, right_chunks[1]);
882
883        // Summary
884        self.render_history_summary(f, right_chunks[2]);
885    }
886
887    fn render_history_filters(&self, f: &mut Frame, area: Rect) {
888        let block = Block::default()
889            .borders(Borders::ALL)
890            .title(" Filters ")
891            .border_style(Style::default().fg(ColorScheme::GRAY_TEXT));
892
893        let inner_area = block.inner(area);
894        f.render_widget(block, area);
895
896        let filter_chunks = Layout::default()
897            .direction(Direction::Vertical)
898            .constraints([
899                Constraint::Length(2), // Date filters
900                Constraint::Length(1), // Project filter
901                Constraint::Length(1), // Duration filter
902                Constraint::Length(1), // Search
903            ])
904            .split(inner_area);
905
906        // Date range
907        let date_layout = Layout::default()
908            .direction(Direction::Horizontal)
909            .constraints([
910                Constraint::Percentage(30),
911                Constraint::Percentage(35),
912                Constraint::Percentage(35),
913            ])
914            .split(filter_chunks[0]);
915
916        f.render_widget(
917            Paragraph::new("Start Date\nEnd Date")
918                .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
919            date_layout[0],
920        );
921        f.render_widget(
922            Paragraph::new("2023-10-01\n2023-10-31")
923                .style(Style::default().fg(ColorScheme::WHITE_TEXT)),
924            date_layout[1],
925        );
926        f.render_widget(
927            Paragraph::new("Project\nDuration Filter")
928                .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
929            date_layout[2],
930        );
931
932        // Project filter
933        let project_layout = Layout::default()
934            .direction(Direction::Horizontal)
935            .constraints([Constraint::Length(15), Constraint::Min(0)])
936            .split(filter_chunks[1]);
937
938        f.render_widget(
939            Paragraph::new("Project").style(Style::default().fg(ColorScheme::GRAY_TEXT)),
940            project_layout[0],
941        );
942        f.render_widget(
943            Paragraph::new("Filter by project ▼")
944                .style(Style::default().fg(ColorScheme::WHITE_TEXT)),
945            project_layout[1],
946        );
947
948        // Duration filter
949        let duration_layout = Layout::default()
950            .direction(Direction::Horizontal)
951            .constraints([Constraint::Length(15), Constraint::Min(0)])
952            .split(filter_chunks[2]);
953
954        f.render_widget(
955            Paragraph::new("Duration Filter").style(Style::default().fg(ColorScheme::GRAY_TEXT)),
956            duration_layout[0],
957        );
958        f.render_widget(
959            Paragraph::new(">1h, <30m").style(Style::default().fg(ColorScheme::WHITE_TEXT)),
960            duration_layout[1],
961        );
962
963        // Free-text search
964        let search_layout = Layout::default()
965            .direction(Direction::Horizontal)
966            .constraints([Constraint::Length(15), Constraint::Min(0)])
967            .split(filter_chunks[3]);
968
969        f.render_widget(
970            Paragraph::new("Free-text Search").style(Style::default().fg(ColorScheme::GRAY_TEXT)),
971            search_layout[0],
972        );
973
974        let search_style = if self.filter_input_mode {
975            Style::default().fg(ColorScheme::CLEAN_BLUE)
976        } else {
977            Style::default().fg(ColorScheme::WHITE_TEXT)
978        };
979
980        let search_text = if self.session_filter.search_text.is_empty() {
981            "Search session notes and context..."
982        } else {
983            &self.session_filter.search_text
984        };
985
986        f.render_widget(
987            Paragraph::new(search_text).style(search_style),
988            search_layout[1],
989        );
990    }
991
992    fn render_session_list(&self, f: &mut Frame, area: Rect) {
993        let block = Block::default()
994            .borders(Borders::ALL)
995            .border_style(Style::default().fg(ColorScheme::GRAY_TEXT));
996
997        let inner_area = block.inner(area);
998        f.render_widget(block, area);
999
1000        // Header row
1001        let header_row = Row::new(vec![
1002            Cell::from("DATE").style(Style::default().add_modifier(Modifier::BOLD)),
1003            Cell::from("PROJECT").style(Style::default().add_modifier(Modifier::BOLD)),
1004            Cell::from("DURATION").style(Style::default().add_modifier(Modifier::BOLD)),
1005            Cell::from("START").style(Style::default().add_modifier(Modifier::BOLD)),
1006            Cell::from("END").style(Style::default().add_modifier(Modifier::BOLD)),
1007            Cell::from("STATUS").style(Style::default().add_modifier(Modifier::BOLD)),
1008        ])
1009        .style(Style::default().fg(ColorScheme::GRAY_TEXT))
1010        .bottom_margin(1);
1011
1012        // Create sample session rows
1013        let rows: Vec<Row> = self
1014            .history_sessions
1015            .iter()
1016            .enumerate()
1017            .map(|(i, session)| {
1018                let is_selected = i == self.selected_session_index;
1019                let style = if is_selected {
1020                    Style::default()
1021                        .bg(ColorScheme::CLEAN_BLUE)
1022                        .fg(Color::Black)
1023                } else {
1024                    Style::default().fg(ColorScheme::WHITE_TEXT)
1025                };
1026
1027                let status = if session.end_time.is_some() {
1028                    "[✓] Completed"
1029                } else {
1030                    "[▶] Running"
1031                };
1032
1033                let start_time = session
1034                    .start_time
1035                    .with_timezone(&Local)
1036                    .format("%H:%M")
1037                    .to_string();
1038                let end_time = if let Some(end) = session.end_time {
1039                    end.with_timezone(&Local).format("%H:%M").to_string()
1040                } else {
1041                    "--:--".to_string()
1042                };
1043
1044                let duration = if let Some(_) = session.end_time {
1045                    let duration_secs =
1046                        (session.start_time.timestamp() - session.start_time.timestamp()).abs();
1047                    Formatter::format_duration(duration_secs)
1048                } else {
1049                    "0h 0m".to_string()
1050                };
1051
1052                Row::new(vec![
1053                    Cell::from(
1054                        session
1055                            .start_time
1056                            .with_timezone(&Local)
1057                            .format("%Y-%m-%d")
1058                            .to_string(),
1059                    ),
1060                    Cell::from("Project Phoenix"), // TODO: Get actual project name
1061                    Cell::from(duration),
1062                    Cell::from(start_time),
1063                    Cell::from(end_time),
1064                    Cell::from(status),
1065                ])
1066                .style(style)
1067            })
1068            .collect();
1069
1070        if rows.is_empty() {
1071            // Show sample data
1072            let sample_rows = vec![
1073                Row::new(vec![
1074                    Cell::from("2023-10-26"),
1075                    Cell::from("Project Phoenix"),
1076                    Cell::from("2h 15m"),
1077                    Cell::from("09:03"),
1078                    Cell::from("11:18"),
1079                    Cell::from("[✓] Completed"),
1080                ])
1081                .style(
1082                    Style::default()
1083                        .bg(ColorScheme::CLEAN_BLUE)
1084                        .fg(Color::Black),
1085                ),
1086                Row::new(vec![
1087                    Cell::from("2023-10-26"),
1088                    Cell::from("Internal Tools"),
1089                    Cell::from("0h 45m"),
1090                    Cell::from("11:30"),
1091                    Cell::from("12:15"),
1092                    Cell::from("[✓] Completed"),
1093                ]),
1094                Row::new(vec![
1095                    Cell::from("2023-10-25"),
1096                    Cell::from("Project Phoenix"),
1097                    Cell::from("4h 05m"),
1098                    Cell::from("13:00"),
1099                    Cell::from("17:05"),
1100                    Cell::from("[✓] Completed"),
1101                ]),
1102                Row::new(vec![
1103                    Cell::from("2023-10-25"),
1104                    Cell::from("Client Support"),
1105                    Cell::from("1h 00m"),
1106                    Cell::from("10:00"),
1107                    Cell::from("11:00"),
1108                    Cell::from("[✓] Completed"),
1109                ]),
1110                Row::new(vec![
1111                    Cell::from("2023-10-24"),
1112                    Cell::from("Project Phoenix"),
1113                    Cell::from("8h 00m"),
1114                    Cell::from("09:00"),
1115                    Cell::from("17:00"),
1116                    Cell::from("[✓] Completed"),
1117                ]),
1118                Row::new(vec![
1119                    Cell::from("2023-10-27"),
1120                    Cell::from("Project Nova"),
1121                    Cell::from("0h 22m"),
1122                    Cell::from("14:00"),
1123                    Cell::from("--:--"),
1124                    Cell::from("[▶] Running"),
1125                ]),
1126            ];
1127
1128            let table = Table::new(sample_rows).header(header_row).widths(&[
1129                Constraint::Length(12),
1130                Constraint::Min(15),
1131                Constraint::Length(10),
1132                Constraint::Length(8),
1133                Constraint::Length(8),
1134                Constraint::Min(12),
1135            ]);
1136
1137            f.render_widget(table, inner_area);
1138        }
1139    }
1140
1141    fn render_session_details(&self, f: &mut Frame, area: Rect) {
1142        let block = Block::default()
1143            .borders(Borders::ALL)
1144            .title(" Session Details ")
1145            .border_style(Style::default().fg(ColorScheme::GRAY_TEXT));
1146
1147        let inner_area = block.inner(area);
1148        f.render_widget(block, area);
1149
1150        let details_chunks = Layout::default()
1151            .direction(Direction::Vertical)
1152            .constraints([
1153                Constraint::Length(3), // Session notes
1154                Constraint::Length(2), // Tags
1155                Constraint::Length(3), // Context
1156            ])
1157            .split(inner_area);
1158
1159        // Session notes
1160        f.render_widget(
1161            Paragraph::new("SESSION NOTES\n\nWorked on the new authentication flow.\nImplemented JWT token refresh logic and fixed\nthe caching issue on the user profile page.\nReady for QA review.")
1162                .style(Style::default().fg(ColorScheme::WHITE_TEXT))
1163                .wrap(ratatui::widgets::Wrap { trim: true }),
1164            details_chunks[0],
1165        );
1166
1167        // Tags
1168        let tag_spans = vec![
1169            Span::styled(
1170                " #backend ",
1171                Style::default()
1172                    .fg(Color::Black)
1173                    .bg(ColorScheme::CLEAN_BLUE),
1174            ),
1175            Span::raw(" "),
1176            Span::styled(
1177                " #auth ",
1178                Style::default()
1179                    .fg(Color::Black)
1180                    .bg(ColorScheme::CLEAN_BLUE),
1181            ),
1182            Span::raw(" "),
1183            Span::styled(
1184                " #bugfix ",
1185                Style::default()
1186                    .fg(Color::Black)
1187                    .bg(ColorScheme::CLEAN_BLUE),
1188            ),
1189        ];
1190
1191        f.render_widget(
1192            Paragraph::new(vec![Line::from("TAGS"), Line::from(tag_spans)])
1193                .style(Style::default().fg(ColorScheme::WHITE_TEXT)),
1194            details_chunks[1],
1195        );
1196
1197        // Context
1198        let context_chunks = Layout::default()
1199            .direction(Direction::Vertical)
1200            .constraints([Constraint::Length(1), Constraint::Min(0)])
1201            .split(details_chunks[2]);
1202
1203        f.render_widget(
1204            Paragraph::new("CONTEXT").style(Style::default().fg(ColorScheme::WHITE_TEXT)),
1205            context_chunks[0],
1206        );
1207
1208        let context_layout = Layout::default()
1209            .direction(Direction::Horizontal)
1210            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
1211            .split(context_chunks[1]);
1212
1213        f.render_widget(
1214            Paragraph::new("Git\nBranch:\nIssue ID:\nCommit:")
1215                .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
1216            context_layout[0],
1217        );
1218        f.render_widget(
1219            Paragraph::new("feature/PHX-123-auth\nPHX-123\na1b2c3d")
1220                .style(Style::default().fg(ColorScheme::WHITE_TEXT)),
1221            context_layout[1],
1222        );
1223    }
1224
1225    fn render_session_actions(&self, f: &mut Frame, area: Rect) {
1226        let button_layout = Layout::default()
1227            .direction(Direction::Horizontal)
1228            .constraints([
1229                Constraint::Percentage(25),
1230                Constraint::Percentage(25),
1231                Constraint::Percentage(25),
1232                Constraint::Percentage(25),
1233            ])
1234            .split(area);
1235
1236        let buttons = [
1237            ("[ Edit ]", ColorScheme::GRAY_TEXT),
1238            ("[ Duplicate ]", ColorScheme::GRAY_TEXT),
1239            ("[ Delete ]", Color::Red),
1240            ("", ColorScheme::GRAY_TEXT),
1241        ];
1242
1243        for (i, (text, color)) in buttons.iter().enumerate() {
1244            if !text.is_empty() {
1245                f.render_widget(
1246                    Paragraph::new(*text)
1247                        .alignment(Alignment::Center)
1248                        .style(Style::default().fg(*color)),
1249                    button_layout[i],
1250                );
1251            }
1252        }
1253    }
1254
1255    fn render_history_summary(&self, f: &mut Frame, area: Rect) {
1256        let block = Block::default()
1257            .borders(Borders::ALL)
1258            .title(" Summary ")
1259            .border_style(Style::default().fg(ColorScheme::GRAY_TEXT));
1260
1261        let inner_area = block.inner(area);
1262        f.render_widget(block, area);
1263
1264        f.render_widget(
1265            Paragraph::new("Showing 7 of 128 sessions. Total Duration: 17h 40m")
1266                .style(Style::default().fg(ColorScheme::WHITE_TEXT))
1267                .alignment(Alignment::Center),
1268            inner_area,
1269        );
1270    }
1271
1272    fn render_project_grid(&mut self, f: &mut Frame) {
1273        let area = f.size();
1274
1275        let main_layout = Layout::default()
1276            .direction(Direction::Vertical)
1277            .constraints([
1278                Constraint::Length(3), // Header
1279                Constraint::Min(10),   // Project grid
1280                Constraint::Length(3), // Stats summary
1281                Constraint::Length(1), // Bottom hints
1282            ])
1283            .split(area);
1284
1285        // Header
1286        f.render_widget(
1287            Paragraph::new("Project Dashboard")
1288                .style(
1289                    Style::default()
1290                        .fg(ColorScheme::CLEAN_BLUE)
1291                        .add_modifier(Modifier::BOLD),
1292                )
1293                .block(
1294                    Block::default()
1295                        .borders(Borders::BOTTOM)
1296                        .border_style(Style::default().fg(ColorScheme::GRAY_TEXT)),
1297                ),
1298            main_layout[0],
1299        );
1300
1301        // Project grid area
1302        self.render_project_cards(f, main_layout[1]);
1303
1304        // Stats summary
1305        self.render_project_stats_summary(f, main_layout[2]);
1306
1307        // Bottom hints
1308        let hints = vec![
1309            ("↑/↓/←/→", "Navigate"),
1310            ("Enter", "Select"),
1311            ("Tab", "Next View"),
1312            ("q", "Quit"),
1313        ];
1314
1315        let spans: Vec<Span> = hints
1316            .iter()
1317            .flat_map(|(key, desc)| {
1318                vec![
1319                    Span::styled(
1320                        format!(" {} ", key),
1321                        Style::default()
1322                            .fg(Color::Yellow)
1323                            .add_modifier(Modifier::BOLD),
1324                    ),
1325                    Span::styled(format!("{} ", desc), Style::default().fg(Color::DarkGray)),
1326                ]
1327            })
1328            .collect();
1329
1330        let line = Line::from(spans);
1331        let block = Block::default()
1332            .borders(Borders::TOP)
1333            .border_style(Style::default().fg(Color::DarkGray));
1334        Paragraph::new(line)
1335            .block(block)
1336            .render(main_layout[3], f.buffer_mut());
1337    }
1338
1339    fn render_project_cards(&mut self, f: &mut Frame, area: Rect) {
1340        if self.available_projects.is_empty() {
1341            // Show empty state
1342            let empty_block = Block::default()
1343                .borders(Borders::ALL)
1344                .border_style(Style::default().fg(ColorScheme::GRAY_TEXT))
1345                .title(" No Projects Found ");
1346
1347            let empty_area = self.centered_rect(50, 30, area);
1348            f.render_widget(empty_block.clone(), empty_area);
1349
1350            let inner = empty_block.inner(empty_area);
1351            f.render_widget(
1352                Paragraph::new("No projects available.\n\nStart a session to create a project.")
1353                    .alignment(Alignment::Center)
1354                    .style(Style::default().fg(ColorScheme::GRAY_TEXT)),
1355                inner,
1356            );
1357            return;
1358        }
1359
1360        // Calculate grid layout
1361        let margin = 2;
1362        let card_height = 8;
1363        let card_spacing = 1;
1364
1365        // Calculate how many rows we can fit
1366        let available_height = area.height.saturating_sub(margin * 2);
1367        let total_rows =
1368            (self.available_projects.len() + self.projects_per_row - 1) / self.projects_per_row;
1369        let visible_rows =
1370            (available_height / (card_height + card_spacing)).min(total_rows as u16) as usize;
1371
1372        // Render visible rows
1373        for row in 0..visible_rows {
1374            let y_offset = margin + row as u16 * (card_height + card_spacing);
1375
1376            // Create horizontal layout for this row
1377            let row_area = Rect::new(area.x, area.y + y_offset, area.width, card_height);
1378            let card_constraints = vec![
1379                Constraint::Percentage(100 / self.projects_per_row as u16);
1380                self.projects_per_row
1381            ];
1382            let row_layout = Layout::default()
1383                .direction(Direction::Horizontal)
1384                .constraints(card_constraints)
1385                .margin(1)
1386                .split(row_area);
1387
1388            // Render cards in this row
1389            for col in 0..self.projects_per_row {
1390                let project_index = row * self.projects_per_row + col;
1391                if project_index >= self.available_projects.len() {
1392                    break;
1393                }
1394
1395                let is_selected =
1396                    row == self.selected_project_row && col == self.selected_project_col;
1397                self.render_project_card(f, row_layout[col], project_index, is_selected);
1398            }
1399        }
1400    }
1401
1402    fn render_project_card(
1403        &self,
1404        f: &mut Frame,
1405        area: Rect,
1406        project_index: usize,
1407        is_selected: bool,
1408    ) {
1409        if let Some(project) = self.available_projects.get(project_index) {
1410            // Card styling based on selection
1411            let border_style = if is_selected {
1412                Style::default().fg(ColorScheme::CLEAN_BLUE)
1413            } else {
1414                Style::default().fg(ColorScheme::GRAY_TEXT)
1415            };
1416
1417            let bg_color = if is_selected {
1418                ColorScheme::CLEAN_BG
1419            } else {
1420                Color::Black
1421            };
1422
1423            let card_block = Block::default()
1424                .borders(Borders::ALL)
1425                .border_style(border_style)
1426                .style(Style::default().bg(bg_color));
1427
1428            f.render_widget(card_block.clone(), area);
1429
1430            let inner_area = card_block.inner(area);
1431            let card_layout = Layout::default()
1432                .direction(Direction::Vertical)
1433                .constraints([
1434                    Constraint::Length(1), // Project name
1435                    Constraint::Length(1), // Path
1436                    Constraint::Length(1), // Spacer
1437                    Constraint::Length(2), // Stats
1438                    Constraint::Length(1), // Status
1439                ])
1440                .split(inner_area);
1441
1442            // Project name
1443            let name = if project.name.len() > 20 {
1444                format!("{}...", &project.name[..17])
1445            } else {
1446                project.name.clone()
1447            };
1448
1449            f.render_widget(
1450                Paragraph::new(name)
1451                    .style(
1452                        Style::default()
1453                            .fg(ColorScheme::WHITE_TEXT)
1454                            .add_modifier(Modifier::BOLD),
1455                    )
1456                    .alignment(Alignment::Center),
1457                card_layout[0],
1458            );
1459
1460            // Path (shortened)
1461            let path_str = project.path.to_string_lossy();
1462            let short_path = if path_str.len() > 25 {
1463                format!("...{}", &path_str[path_str.len() - 22..])
1464            } else {
1465                path_str.to_string()
1466            };
1467
1468            f.render_widget(
1469                Paragraph::new(short_path)
1470                    .style(Style::default().fg(ColorScheme::GRAY_TEXT))
1471                    .alignment(Alignment::Center),
1472                card_layout[1],
1473            );
1474
1475            // Stats placeholder - in real implementation, we'd fetch these
1476            let stats_layout = Layout::default()
1477                .direction(Direction::Horizontal)
1478                .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
1479                .split(card_layout[3]);
1480
1481            f.render_widget(
1482                Paragraph::new("Sessions\n42")
1483                    .style(Style::default().fg(ColorScheme::WHITE_TEXT))
1484                    .alignment(Alignment::Center),
1485                stats_layout[0],
1486            );
1487
1488            f.render_widget(
1489                Paragraph::new("Time\n24h 15m")
1490                    .style(Style::default().fg(ColorScheme::WHITE_TEXT))
1491                    .alignment(Alignment::Center),
1492                stats_layout[1],
1493            );
1494
1495            // Status
1496            let status = if project.is_archived {
1497                (" Archived ", Color::Red)
1498            } else {
1499                (" Active ", ColorScheme::CLEAN_GREEN)
1500            };
1501
1502            f.render_widget(
1503                Paragraph::new(status.0)
1504                    .style(Style::default().fg(status.1))
1505                    .alignment(Alignment::Center),
1506                card_layout[4],
1507            );
1508        }
1509    }
1510
1511    fn render_project_stats_summary(&self, f: &mut Frame, area: Rect) {
1512        let block = Block::default()
1513            .borders(Borders::ALL)
1514            .title(" Summary ")
1515            .border_style(Style::default().fg(ColorScheme::GRAY_TEXT));
1516
1517        f.render_widget(block.clone(), area);
1518
1519        let inner = block.inner(area);
1520        let stats_layout = Layout::default()
1521            .direction(Direction::Horizontal)
1522            .constraints([
1523                Constraint::Percentage(25),
1524                Constraint::Percentage(25),
1525                Constraint::Percentage(25),
1526                Constraint::Percentage(25),
1527            ])
1528            .split(inner);
1529
1530        let total_projects = self.available_projects.len();
1531        let active_projects = self
1532            .available_projects
1533            .iter()
1534            .filter(|p| !p.is_archived)
1535            .count();
1536        let archived_projects = total_projects - active_projects;
1537
1538        let stats = [
1539            ("Total Projects", total_projects.to_string()),
1540            ("Active", active_projects.to_string()),
1541            ("Archived", archived_projects.to_string()),
1542            (
1543                "Selected",
1544                format!(
1545                    "{}/{}",
1546                    self.selected_project_row * self.projects_per_row
1547                        + self.selected_project_col
1548                        + 1,
1549                    total_projects
1550                ),
1551            ),
1552        ];
1553
1554        for (i, (label, value)) in stats.iter().enumerate() {
1555            let content = Paragraph::new(vec![
1556                Line::from(Span::styled(
1557                    *label,
1558                    Style::default().fg(ColorScheme::GRAY_TEXT),
1559                )),
1560                Line::from(Span::styled(
1561                    value.as_str(),
1562                    Style::default()
1563                        .fg(ColorScheme::WHITE_TEXT)
1564                        .add_modifier(Modifier::BOLD),
1565                )),
1566            ])
1567            .alignment(Alignment::Center);
1568
1569            f.render_widget(content, stats_layout[i]);
1570        }
1571    }
1572
1573    fn render_active_session_panel(
1574        &self,
1575        f: &mut Frame,
1576        area: Rect,
1577        session: &Option<Session>,
1578        project: &Option<Project>,
1579    ) {
1580        // Use pulsing border color when session is active
1581        use crate::ui::animations::pulse_color;
1582        let border_color = if session.is_some() {
1583            pulse_color(
1584                ColorScheme::PRIMARY_DASHBOARD,
1585                ColorScheme::PRIMARY_FOCUS,
1586                self.pulsing_indicator.start_time.elapsed(),
1587                Duration::from_millis(2000),
1588            )
1589        } else {
1590            ColorScheme::BORDER_DARK
1591        };
1592
1593        let block = Block::default()
1594            .borders(Borders::ALL)
1595            .border_style(Style::default().fg(border_color))
1596            .style(Style::default().bg(ColorScheme::BG_DARK));
1597
1598        f.render_widget(block.clone(), area);
1599
1600        let inner_area = block.inner(area);
1601        let layout = Layout::default()
1602            .direction(Direction::Vertical)
1603            .constraints([
1604                Constraint::Length(1), // "Active Session" header
1605                Constraint::Length(1), // Spacer
1606                Constraint::Min(4),    // Content (Project + Status + Timer)
1607            ])
1608            .margin(1)
1609            .split(inner_area);
1610
1611        // Header
1612        f.render_widget(
1613            Paragraph::new("Active Session")
1614                .style(
1615                    Style::default()
1616                        .fg(ColorScheme::TEXT_MAIN)
1617                        .add_modifier(Modifier::BOLD),
1618                )
1619                .block(
1620                    Block::default()
1621                        .borders(Borders::BOTTOM)
1622                        .border_style(Style::default().fg(ColorScheme::BORDER_DARK)),
1623                ),
1624            layout[0],
1625        );
1626
1627        if let Some(session) = session {
1628            let project_name = project
1629                .as_ref()
1630                .map(|p| p.name.as_str())
1631                .unwrap_or("Unknown Project");
1632
1633            let content_layout = Layout::default()
1634                .direction(Direction::Vertical)
1635                .constraints([
1636                    Constraint::Length(3), // Project Name + Status
1637                    Constraint::Min(3),    // Timer
1638                ])
1639                .split(layout[2]);
1640
1641            // Project Name + Status
1642            f.render_widget(
1643                Paragraph::new(vec![
1644                    Line::from(Span::styled(
1645                        project_name,
1646                        Style::default()
1647                            .fg(ColorScheme::TEXT_MAIN)
1648                            .add_modifier(Modifier::BOLD)
1649                            .add_modifier(Modifier::UNDERLINED), // Make it look like a title
1650                    )),
1651                    Line::from(Span::styled(
1652                        "▶ RUNNING",
1653                        Style::default().fg(ColorScheme::SUCCESS),
1654                    )),
1655                ])
1656                .alignment(Alignment::Center),
1657                content_layout[0],
1658            );
1659
1660            // Timer
1661            let now = Local::now();
1662            let elapsed_seconds = (now.timestamp() - session.start_time.timestamp())
1663                - session.paused_duration.num_seconds();
1664
1665            let hours = elapsed_seconds / 3600;
1666            let minutes = (elapsed_seconds % 3600) / 60;
1667            let seconds = elapsed_seconds % 60;
1668
1669            // Render large timer digits (simplified for TUI)
1670            // Ideally we'd use tui-big-text or similar, but standard text is fine for now
1671            // We'll mimic the design with boxes
1672
1673            let timer_layout = Layout::default()
1674                .direction(Direction::Horizontal)
1675                .constraints([
1676                    Constraint::Ratio(1, 3),
1677                    Constraint::Ratio(1, 3),
1678                    Constraint::Ratio(1, 3),
1679                ])
1680                .split(content_layout[1]);
1681
1682            self.render_timer_digit(f, timer_layout[0], hours, "Hours");
1683            self.render_timer_digit(f, timer_layout[1], minutes, "Minutes");
1684            self.render_timer_digit(f, timer_layout[2], seconds, "Seconds");
1685        } else {
1686            // Idle State
1687            f.render_widget(
1688                Paragraph::new("No Active Session\n\nPress 's' to start tracking")
1689                    .alignment(Alignment::Center)
1690                    .style(Style::default().fg(ColorScheme::TEXT_SECONDARY)),
1691                layout[2],
1692            );
1693        }
1694    }
1695
1696    fn render_timer_digit(&self, f: &mut Frame, area: Rect, value: i64, label: &str) {
1697        let block = Block::default()
1698            .borders(Borders::ALL)
1699            .border_style(Style::default().fg(ColorScheme::BORDER_DARK))
1700            .style(Style::default().bg(ColorScheme::PANEL_DARK));
1701
1702        let inner = block.inner(area);
1703        f.render_widget(block, area);
1704
1705        let layout = Layout::default()
1706            .direction(Direction::Vertical)
1707            .constraints([
1708                Constraint::Min(1),    // Digit
1709                Constraint::Length(1), // Label
1710            ])
1711            .split(inner);
1712
1713        f.render_widget(
1714            Paragraph::new(format!("{:02}", value))
1715                .alignment(Alignment::Center)
1716                .style(
1717                    Style::default()
1718                        .fg(ColorScheme::TEXT_MAIN)
1719                        .add_modifier(Modifier::BOLD),
1720                ),
1721            layout[0],
1722        );
1723
1724        f.render_widget(
1725            Paragraph::new(label)
1726                .alignment(Alignment::Center)
1727                .style(Style::default().fg(ColorScheme::TEXT_SECONDARY)),
1728            layout[1],
1729        );
1730    }
1731
1732    fn render_quick_stats(&self, f: &mut Frame, area: Rect, daily_stats: &(i64, i64, i64)) {
1733        let (_sessions_count, total_seconds, _avg_seconds) = *daily_stats;
1734
1735        let block = Block::default()
1736            .borders(Borders::ALL)
1737            .border_style(Style::default().fg(ColorScheme::BORDER_DARK));
1738
1739        f.render_widget(block.clone(), area);
1740        let inner_area = block.inner(area);
1741
1742        let layout = Layout::default()
1743            .direction(Direction::Vertical)
1744            .constraints([
1745                Constraint::Length(2), // Header
1746                Constraint::Min(1),    // Stats
1747            ])
1748            .split(inner_area);
1749
1750        // Header
1751        f.render_widget(
1752            Paragraph::new("Quick Stats")
1753                .style(
1754                    Style::default()
1755                        .fg(ColorScheme::TEXT_MAIN)
1756                        .add_modifier(Modifier::BOLD),
1757                )
1758                .block(
1759                    Block::default()
1760                        .borders(Borders::BOTTOM)
1761                        .border_style(Style::default().fg(ColorScheme::BORDER_DARK)),
1762                ),
1763            layout[0],
1764        );
1765
1766        // Stats Grid
1767        let stats_layout = Layout::default()
1768            .direction(Direction::Vertical)
1769            .constraints([
1770                Constraint::Length(3), // Today
1771                Constraint::Length(3), // Week
1772                Constraint::Length(3), // Active Projects
1773            ])
1774            .margin(1)
1775            .split(layout[1]);
1776
1777        let stats = [
1778            (
1779                "Today's Total",
1780                Formatter::format_duration(total_seconds),
1781                stats_layout[0],
1782            ),
1783            (
1784                "This Week's Total",
1785                Formatter::format_duration(self.weekly_stats),
1786                stats_layout[1],
1787            ),
1788            (
1789                "Active Projects",
1790                self.available_projects
1791                    .iter()
1792                    .filter(|p| !p.is_archived)
1793                    .count()
1794                    .to_string(),
1795                stats_layout[2],
1796            ),
1797        ];
1798
1799        for (label, value, chunk) in stats.iter() {
1800            let item_block = Block::default()
1801                .borders(Borders::ALL)
1802                .border_style(Style::default().fg(ColorScheme::BORDER_DARK))
1803                .style(Style::default().bg(ColorScheme::PANEL_DARK));
1804
1805            f.render_widget(item_block.clone(), *chunk);
1806            let item_inner = item_block.inner(*chunk);
1807
1808            let item_layout = Layout::default()
1809                .direction(Direction::Vertical)
1810                .constraints([
1811                    Constraint::Length(1), // Label
1812                    Constraint::Length(1), // Value
1813                ])
1814                .split(item_inner);
1815
1816            f.render_widget(
1817                Paragraph::new(*label).style(Style::default().fg(ColorScheme::TEXT_SECONDARY)),
1818                item_layout[0],
1819            );
1820
1821            f.render_widget(
1822                Paragraph::new(value.as_str()).style(
1823                    Style::default()
1824                        .fg(ColorScheme::TEXT_MAIN)
1825                        .add_modifier(Modifier::BOLD),
1826                ),
1827                item_layout[1],
1828            );
1829        }
1830    }
1831
1832    fn render_projects_table(&self, f: &mut Frame, area: Rect) {
1833        let block = Block::default()
1834            .borders(Borders::ALL)
1835            .border_style(Style::default().fg(ColorScheme::BORDER_DARK));
1836
1837        f.render_widget(block.clone(), area);
1838        let inner_area = block.inner(area);
1839
1840        let layout = Layout::default()
1841            .direction(Direction::Vertical)
1842            .constraints([
1843                Constraint::Length(2), // Header
1844                Constraint::Min(1),    // Table
1845            ])
1846            .split(inner_area);
1847
1848        // Header
1849        f.render_widget(
1850            Paragraph::new("Project List")
1851                .style(
1852                    Style::default()
1853                        .fg(ColorScheme::TEXT_MAIN)
1854                        .add_modifier(Modifier::BOLD),
1855                )
1856                .block(
1857                    Block::default()
1858                        .borders(Borders::BOTTOM)
1859                        .border_style(Style::default().fg(ColorScheme::BORDER_DARK)),
1860                ),
1861            layout[0],
1862        );
1863
1864        // Table
1865        let header_row = Row::new(vec![
1866            Cell::from("PROJECT NAME").style(Style::default().fg(ColorScheme::TEXT_SECONDARY)),
1867            Cell::from("TIME TODAY").style(Style::default().fg(ColorScheme::TEXT_SECONDARY)),
1868            Cell::from("TOTAL TIME").style(Style::default().fg(ColorScheme::TEXT_SECONDARY)),
1869            Cell::from("LAST ACTIVITY").style(Style::default().fg(ColorScheme::TEXT_SECONDARY)),
1870        ])
1871        .bottom_margin(1);
1872
1873        let rows: Vec<Row> = self
1874            .recent_projects
1875            .iter()
1876            .map(|p| {
1877                let time_today = Formatter::format_duration(p.today_seconds);
1878                let total_time = Formatter::format_duration(p.total_seconds);
1879                let last_activity = if let Some(last) = p.last_active {
1880                    let now = chrono::Utc::now();
1881                    let diff = now - last;
1882                    if diff.num_days() == 0 {
1883                        format!("Today, {}", last.with_timezone(&Local).format("%H:%M"))
1884                    } else if diff.num_days() == 1 {
1885                        "Yesterday".to_string()
1886                    } else {
1887                        format!("{} days ago", diff.num_days())
1888                    }
1889                } else {
1890                    "Never".to_string()
1891                };
1892
1893                Row::new(vec![
1894                    Cell::from(p.project.name.clone()).style(
1895                        Style::default()
1896                            .fg(ColorScheme::TEXT_MAIN)
1897                            .add_modifier(Modifier::BOLD),
1898                    ),
1899                    Cell::from(time_today).style(Style::default().fg(if p.today_seconds > 0 {
1900                        ColorScheme::SUCCESS
1901                    } else {
1902                        ColorScheme::TEXT_SECONDARY
1903                    })),
1904                    Cell::from(total_time).style(Style::default().fg(ColorScheme::TEXT_SECONDARY)),
1905                    Cell::from(last_activity)
1906                        .style(Style::default().fg(ColorScheme::TEXT_SECONDARY)),
1907                ])
1908            })
1909            .collect();
1910
1911        let table = Table::new(rows)
1912            .header(header_row)
1913            .widths(&[
1914                Constraint::Percentage(30),
1915                Constraint::Percentage(20),
1916                Constraint::Percentage(20),
1917                Constraint::Percentage(30),
1918            ])
1919            .column_spacing(1);
1920
1921        f.render_widget(table, layout[1]);
1922    }
1923
1924    fn render_activity_timeline(&self, f: &mut Frame, area: Rect) {
1925        let block = Block::default()
1926            .borders(Borders::ALL)
1927            .border_style(Style::default().fg(ColorScheme::BORDER_DARK));
1928
1929        f.render_widget(block.clone(), area);
1930        let inner_area = block.inner(area);
1931
1932        let layout = Layout::default()
1933            .direction(Direction::Vertical)
1934            .constraints([
1935                Constraint::Length(2), // Header
1936                Constraint::Min(1),    // Timeline
1937            ])
1938            .split(inner_area);
1939
1940        // Header
1941        f.render_widget(
1942            Paragraph::new("Activity Timeline")
1943                .style(
1944                    Style::default()
1945                        .fg(ColorScheme::TEXT_MAIN)
1946                        .add_modifier(Modifier::BOLD),
1947                )
1948                .block(
1949                    Block::default()
1950                        .borders(Borders::BOTTOM)
1951                        .border_style(Style::default().fg(ColorScheme::BORDER_DARK)),
1952                ),
1953            layout[0],
1954        );
1955
1956        let timeline_area = layout[1];
1957        let bar_area = Rect::new(
1958            timeline_area.x,
1959            timeline_area.y + 1,
1960            timeline_area.width,
1961            2, // Height of the bar
1962        );
1963
1964        // Draw background bar
1965        f.render_widget(
1966            Block::default().style(Style::default().bg(ColorScheme::PANEL_DARK)),
1967            bar_area,
1968        );
1969
1970        // Draw session segments
1971        let total_width = bar_area.width as f64;
1972        let seconds_in_day = 86400.0;
1973
1974        for session in &self.today_sessions {
1975            let start_seconds = session
1976                .start_time
1977                .with_timezone(&Local)
1978                .num_seconds_from_midnight() as f64;
1979            let end_seconds = if let Some(end) = session.end_time {
1980                end.with_timezone(&Local).num_seconds_from_midnight() as f64
1981            } else {
1982                Local::now().num_seconds_from_midnight() as f64
1983            };
1984
1985            let start_x = (start_seconds / seconds_in_day * total_width).floor() as u16;
1986            let width =
1987                ((end_seconds - start_seconds) / seconds_in_day * total_width).ceil() as u16;
1988
1989            let draw_width = width.min(bar_area.width.saturating_sub(start_x));
1990
1991            if draw_width > 0 {
1992                let segment_area = Rect::new(
1993                    bar_area.x + start_x,
1994                    bar_area.y,
1995                    draw_width,
1996                    bar_area.height,
1997                );
1998
1999                let color = if session.end_time.is_none() {
2000                    ColorScheme::SUCCESS
2001                } else {
2002                    ColorScheme::SUCCESS // Just use success color for now
2003                };
2004
2005                f.render_widget(
2006                    Block::default().style(Style::default().bg(color)),
2007                    segment_area,
2008                );
2009            }
2010        }
2011
2012        // Time Labels
2013        let labels_y = bar_area.y + bar_area.height + 1;
2014        let labels = ["00:00", "06:00", "12:00", "18:00", "24:00"];
2015        let positions = [0.0, 0.25, 0.5, 0.75, 1.0];
2016
2017        for (label, pos) in labels.iter().zip(positions.iter()) {
2018            let x = (timeline_area.x as f64 + (timeline_area.width as f64 * pos)
2019                - (label.len() as f64 / 2.0)) as u16;
2020            let x = x
2021                .max(timeline_area.x)
2022                .min(timeline_area.x + timeline_area.width - label.len() as u16);
2023
2024            f.render_widget(
2025                Paragraph::new(*label).style(Style::default().fg(ColorScheme::TEXT_SECONDARY)),
2026                Rect::new(x, labels_y, label.len() as u16, 1),
2027            );
2028        }
2029    }
2030
2031    fn render_project_switcher(&self, f: &mut Frame, area: Rect) {
2032        let popup_area = self.centered_rect(60, 50, area);
2033
2034        let block = Block::default()
2035            .borders(Borders::ALL)
2036            .border_style(Style::default().fg(ColorScheme::CLEAN_BLUE))
2037            .title(" Select Project ")
2038            .title_alignment(Alignment::Center)
2039            .style(Style::default().bg(ColorScheme::CLEAN_BG));
2040
2041        f.render_widget(block.clone(), popup_area);
2042
2043        let list_area = block.inner(popup_area);
2044
2045        if self.available_projects.is_empty() {
2046            let no_projects = Paragraph::new("No projects found")
2047                .alignment(Alignment::Center)
2048                .style(Style::default().fg(ColorScheme::GRAY_TEXT));
2049            f.render_widget(no_projects, list_area);
2050        } else {
2051            let items: Vec<ListItem> = self
2052                .available_projects
2053                .iter()
2054                .enumerate()
2055                .map(|(i, p)| {
2056                    let style = if i == self.selected_project_index {
2057                        Style::default()
2058                            .fg(ColorScheme::CLEAN_BG)
2059                            .bg(ColorScheme::CLEAN_BLUE)
2060                    } else {
2061                        Style::default().fg(ColorScheme::WHITE_TEXT)
2062                    };
2063                    ListItem::new(format!(" {} ", p.name)).style(style)
2064                })
2065                .collect();
2066
2067            let list = List::new(items);
2068            f.render_widget(list, list_area);
2069        }
2070    }
2071
2072    fn centered_rect(&self, percent_x: u16, percent_y: u16, r: Rect) -> Rect {
2073        let popup_layout = Layout::default()
2074            .direction(Direction::Vertical)
2075            .constraints([
2076                Constraint::Percentage((100 - percent_y) / 2),
2077                Constraint::Percentage(percent_y),
2078                Constraint::Percentage((100 - percent_y) / 2),
2079            ])
2080            .split(r);
2081
2082        Layout::default()
2083            .direction(Direction::Horizontal)
2084            .constraints([
2085                Constraint::Percentage((100 - percent_x) / 2),
2086                Constraint::Percentage(percent_x),
2087                Constraint::Percentage((100 - percent_x) / 2),
2088            ])
2089            .split(popup_layout[1])[1]
2090    }
2091
2092    async fn get_current_session(&mut self) -> Result<Option<Session>> {
2093        if !is_daemon_running() {
2094            return Ok(None);
2095        }
2096
2097        self.ensure_connected().await?;
2098
2099        let response = self
2100            .client
2101            .send_message(&IpcMessage::GetActiveSession)
2102            .await?;
2103        match response {
2104            IpcResponse::ActiveSession(session) => Ok(session),
2105            IpcResponse::Error(e) => Err(anyhow::anyhow!("Failed to get active session: {}", e)),
2106            _ => Ok(None),
2107        }
2108    }
2109
2110    async fn get_project_by_session(&mut self, session: &Session) -> Result<Option<Project>> {
2111        if !is_daemon_running() {
2112            return Ok(None);
2113        }
2114
2115        self.ensure_connected().await?;
2116
2117        let response = self
2118            .client
2119            .send_message(&IpcMessage::GetProject(session.project_id))
2120            .await?;
2121        match response {
2122            IpcResponse::Project(project) => Ok(project),
2123            IpcResponse::Error(e) => Err(anyhow::anyhow!("Failed to get project: {}", e)),
2124            _ => Ok(None),
2125        }
2126    }
2127
2128    async fn get_today_stats(&mut self) -> Result<(i64, i64, i64)> {
2129        // (sessions_count, total_seconds, avg_seconds)
2130        if !is_daemon_running() {
2131            return Ok((0, 0, 0));
2132        }
2133
2134        self.ensure_connected().await?;
2135
2136        let today = chrono::Local::now().date_naive();
2137        let response = self
2138            .client
2139            .send_message(&IpcMessage::GetDailyStats(today))
2140            .await?;
2141        match response {
2142            IpcResponse::DailyStats {
2143                sessions_count,
2144                total_seconds,
2145                avg_seconds,
2146            } => Ok((sessions_count, total_seconds, avg_seconds)),
2147            IpcResponse::Error(e) => Err(anyhow::anyhow!("Failed to get daily stats: {}", e)),
2148            _ => Ok((0, 0, 0)),
2149        }
2150    }
2151
2152    async fn get_today_sessions(&mut self) -> Result<Vec<Session>> {
2153        if !is_daemon_running() {
2154            return Ok(Vec::new());
2155        }
2156
2157        self.ensure_connected().await?;
2158
2159        let today = chrono::Local::now().date_naive();
2160        let response = self
2161            .client
2162            .send_message(&IpcMessage::GetSessionsForDate(today))
2163            .await?;
2164        match response {
2165            IpcResponse::SessionList(sessions) => Ok(sessions),
2166            IpcResponse::Error(e) => Err(anyhow::anyhow!("Failed to get sessions: {}", e)),
2167            _ => Ok(Vec::new()),
2168        }
2169    }
2170
2171    async fn get_history_sessions(&mut self) -> Result<Vec<Session>> {
2172        if !is_daemon_running() {
2173            return Ok(Vec::new());
2174        }
2175
2176        self.ensure_connected().await?;
2177
2178        // For now, get a date range of the last 30 days
2179        let end_date = chrono::Local::now().date_naive();
2180        let _start_date = end_date - chrono::Duration::days(30);
2181
2182        // Get sessions for the date range (simplified - in a real implementation,
2183        // this would use a new IPC message like GetSessionsInRange)
2184        let mut all_sessions = Vec::new();
2185        for days_ago in 0..30 {
2186            let date = end_date - chrono::Duration::days(days_ago);
2187            if let Ok(IpcResponse::SessionList(sessions)) = self
2188                .client
2189                .send_message(&IpcMessage::GetSessionsForDate(date))
2190                .await
2191            {
2192                all_sessions.extend(sessions);
2193            }
2194        }
2195
2196        // Apply filters
2197        let filtered_sessions: Vec<Session> = all_sessions
2198            .into_iter()
2199            .filter(|session| {
2200                // Apply search filter if set
2201                if !self.session_filter.search_text.is_empty() {
2202                    if let Some(notes) = &session.notes {
2203                        if !notes
2204                            .to_lowercase()
2205                            .contains(&self.session_filter.search_text.to_lowercase())
2206                        {
2207                            return false;
2208                        }
2209                    } else {
2210                        return false;
2211                    }
2212                }
2213                true
2214            })
2215            .collect();
2216
2217        Ok(filtered_sessions)
2218    }
2219
2220    async fn send_activity_heartbeat(&mut self) -> Result<()> {
2221        if !is_daemon_running() {
2222            return Ok(());
2223        }
2224
2225        self.ensure_connected().await?;
2226
2227        let _response = self
2228            .client
2229            .send_message(&IpcMessage::ActivityHeartbeat)
2230            .await?;
2231        Ok(())
2232    }
2233
2234    // Helper methods for project switcher navigation
2235
2236    fn navigate_projects(&mut self, direction: i32) {
2237        if self.available_projects.is_empty() {
2238            return;
2239        }
2240
2241        let new_index = self.selected_project_index as i32 + direction;
2242        if new_index >= 0 && new_index < self.available_projects.len() as i32 {
2243            self.selected_project_index = new_index as usize;
2244        }
2245    }
2246
2247    async fn refresh_projects(&mut self) -> Result<()> {
2248        if !is_daemon_running() {
2249            return Ok(());
2250        }
2251
2252        self.ensure_connected().await?;
2253
2254        let response = self.client.send_message(&IpcMessage::ListProjects).await?;
2255        if let IpcResponse::ProjectList(projects) = response {
2256            self.available_projects = projects;
2257            self.selected_project_index = 0;
2258        }
2259        Ok(())
2260    }
2261
2262    async fn switch_to_selected_project(&mut self) -> Result<()> {
2263        if let Some(selected_project) = self.available_projects.get(self.selected_project_index) {
2264            let project_id = selected_project.id.unwrap_or(0);
2265
2266            self.ensure_connected().await?;
2267
2268            // Switch to the selected project
2269            let response = self
2270                .client
2271                .send_message(&IpcMessage::SwitchProject(project_id))
2272                .await?;
2273            match response {
2274                IpcResponse::Success => {
2275                    self.show_project_switcher = false;
2276                }
2277                IpcResponse::Error(e) => {
2278                    return Err(anyhow::anyhow!("Failed to switch project: {}", e))
2279                }
2280                _ => return Err(anyhow::anyhow!("Unexpected response")),
2281            }
2282        }
2283        Ok(())
2284    }
2285
2286    fn render_header(&self, f: &mut Frame, area: Rect) {
2287        let time_str = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
2288
2289        let header_layout = Layout::default()
2290            .direction(Direction::Horizontal)
2291            .constraints([
2292                Constraint::Min(20), // Title
2293                Constraint::Min(30), // Date/Time + Daemon Status
2294            ])
2295            .split(area);
2296
2297        // Title
2298        let title_block = Block::default()
2299            .borders(Borders::BOTTOM)
2300            .border_style(Style::default().fg(ColorScheme::BORDER_DARK));
2301
2302        let title_inner = title_block.inner(header_layout[0]);
2303        f.render_widget(title_block, header_layout[0]);
2304
2305        f.render_widget(
2306            Paragraph::new("Tempo TUI").style(
2307                Style::default()
2308                    .fg(ColorScheme::TEXT_MAIN)
2309                    .add_modifier(Modifier::BOLD),
2310            ),
2311            title_inner,
2312        );
2313
2314        // Right side: Date/Time + Daemon Status
2315        let status_block = Block::default()
2316            .borders(Borders::BOTTOM)
2317            .border_style(Style::default().fg(ColorScheme::BORDER_DARK));
2318
2319        let status_inner = status_block.inner(header_layout[1]);
2320        f.render_widget(status_block, header_layout[1]);
2321
2322        let status_layout = Layout::default()
2323            .direction(Direction::Horizontal)
2324            .constraints([
2325                Constraint::Min(20),    // Date/Time
2326                Constraint::Length(15), // Daemon Status
2327            ])
2328            .split(status_inner);
2329
2330        f.render_widget(
2331            Paragraph::new(time_str)
2332                .alignment(Alignment::Right)
2333                .style(Style::default().fg(ColorScheme::TEXT_MAIN)),
2334            status_layout[0],
2335        );
2336
2337        // Daemon status with animated spinner when running
2338        let daemon_status_line = if is_daemon_running() {
2339            let spinner_frame = self.animated_spinner.current();
2340            Line::from(vec![
2341                Span::raw("Daemon: "),
2342                Span::styled(
2343                    spinner_frame,
2344                    Style::default().fg(ColorScheme::SUCCESS),
2345                ),
2346                Span::raw(" "),
2347                Span::styled(
2348                    "Running",
2349                    Style::default()
2350                        .fg(ColorScheme::SUCCESS)
2351                        .add_modifier(Modifier::BOLD),
2352                ),
2353            ])
2354        } else {
2355            Line::from(vec![
2356                Span::raw("Daemon: "),
2357                Span::styled(
2358                    "Offline",
2359                    Style::default()
2360                        .fg(ColorScheme::ERROR)
2361                        .add_modifier(Modifier::BOLD),
2362                ),
2363            ])
2364        };
2365
2366        f.render_widget(
2367            Paragraph::new(daemon_status_line)
2368                .alignment(Alignment::Right),
2369            status_layout[1],
2370        );
2371    }
2372
2373    fn get_daily_stats(&self) -> &(i64, i64, i64) {
2374        &self.daily_stats
2375    }
2376}