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