Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
5 changes: 0 additions & 5 deletions cmd/serve/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
85 changes: 52 additions & 33 deletions frontend/src/components/insights/bento-dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -243,37 +249,50 @@ export function BentoDashboard() {
<p className="text-xs font-bold uppercase tracking-widest text-blue-400">
Focus Score
</p>
<p className="text-5xl font-bold text-blue-400 mt-1">
{focusScore}%
</p>
<p className="text-xs text-muted-foreground mt-2">
When I chose between work and distraction, I won {focusScore}% of the time
</p>
</div>
<div className="w-24 h-24">
<svg viewBox="0 0 100 100" className="transform -rotate-90">
<circle
cx="50"
cy="50"
r="40"
fill="none"
stroke="currentColor"
strokeWidth="8"
className="text-blue-500/20"
/>
<circle
cx="50"
cy="50"
r="40"
fill="none"
stroke="currentColor"
strokeWidth="8"
strokeDasharray={`${focusScore * 2.51} 251`}
strokeLinecap="round"
className="text-blue-500"
/>
</svg>
{hasEnoughData ? (
<>
<p className="text-5xl font-bold text-blue-400 mt-1">
{focusScore}%
</p>
<p className="text-xs text-muted-foreground mt-2">
When I chose between work and distraction, I won {focusScore}% of the time
</p>
</>
) : (
<>
<p className="text-3xl font-bold text-muted-foreground/40 mt-1">--</p>
<p className="text-xs text-muted-foreground mt-2">
Requires at least 1h of activity
</p>
</>
)}
</div>
{hasEnoughData && (
<div className="w-24 h-24">
<svg viewBox="0 0 100 100" className="transform -rotate-90">
<circle
cx="50"
cy="50"
r="40"
fill="none"
stroke="currentColor"
strokeWidth="8"
className="text-blue-500/20"
/>
<circle
cx="50"
cy="50"
r="40"
fill="none"
stroke="currentColor"
strokeWidth="8"
strokeDasharray={`${focusScore * 2.51} 251`}
strokeLinecap="round"
className="text-blue-500"
/>
</svg>
</div>
)}
</div>
</CardContent>
</Card>
Expand Down
6 changes: 3 additions & 3 deletions internal/usage/insights_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down