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
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,54 @@

## [Unreleased]

### Added

- **Activity Report Tool Window** - New IDE tool window accessible from the status bar popup showing detailed activity breakdown:
- Tree-table view with daily activity organized by project
- Commit history display with hash and message
- Project filter dropdown to view all projects or a specific one
- Auto-refresh every 10 seconds to show live data
- Expand/collapse all functionality

- **CSV Export for Invoicing** - Export activity data to CSV format:
- Date range selection dialog
- Includes date, project, hours (decimal), and commit descriptions
- Proper CSV escaping for special characters

- **Git Branch and Commit Tracking** - Enhanced VCS integration:
- Track time spent per Git branch within each project
- Record commits with hash, message, author, timestamp, and changed files count
- Branch change listener to track branch switches

- **Auto-Pause Settings** - Configure tracking behavior via "Auto-Pause..." in status bar popup:
- Toggle pause when IDE loses focus
- Configure inactivity timeout with minutes and seconds precision (10 sec - 60 min range)

### Changed

- **UTC Timezone Storage** - Local storage now uses UTC timezone for hour buckets:
- Consistent data storage regardless of timezone changes
- Automatic conversion to local timezone for display in Activity Report

- **Idempotent Hub Sync** - Improved data sync reliability:
- Added `recordId` field for local storage records
- Prevents data duplication on double-sync while supporting multiple IDEs
- Live reporting still uses delta (ADD) mode for real-time updates

- **Reworked Time Tracking Architecture** - Internal improvements:
- New `ProjectTimeAccumulator` for per-project time accumulation
- `CodingTimeCalculator` for total coding time calculations
- Better separation of concerns between tracking and reporting

- **Analytics Event Types** - Cleaner analytics tracking:
- Unique event type constants for each trackable action
- Removed Map-based properties in favor of descriptive event names

### Fixed

- Activity Report now shows same totals as status bar widget (includes unsaved deltas)
- Improved data persistence during IDE shutdown with final flush

## [1.5.2] - 2025-12-31

- Update README
Expand Down
77 changes: 77 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build Commands

```bash
# Build the plugin
./gradlew build

# Run IntelliJ IDE with the plugin installed for testing
./gradlew runIde

# Format code (uses Google Java Format via Spotless)
./gradlew spotlessApply

# Run tests
./gradlew test

# Verify plugin compatibility
./gradlew verifyPlugin
```

## Architecture Overview

CodeClocker is an IntelliJ IDEA plugin that tracks coding time and activity. It operates in two modes:
- **Local Mode** (default): All tracking data stays on the user's machine
- **Hub Mode** (optional): Syncs data to CodeClocker Hub web dashboards via API key

### Core Components

**Time Tracking Pipeline:**
- `FocusListener` (AWT event listener) → detects user activity in the IDE
- `TimeSpentActivityTracker` → manages activity detection with 2-minute inactivity timeout
- `TimeSpentPerProjectLogger` → accumulates time per project using `ProjectTimeAccumulator`
- `LocalStateRepository` → persists hourly activity snapshots to XML via IntelliJ's `PersistentStateComponent`
- `DataReportingTask` → scheduled task that reports accumulated data to Hub (if API key present)

**VCS/Git Integration:**
- `ChangesActivityTracker` → tracks added/removed lines from VCS
- `GitCommitStatsListener` (CheckinHandlerFactory) → captures commit statistics
- `BranchActivityTracker` → tracks time per git branch
- Git features are optional via `git-features.xml` config that depends on `Git4Idea`

**UI Components:**
- `TimeTrackerWidget` / `TimeTrackerWidgetFactory` → status bar widget showing daily time
- `TimeTrackerPopup` → popup panel with detailed stats and goal progress
- `BranchActivityToolWindowFactory` → tool window for branch activity reports

**State Persistence:**
- Local state stored in `codeclocker-local-state.xml` with hourly granularity
- Data retained for max 2 weeks, auto-cleaned on load
- Hour keys use UTC timezone (migration from local timezone happens automatically)

### Package Structure

- `services/` - Core tracking services (time tracking, VCS changes, per-project accumulation)
- `local/` - Local state persistence and data models
- `reporting/` - Hub sync and HTTP clients for data reporting
- `widget/` - Status bar widget and popup UI
- `toolwindow/` - Branch activity tool window
- `goal/` - Daily/weekly goal tracking and notifications
- `analytics/` - Anonymous usage analytics
- `onboarding/` - First-run onboarding flow
- `apikey/` - API key management for Hub Mode
- `git/` - Git/VCS integration handlers

### Key Design Patterns

- Application-level services registered in `plugin.xml` for singleton tracking components
- Project-level services for project-specific state
- `ListenerRegistrator` is the main startup entry point that initializes all background tasks
- Scheduled tasks use a shared `ScheduledExecutor` thread pool

## Code Style

- Only add comments for complex logic; avoid commenting obvious code
29 changes: 24 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<!-- Plugin description -->

## CodeClocker - Automatic Time & Activity Tracker
## Automatic Time & Activity Tracker

Plugin automatically tracks your **active coding time per project**, shows your progress in the **IDE status bar**, and helps you build consistent habits with **daily/weekly goals**.
Use it **locally (offline)** by default, or optionally sync to **[CodeClocker Hub](https://hub.codeclocker.com/)** for **web dashboards** and **team analytics**.
Expand All @@ -15,10 +15,12 @@ Use it **locally (offline)** by default, or optionally sync to **[CodeClocker Hu

- **Automatic coding time tracking** - records active time while you work, organized by project.
- **Daily & weekly goals** - set targets, monitor progress, and get notified when you reach them.
- **Activity Report** - detailed tree-view of daily activity with project breakdown and commit details.
- **CSV export** - export activity data for invoicing with date range selection.
- **Status bar widget** - see today's tracked time, current activity, and goal progress at a glance.
- **Auto-pause settings** - configure when tracking pauses (IDE focus lost, inactivity timeout).
- **Privacy** - all tracking data stays on your machine in Local Mode.
- **VCS / Git insights** - tracks **added & removed lines** from version control activity.
- **Status bar widget** - see today’s tracked time, current activity, and goal progress at a glance.

**Privacy:** All tracking data stays on your machine in Local Mode.

### Hub Mode (Optional - Web dashboards & team reporting)

Expand All @@ -30,8 +32,25 @@ Enable Hub Mode with an API key to sync activity to **[CodeClocker Hub](https://
- **Contributor overview** (individual hours and activity)
- **Project activity charts** (who contributed and when)
- **Time range filtering** (e.g., last 7 days, custom periods)
- **Data storage** - Activity is synced to CodeClocker Hub only when you enable Hub Mode and provide an API key.

### Activity Report

Click the status bar widget and select **Activity Report...** to open a detailed view of your coding activity:

- **Tree-table view** - Daily activity organized by project with expandable rows
- **Commit history** - See commits with hash and message for each project
- **Project filter** - View all projects or filter to a specific one
- **CSV export** - Export data for invoicing with customizable date range

The Activity Report auto-refreshes every 10 seconds to show live data.

### Auto-Pause Settings

Click the status bar widget and select **Auto-Pause...** to configure tracking behavior:

**Data storage:** Activity is synced to CodeClocker Hub only when you enable Hub Mode and provide an API key.
- **Pause when IDE loses focus** - Automatically pause tracking when you switch to another application
- **Inactivity timeout** - Set how long to wait before pausing when there's no activity (10 seconds to 60 minutes)

<!-- Plugin description end -->

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ platformVersion = 2025.2.5
# Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP
platformPlugins =
# Example: platformBundledPlugins = com.intellij.java
platformBundledPlugins =
platformBundledPlugins = Git4Idea
# Example: platformBundledModules = intellij.spellchecker
platformBundledModules =

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.codeclocker.plugin.intellij.listeners.FocusListener;
import com.codeclocker.plugin.intellij.reporting.DataReportingTask;
import com.codeclocker.plugin.intellij.reporting.TimeComparisonFetchTask;
import com.codeclocker.plugin.intellij.services.BranchActivityTracker;
import com.codeclocker.plugin.intellij.subscription.SubscriptionStateCheckerTask;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
Expand Down Expand Up @@ -44,9 +45,20 @@ public Object execute(
return true;
});

// Initialize branch tracking for this project
initializeBranchTracking(project);

return null;
}

private static void initializeBranchTracking(Project project) {
BranchActivityTracker branchTracker =
ApplicationManager.getApplication().getService(BranchActivityTracker.class);
if (branchTracker != null) {
branchTracker.initializeFromGit(project);
}
}

private static void registerFocusListener() {
Toolkit.getDefaultToolkit().addAWTEventListener(new FocusListener(), FOCUS_EVENT_MASK);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
* Simple facade for tracking analytics events from anywhere in the plugin. Usage:
*
* <pre>
* Analytics.track(AnalyticsEventType.WIDGET_CLICK);
* Analytics.track(AnalyticsEventType.WIDGET_POPUP_ACTION, Map.of("action", "pause"));
* Analytics.track(AnalyticsEventType.STATUS_BAR_WIDGET_CLICK);
* Analytics.track(AnalyticsEventType.POPUP_SET_GOALS_CLICK);
* </pre>
*/
public final class Analytics {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,37 @@
package com.codeclocker.plugin.intellij.analytics;

/** Constants for analytics event types. Using constants instead of enum for extensibility. */
public final class AnalyticsEventType {

private AnalyticsEventType() {}

// Widget events
public static final String STATUS_BAR_WIDGET_CLICK = "status_bar_widget_click";
public static final String WIDGET_POPUP_OPEN = "widget_popup_open";
public static final String WIDGET_POPUP_ACTION = "widget_popup_action";

// API key events
public static final String API_KEY_ENTERED = "api_key_entered";
public static final String API_KEY_CLEARED = "api_key_cleared";
public static final String API_KEY_DIALOG_OPENED = "api_key_dialog_opened";
public static final String API_KEY_DIALOG_CANCELLED = "api_key_dialog_cancelled";
// Widget popup actions
public static final String POPUP_WEB_DASHBOARD_CLICK = "popup_web_dashboard_click";
public static final String POPUP_SAVE_HISTORY_CLICK = "popup_save_history_click";
public static final String POPUP_RENEW_SUBSCRIPTION_CLICK = "popup_renew_subscription_click";
public static final String POPUP_SET_GOALS_CLICK = "popup_set_goals_click";
public static final String POPUP_AUTO_PAUSE_CLICK = "popup_auto_pause_click";
public static final String POPUP_ACTIVITY_REPORT_CLICK = "popup_activity_report_click";

// Plugin lifecycle events
public static final String PLUGIN_STARTED = "plugin_started";
public static final String PLUGIN_STOPPED = "plugin_stopped";

// Action events
public static final String ACTION_SHOW_STATISTICS = "action_show_statistics";

// Feature usage events
public static final String FEATURE_LOCAL_STORAGE_SYNC = "feature_local_storage_sync";

// Error events
public static final String ERROR_DATA_REPORT_FAILED = "error_data_report_failed";
public static final String ERROR_API_REQUEST_FAILED = "error_api_request_failed";

// Onboarding tour user actions
public static final String TOUR_ACTION = "tour_action";
// Onboarding tour events
public static final String TOUR_WELCOME_START = "tour_welcome_start";
public static final String TOUR_WELCOME_SKIP = "tour_welcome_skip";
public static final String TOUR_STATUS_BAR_NEXT = "tour_status_bar_next";
public static final String TOUR_STATUS_BAR_SKIP = "tour_status_bar_skip";
public static final String TOUR_ACTIVITY_POPUP_NEXT = "tour_activity_popup_next";
public static final String TOUR_ACTIVITY_POPUP_SKIP = "tour_activity_popup_skip";
public static final String TOUR_GOALS_SET = "tour_goals_set";
public static final String TOUR_GOALS_LATER = "tour_goals_later";
public static final String TOUR_GOALS_SKIP = "tour_goals_skip";
public static final String TOUR_HUB_CONNECT = "tour_hub_connect";
public static final String TOUR_HUB_SKIP = "tour_hub_skip";

// Goal events
public static final String SET_NEW_GOAL = "set_new_goal";
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,27 @@ public static void persistApiKey(String apiKey) {
}

private static void syncLocalDataToServer(String apiKey) {
try {
DataReportingTask dataReportingTask =
ApplicationManager.getApplication().getService(DataReportingTask.class);
if (dataReportingTask != null) {
dataReportingTask.syncLocalDataToServer(apiKey);
}

// Refetch trends data now that local data has been synced
TimeComparisonFetchTask timeComparisonFetchTask =
ApplicationManager.getApplication().getService(TimeComparisonFetchTask.class);
if (timeComparisonFetchTask != null) {
timeComparisonFetchTask.refetch();
}
} catch (Exception e) {
LOG.warn("Failed to sync local data after API key was set", e);
}
// Run sync on background thread to avoid blocking UI
ApplicationManager.getApplication()
.executeOnPooledThread(
() -> {
try {
DataReportingTask dataReportingTask =
ApplicationManager.getApplication().getService(DataReportingTask.class);
if (dataReportingTask != null) {
dataReportingTask.syncLocalDataToServer(apiKey);
}

// Refetch trends data now that local data has been synced
TimeComparisonFetchTask timeComparisonFetchTask =
ApplicationManager.getApplication().getService(TimeComparisonFetchTask.class);
if (timeComparisonFetchTask != null) {
timeComparisonFetchTask.refetch();
}
} catch (Exception e) {
LOG.warn("Failed to sync local data after API key was set", e);
}
});
}

public static void unsetApiKey() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static com.codeclocker.plugin.intellij.HubHost.HUB_UI_HOST;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

import com.codeclocker.plugin.intellij.subscription.SubscriptionStateCheckerTask;
import com.codeclocker.plugin.intellij.widget.TimeTrackerInitializer;
Expand All @@ -23,12 +24,14 @@ public void actionPerformed(@NotNull AnActionEvent e) {

public static void showAction() {
String text = getText();
boolean hasApiKey = isNotBlank(ApiKeyPersistence.getApiKey());

int result =
Messages.showOkCancelDialog(
text,
"Enter CodeClocker API Key",
"Get API Key",
"Continue Without API Key",
hasApiKey ? "Cancel" : "Continue Without API Key",
Messages.getInformationIcon());
if (result == Messages.YES) {
BrowserUtil.browse(HUB_UI_HOST + "/api-key");
Expand Down Expand Up @@ -63,8 +66,6 @@ private static void showApiKeyInputDialog() {
TimeTrackerInitializer.markApiKeyAsChanged();
ApiKeyPersistence.persistApiKey(apiKey);
ApplicationManager.getApplication().getService(SubscriptionStateCheckerTask.class).schedule();

TimeTrackerInitializer.reinitializeTimerWidgetsRefetchingDataFromHub();
}
}
}
Loading
Loading