From 5747f577f308d619bb3c2af4ec8ece18e843521a Mon Sep 17 00:00:00 2001 From: Aram Petrosyan Date: Sun, 8 Mar 2026 13:36:16 +0400 Subject: [PATCH] feat(insights): local time, focus score threshold, dev server - insights_report: use time.Local instead of UTC for hourly breakdown - bento-dashboard: require 1h of tracked activity before showing focus score - Makefile: run server alongside wails3 in dev target - cmd: godotenv only in main, warn on load failure instead of exit Made-with: Cursor --- Makefile | 9 +- cmd/main.go | 3 +- cmd/serve/serve.go | 5 -- .../components/insights/bento-dashboard.tsx | 85 ++++++++++++------- internal/usage/insights_report.go | 6 +- 5 files changed, 63 insertions(+), 45 deletions(-) diff --git a/Makefile b/Makefile index b70a0d6..928233a 100644 --- a/Makefile +++ b/Makefile @@ -25,10 +25,15 @@ build: cli: go build -ldflags "$(LDFLAGS)" -o bin/focusd-cli cmd/main.go -.PHONY: dev -dev: +server: + go run cmd/main.go serve + +wails3: wails3 dev --port 17000 LDFLAGS="$(LDFLAGS)" +.PHONY: dev +dev: server wails3 &> /dev/null & + .PHONY: tidy tidy: go mod tidy diff --git a/cmd/main.go b/cmd/main.go index cdf0d3d..93835c8 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -13,8 +13,7 @@ import ( func main() { if err := godotenv.Load(); err != nil { - slog.Error("failed to load .env file", "error", err) - os.Exit(1) + slog.Warn("failed to load .env file", "error", err) } root := &cli.Command{Name: "focusd", Commands: []*cli.Command{serve.Command}} diff --git a/cmd/serve/serve.go b/cmd/serve/serve.go index b350008..067e0d4 100644 --- a/cmd/serve/serve.go +++ b/cmd/serve/serve.go @@ -19,7 +19,6 @@ import ( "connectrpc.com/connect" "connectrpc.com/validate" - "github.com/joho/godotenv" _ "github.com/tursodatabase/libsql-client-go/libsql" "github.com/urfave/cli/v3" "golang.org/x/net/http2" @@ -52,10 +51,6 @@ var Command = &cli.Command{ }, }, Action: func(ctx context.Context, cmd *cli.Command) error { - if err := godotenv.Load(); err != nil { - return fmt.Errorf("failed to load .env file: %w", err) - } - gormDB, err := setupDatabase(cmd.String("turso-db-url"), cmd.String("turso-db-token")) if err != nil { return fmt.Errorf("failed to setup database: %w", err) diff --git a/frontend/src/components/insights/bento-dashboard.tsx b/frontend/src/components/insights/bento-dashboard.tsx index a0ff549..d80c734 100644 --- a/frontend/src/components/insights/bento-dashboard.tsx +++ b/frontend/src/components/insights/bento-dashboard.tsx @@ -26,6 +26,8 @@ import { TopDistractionsCard } from "./top-distractions-card"; import { CategoriesCard } from "./categories-card"; import { CommunicationCard } from "./communication-card"; +const MIN_SECONDS_FOR_INSIGHTS = 3600; + // Hourly breakdown chart component function HourlyBreakdownChart({ hourlyData, @@ -159,10 +161,14 @@ export function BentoDashboard() { const isLoading = isStoreLoading || isQueryLoading; - // Real data from backend (values are in seconds, convert to minutes for display) + const productiveSeconds = overview?.UsageOverview?.ProductiveSeconds ?? 0; + const distractiveSeconds = overview?.UsageOverview?.DistractiveSeconds ?? 0; + const totalTrackedSeconds = productiveSeconds + distractiveSeconds; + const hasEnoughData = totalTrackedSeconds >= MIN_SECONDS_FOR_INSIGHTS; + const focusScore = Math.round(overview?.UsageOverview?.ProductivityScore ?? 0); - const productiveMinutes = Math.round((overview?.UsageOverview?.ProductiveSeconds ?? 0) / 60); - const distractiveMinutes = Math.round((overview?.UsageOverview?.DistractiveSeconds ?? 0) / 60); + const productiveMinutes = Math.round(productiveSeconds / 60); + const distractiveMinutes = Math.round(distractiveSeconds / 60); // Get hourly breakdown from backend (already in UsagePerHourBreakdown format with seconds) // Filter out any null values that may come from the backend @@ -243,37 +249,50 @@ export function BentoDashboard() {

Focus Score

-

- {focusScore}% -

-

- When I chose between work and distraction, I won {focusScore}% of the time -

- -
- - - - + {hasEnoughData ? ( + <> +

+ {focusScore}% +

+

+ When I chose between work and distraction, I won {focusScore}% of the time +

+ + ) : ( + <> +

--

+

+ Requires at least 1h of activity +

+ + )}
+ {hasEnoughData && ( +
+ + + + +
+ )} diff --git a/internal/usage/insights_report.go b/internal/usage/insights_report.go index 8231d7f..251cb54 100644 --- a/internal/usage/insights_report.go +++ b/internal/usage/insights_report.go @@ -55,13 +55,13 @@ func resolveEndTime(usage ApplicationUsage, usages []ApplicationUsage, i int) in } func splitSecondsPerHour(startUnix, endUnix int64) map[int]int { - start := time.Unix(startUnix, 0).UTC() - end := time.Unix(endUnix, 0).UTC() + start := time.Unix(startUnix, 0) + end := time.Unix(endUnix, 0) result := make(map[int]int) for cursor := start; cursor.Before(end); { hour := cursor.Hour() - nextHour := time.Date(cursor.Year(), cursor.Month(), cursor.Day(), hour+1, 0, 0, 0, time.UTC) + nextHour := time.Date(cursor.Year(), cursor.Month(), cursor.Day(), hour+1, 0, 0, 0, time.Local) segmentEnd := end if nextHour.Before(end) {