Skip to content

Conversation

@dhth
Copy link
Owner

@dhth dhth commented Aug 17, 2025

Summary by CodeRabbit

  • New Features

    • App now uses a single, consistent time source so relative timestamps and “last updated” text are uniform across task lists, logs, stats, and reports.
    • Interactive views render “now” consistently when starting, stopping, or editing tracked time.
  • Bug Fixes

    • Eliminates mismatched or drifting time displays between different views and list items.

@coderabbitai
Copy link

coderabbitai bot commented Aug 17, 2025

Walkthrough

Introduces a TimeProvider interface and concrete providers (RealTimeProvider, TestTimeProvider); replaces direct time.Now() usage with provider-based calls; updates Task and TaskLogEntry UpdateListDesc signatures to accept TimeProvider; wires the provider through UI initialization and interactive/report/stats flows.

Changes

Cohort / File(s) Summary
Time provider types & task rendering
internal/types/types.go
Adds TimeProvider interface (Now()), RealTimeProvider, TestTimeProvider; updates Task.UpdateListDesc and TaskLogEntry.UpdateListDesc to accept a TimeProvider and use humanize.RelTime(..., provider.Now(), ...).
Model structs & initialization
internal/ui/model.go, internal/ui/initial.go
Adds timeProvider types.TimeProvider field to Model and recordsModel; updates InitialModel signature to accept a types.TimeProvider parameter and assigns it to the model.
UI entrypoints wiring
internal/ui/ui.go, cmd/root.go
RenderUI signature now accepts timeProvider types.TimeProvider; callers (CLI/run) pass types.RealTimeProvider{} into RenderUI and InitialModel.
Interactive reports/stats/logs
internal/ui/log.go, internal/ui/report.go, internal/ui/stats.go
initialRecordsModel calls in interactive paths updated to include types.RealTimeProvider{} argument (provider inserted after style).
Handlers and update flows
internal/ui/handle.go, internal/ui/update.go
Replaces time.Now() with m.timeProvider.Now() across handlers and update logic; calls UpdateListDesc(m.timeProvider) for Task and TaskLogEntry; uses provider time for date calculations and truncation.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as cmd.Root
  participant UI as RenderUI
  participant Init as InitialModel
  participant M as Model
  participant TP as RealTimeProvider

  CLI->>UI: RenderUI(db, style, RealTimeProvider{})
  UI->>Init: InitialModel(db, style, RealTimeProvider{}, debug, cfg)
  Init->>M: construct Model{timeProvider: RealTimeProvider{}}
  M-->>UI: ready
Loading
sequenceDiagram
  participant M as Model
  participant TP as TimeProvider
  participant T as Task
  participant TL as TaskLogEntry

  Note over M: During render/update
  M->>TP: Now()
  TP-->>M: time.Time
  M->>T: UpdateListDesc(TimeProvider)
  M->>TL: UpdateListDesc(TimeProvider)
  T-->>M: updated desc
  TL-->>M: updated desc
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I hop with clocks both real and test,
No brittle Now to break my quest.
Providers forward, fixed or true,
List lines update, time made new.
A rabbit cheers: precise review 🐇

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 2b1e7fb and e637086.

📒 Files selected for processing (4)
  • cmd/root.go (1 hunks)
  • internal/types/types.go (4 hunks)
  • internal/ui/ui.go (2 hunks)
  • internal/ui/update.go (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
cmd/root.go (2)
internal/ui/ui.go (1)
  • RenderUI (19-55)
internal/types/types.go (2)
  • RealTimeProvider (67-67)
  • RealTimeProvider (69-71)
internal/ui/ui.go (2)
internal/types/types.go (1)
  • TimeProvider (63-65)
internal/ui/initial.go (1)
  • InitialModel (18-119)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: live-tests (macos-latest)
  • GitHub Check: live-tests (ubuntu-latest)
🔇 Additional comments (7)
internal/types/types.go (4)

63-79: Good time abstraction; enables deterministic tests and clean wiring

TimeProvider + RealTimeProvider/TestTimeProvider are concise and fit the use case well. Zero-alloc RealTimeProvider is a nice touch.


107-125: Consistent time-capture for relative rendering

Capturing now via the provider and reusing it for both getTSRelative and humanize.RelTime is correct and prevents intra-render inconsistencies.


90-90: All UpdateListDesc call sites updated

All invocations of UpdateListDesc now pass the required TimeProvider (m.timeProvider). No stale calls remain.

Call sites verified:

  • internal/ui/update.go:339
  • internal/ui/handle.go:700, 714, 768

90-101: Minor: cache Now() once for consistency and readability

In Task.UpdateListDesc, mirror the pattern used in TaskLogEntry by capturing now once. It avoids two call sites drifting over time and makes tests easier to reason about.

 func (t *Task) UpdateListDesc(timeProvider TimeProvider) {
   var timeSpent string

   if t.SecsSpent != 0 {
     timeSpent = "worked on for " + HumanizeDuration(t.SecsSpent)
   } else {
     timeSpent = "no time spent"
   }
-  lastUpdated := fmt.Sprintf("last updated: %s", humanize.RelTime(t.UpdatedAt, timeProvider.Now(), "ago", "from now"))
+  now := timeProvider.Now()
+  lastUpdated := fmt.Sprintf("last updated: %s", humanize.RelTime(t.UpdatedAt, now, "ago", "from now"))

   t.ListDesc = fmt.Sprintf("%s %s", utils.RightPadTrim(lastUpdated, 60, true), timeSpent)
 }

Likely an incorrect or invalid review comment.

internal/ui/update.go (1)

339-341: Call site updated to pass TimeProvider: LGTM

Using m.timeProvider here aligns with the new signature and keeps UI rendering time-deterministic.

cmd/root.go (1)

250-251: RenderUI wired with RealTimeProvider: LGTM

Passing types.RealTimeProvider{} from the entrypoint cleanly anchors the UI to a pluggable time source in production.

internal/ui/ui.go (1)

19-51: RenderUI signature and frame timestamping via TimeProvider look correct

  • Signature change is consistent with broader wiring.
  • Frames directory timestamp uses provider.Now().Unix(), enabling reproducible runs under TestTimeProvider when needed.
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch pass-time-provider-as-dependency

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (9)
internal/types/types.go (3)

63-79: TimeProvider abstraction looks good; simple and testable.

The interface and the two concrete providers are straightforward and enable deterministic time in tests.

Consider adding brief doc comments to exported symbols (TimeProvider, RealTimeProvider, TestTimeProvider) to improve discoverability in GoDoc.


90-101: Pin “now” once per call to avoid subtle drift and double syscalls.

Call Now() once and reuse it to keep a consistent reference time within the method and reduce overhead.

Apply:

-func (t *Task) UpdateListDesc(timeProvider TimeProvider) {
+func (t *Task) UpdateListDesc(timeProvider TimeProvider) {
   var timeSpent string

   if t.SecsSpent != 0 {
     timeSpent = "worked on for " + HumanizeDuration(t.SecsSpent)
   } else {
     timeSpent = "no time spent"
   }
-  lastUpdated := fmt.Sprintf("last updated: %s", humanize.RelTime(t.UpdatedAt, timeProvider.Now(), "ago", "from now"))
+  now := timeProvider.Now()
+  lastUpdated := fmt.Sprintf("last updated: %s", humanize.RelTime(t.UpdatedAt, now, "ago", "from now"))

   t.ListDesc = fmt.Sprintf("%s %s", utils.RightPadTrim(lastUpdated, 60, true), timeSpent)
 }

107-131: Reuse a single “now” in TaskLogEntry.UpdateListDesc for consistency. We’ve confirmed that getTSRelative(ts, reference) already accepts a reference time and that the only call site in internal/types/types.go still uses timeProvider.Now() twice. Capture now := timeProvider.Now() once and pass it to both getTSRelative and humanize.RelTime:

 func (tl *TaskLogEntry) UpdateListDesc(timeProvider TimeProvider) {
     timeSpentStr := HumanizeDuration(tl.SecsSpent)

     var timeStr string
     var durationMsg string

-    endTSRelative := getTSRelative(tl.EndTS, timeProvider.Now())
+    now := timeProvider.Now()
+    endTSRelative := getTSRelative(tl.EndTS, now)

     switch endTSRelative {
     case tsFromToday:
         durationMsg = fmt.Sprintf("%s  ...  %s", tl.BeginTS.Format(timeOnlyFormat), tl.EndTS.Format(timeOnlyFormat))
     case tsFromYesterday:
         durationMsg = "Yesterday"
     case tsFromThisWeek:
         durationMsg = tl.EndTS.Format(dayFormat)
     default:
-        durationMsg = humanize.RelTime(tl.EndTS, timeProvider.Now(), "ago", "from now")
+        durationMsg = humanize.RelTime(tl.EndTS, now, "ago", "from now")
     }

     timeStr = fmt.Sprintf("%s (%s)",
         utils.RightPadTrim(durationMsg, 40, true),
         timeSpentStr)

     tl.ListDesc = fmt.Sprintf("%s %s", utils.RightPadTrim(tl.TaskSummary, 60, true), timeStr)
 }
internal/ui/log.go (1)

48-58: Good: interactive path now injects a real-time provider.

This wires the provider cleanly into the interactive flow.

For testability, consider accepting a types.TimeProvider parameter in RenderTaskLog (with a default of RealTimeProvider when omitted) so non-interactive code paths and higher-level tests can also control “now” deterministically.

internal/ui/report.go (1)

52-59: Provider injection aligned with the new plumbing.

Passing types.RealTimeProvider{} into initialRecordsModel keeps interactive reports consistent with the new abstraction.

Similar to logs: consider making RenderReport accept a types.TimeProvider (defaulting to RealTimeProvider) to broaden test control without reaching into internals.

internal/ui/model.go (1)

100-108: Model.timeProvider initialization is covered

All code paths into Model supply a non-nil TimeProvider:

  • internal/ui/initial.go (lines 18, 56–60):
    InitialModel takes timeProvider types.TimeProvider and assigns it to Model.timeProvider.
  • internal/ui/ui.go (line 44):
    Calls InitialModel(..., types.RealTimeProvider{}).

No entry point leaves timeProvider unset, so panics are not possible today.

Optional refactor (future-proofing): in InitialModel, guard against a nil provider:

func InitialModel(db *sql.DB, style Style, timeProvider types.TimeProvider, debug bool) Model {
    if timeProvider == nil {
        timeProvider = types.RealTimeProvider{}
    }
    m := Model{
        db:           db,
        style:        style,
        timeProvider: timeProvider,
        // …
    }
    // …
}
internal/ui/update.go (1)

429-442: Minor: compute “now” once per ctrl+t branch for clarity.

You’re calling Now() twice in separate cases; computing it once reduces duplication and eliminates micro drift.

Apply:

-      switch m.period {
-      case types.TimePeriodWeek:
-        now := m.timeProvider.Now()
+      now := m.timeProvider.Now()
+      switch m.period {
+      case types.TimePeriodWeek:
         weekday := now.Weekday()
         offset := (7 + weekday - time.Monday) % 7
         startOfWeek := now.AddDate(0, 0, -int(offset))
         dr.Start = time.Date(startOfWeek.Year(), startOfWeek.Month(), startOfWeek.Day(), 0, 0, 0, 0, startOfWeek.Location())
         dr.NumDays = 7
       default:
-        now := m.timeProvider.Now()
         nDaysBack := now.AddDate(0, 0, -1*(m.dateRange.NumDays-1))
         dr.Start = time.Date(nDaysBack.Year(), nDaysBack.Month(), nDaysBack.Day(), 0, 0, 0, 0, nDaysBack.Location())
       }
internal/ui/ui.go (1)

9-13: Prefer using the same TimeProvider for framesDir and model wiring (remove direct time.Now usage).

Unifying the time source here removes the last direct time.Now dependency in this flow and improves determinism for tests (e.g., framesDir naming). It also lets you drop the time import.

Apply:

 import (
 	"database/sql"
 	"errors"
 	"fmt"
 	"os"
 	"path/filepath"
-	"time"
 
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/dhth/hours/internal/types"
 )
@@
 	logFramesCfg := logFramesConfig{
 		log: logFrames,
 	}
+	// Use a single provider for both framesDir and UI model.
+	tp := types.RealTimeProvider{}
 	if logFrames {
-		framesDir := filepath.Join(".frames", fmt.Sprintf("%d", time.Now().Unix()))
+		framesDir := filepath.Join(".frames", fmt.Sprintf("%d", tp.Now().Unix()))
 		err := os.MkdirAll(framesDir, 0o755)
 		if err != nil {
 			return fmt.Errorf("%w: %s", errCouldnCreateFramesDir, err.Error())
 		}
 		logFramesCfg.framesDir = framesDir
 	}
 
 	p := tea.NewProgram(
 		InitialModel(
 			db,
 			style,
-			types.RealTimeProvider{},
+			tp,
 			debug,
 			logFramesCfg,
 		),
 		tea.WithAltScreen(),
 	)

Also applies to: 31-41, 43-52

internal/ui/handle.go (1)

1-13: Optional: Replace remaining direct time.Now usages in the UI package
To complete the time-refactor in this layer, swap these calls out for your injected/common time provider for consistency and testability:

• internal/ui/ui.go:35 – generating framesDir via time.Now().Unix()
• internal/ui/generate.go:145 – now := time.Now().Local() in randomTimestamp

Review these spots (or justify acceptable exceptions) and update to use the shared Clock/Now API.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 70049e3 and 2b1e7fb.

📒 Files selected for processing (9)
  • internal/types/types.go (4 hunks)
  • internal/ui/handle.go (11 hunks)
  • internal/ui/initial.go (3 hunks)
  • internal/ui/log.go (1 hunks)
  • internal/ui/model.go (2 hunks)
  • internal/ui/report.go (1 hunks)
  • internal/ui/stats.go (1 hunks)
  • internal/ui/ui.go (2 hunks)
  • internal/ui/update.go (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (7)
internal/ui/log.go (1)
internal/types/types.go (2)
  • RealTimeProvider (67-67)
  • RealTimeProvider (69-71)
internal/ui/initial.go (2)
internal/types/types.go (1)
  • TimeProvider (63-65)
internal/ui/model.go (1)
  • Model (100-138)
internal/ui/model.go (2)
internal/types/types.go (3)
  • TimeProvider (63-65)
  • DateRange (230-234)
  • TaskStatus (201-201)
internal/ui/styles.go (1)
  • Style (13-44)
internal/ui/report.go (1)
internal/types/types.go (2)
  • RealTimeProvider (67-67)
  • RealTimeProvider (69-71)
internal/ui/ui.go (2)
internal/ui/initial.go (1)
  • InitialModel (18-119)
internal/types/types.go (2)
  • RealTimeProvider (67-67)
  • RealTimeProvider (69-71)
internal/ui/handle.go (1)
internal/ui/model.go (1)
  • Model (100-138)
internal/ui/stats.go (1)
internal/types/types.go (2)
  • RealTimeProvider (67-67)
  • RealTimeProvider (69-71)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: live-tests (ubuntu-latest)
🔇 Additional comments (9)
internal/ui/model.go (1)

156-169: initialRecordsModel correctly initializes timeProvider

The return recordsModel{…} in initialRecordsModel includes the timeProvider: timeProvider assignment, so the new field is properly set for all variants. No further changes are needed.

internal/ui/update.go (1)

336-341: Correct: UpdateListDesc now uses the injected time provider.

This keeps list rendering fully provider-driven.

internal/ui/ui.go (1)

12-12: LGTM: Provider injected into InitialModel at composition root.

Passing a concrete types.RealTimeProvider here is consistent with the new dependency model and keeps main wiring simple.

Also applies to: 43-52

internal/ui/stats.go (1)

63-69: LGTM: TimeProvider threaded into interactive stats records model.

This keeps the interactive path consistent with the rest of the UI’s time abstraction.

internal/ui/initial.go (3)

18-23: LGTM: InitialModel now accepts and stores a TimeProvider.

Cleanly inserted before debug, and the Model.timeProvider field is correctly initialized.

Also applies to: 56-60


125-142: LGTM: initialRecordsModel propagates TimeProvider into recordsModel.

Keeps record views aligned with the same abstraction.


18-23: All call sites match the new signatures

I’ve verified across the UI package that:

  • internal/ui/ui.go invokes InitialModel(db, style, timeProvider, debug, logFramesCfg) (5 args).
  • internal/ui/{stats,report,log}.go invoke initialRecordsModel(kind, db, style, timeProvider, dateRange) (5 args).
  • UpdateListDesc is always called with m.timeProvider—no zero-arg usages remain.
  • No test files reference the old signatures.

No further updates are needed.

internal/ui/handle.go (2)

57-60: LGTM: Replaced direct time.Now with m.timeProvider.Now across flows.

This aligns validation, tracking start/stop, and quick-switch logic with the injected time source. Truncation to second is consistently applied where required.

Also applies to: 79-84, 120-128, 137-141, 393-401, 488-491, 494-499, 524-533, 533-535


699-705: LGTM: List description renderers updated to accept a TimeProvider.

Passing m.timeProvider into Task and TaskLogEntry UpdateListDesc ensures consistent time formatting in list UIs.

Also applies to: 712-716, 767-771

@dhth dhth merged commit 3c4cf98 into main Aug 17, 2025
14 checks passed
@dhth dhth deleted the pass-time-provider-as-dependency branch August 17, 2025 13:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants