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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Removed duplicate `generateTimeOptions()` helper functions from all dialog components

### Fixed
- Improved Weekly Report error messages to distinguish between distinct Gemini API failure modes
— `src/hooks/useReportSummary.ts` (added `classifyGeminiError()` and `classifyFinishReason()` helpers; 429 rate limit vs. quota exhaustion, 503 overload, 500 server error, 504 timeout, 403 bad key, 400 precondition, 404 model not found, network failures, and content-blocked responses each produce specific actionable messages instead of Gemini's generic "high demand" text)
- Fixed Weekly Report page only reading archived data from localStorage, causing empty reports for authenticated (Supabase) users
— `src/pages/Report.tsx`, `src/utils/reportUtils.ts` (replaced direct `localStorage.getItem()` call with `useTimeTracking().archivedDays` via new `dayRecordsToArchivedDays()` adapter; both storage modes now work correctly)
- Resolved merge conflicts in `src/index.css`

## [0.21.1] - 2026-02-06
Expand Down
46 changes: 44 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# CLAUDE.md - AI Assistant Codebase Guide

**Last Updated:** 2026-02-06
**Version:** 1.0.4
**Last Updated:** 2026-03-09
**Version:** 1.0.6

This document provides comprehensive guidance for AI assistants working with the TimeTracker Pro codebase. It covers architecture, conventions, workflows, and best practices.

Expand Down Expand Up @@ -46,6 +46,7 @@ TimeTracker Pro is a modern time tracking application built for freelancers, con
- **Archive System**: Permanent record of completed work days
- **Export/Import**: CSV and JSON formats, database-compatible schema
- **Revenue Tracking**: Automatic calculation based on hourly rates
- **Weekly Report**: AI-generated work summaries (standup, client, retrospective tones) sourced from `archivedDays` via context — works in both guest and authenticated modes
- **Responsive Design**: Works on desktop and mobile

### Project Origin
Expand Down Expand Up @@ -225,6 +226,7 @@ TimeTrackerPro/
│ │ ├── useAuth.tsx # Auth hook
│ │ ├── useOffline.tsx # Offline state hook (PWA)
│ │ ├── useTimeTracking.tsx # Time tracking hook
│ │ ├── useReportSummary.ts # Gemini API call + error classification for /report
│ │ ├── use-toast.tsx # Toast notifications
│ │ └── useRealtimeSync.ts # Database sync
│ ├── lib/ # Utility libraries
Expand All @@ -235,12 +237,14 @@ TimeTrackerPro/
│ │ ├── Archive.tsx # Archived days
│ │ ├── ProjectList.tsx # Project management
│ │ ├── Categories.tsx # Category management
│ │ ├── Report.tsx # AI weekly summary generator
│ │ ├── Settings.tsx # App settings
│ │ └── NotFound.tsx # 404 page
│ ├── services/ # Business logic services
│ │ └── dataService.ts # Data persistence layer
│ ├── utils/ # Utility functions
│ │ ├── timeUtil.ts # Time formatting helpers
│ │ ├── reportUtils.ts # Report grouping, serialization, and DayRecord→ArchivedDay adapter
│ │ └── supabase.ts # Supabase utilities
│ ├── App.tsx # Root component
│ ├── main.tsx # Application entry point
Expand Down Expand Up @@ -888,6 +892,22 @@ setTasks([...tasks, newTask]);
await forceSyncToDatabase(); // Or wait for critical event
```

#### 6. Bypassing the DataService (reading localStorage directly)
```typescript
// ❌ WRONG - Reads localStorage only; breaks for authenticated users whose
// data lives in Supabase, not localStorage
const raw = localStorage.getItem("timetracker_archived_days");

// ✅ CORRECT - Use the context, which routes through LocalStorageService
// or SupabaseService automatically
const { archivedDays } = useTimeTracking();
```

Any utility that needs archived days must consume the context or accept
`DayRecord[]` as a parameter. Use `dayRecordsToArchivedDays()` from
`src/utils/reportUtils.ts` to convert to the `ArchivedDay[]` shape that
the report utils expect.

### Architecture Gotchas

#### 1. Manual Sync Required
Expand Down Expand Up @@ -922,6 +942,26 @@ const { data: { user } } = await supabase.auth.getUser();
const user = await getCachedUser();
```

#### 5. Gemini API Error Classification

Do **not** surface Gemini's raw `error.message` string to the user — it is often
vague ("high demand", "Resource has been exhausted") and gives no guidance on
whether to wait, fix a key, or switch plans.

Always classify by HTTP status + `error.status` using `classifyGeminiError()`
in `src/hooks/useReportSummary.ts`. Key distinctions:

| Symptom | HTTP | Gemini status | Meaning |
|---------|------|---------------|---------|
| Multiple consecutive failures | 503 | `UNAVAILABLE` | Server overloaded — retry in seconds |
| First request of the day fails | 429 | `RESOURCE_EXHAUSTED` + "quota" | Daily free-tier limit hit |
| Rapid retries fail | 429 | `RESOURCE_EXHAUSTED` + "rate" | Per-minute RPM limit — wait 30–60 s |
| Always fails | 403 | `PERMISSION_DENIED` | Bad API key |
| Always fails in region | 400 | `FAILED_PRECONDITION` | Free tier not available in region |

Also handle `finishReason` on 200 OK responses: a `SAFETY` or `RECITATION`
block returns HTTP 200 with an empty `text` — use `classifyFinishReason()`.

### Performance Gotchas

#### 1. Avoiding Unnecessary Re-renders
Expand Down Expand Up @@ -1063,6 +1103,8 @@ Before making changes, verify:

| Version | Date | Changes |
|---------|------|---------|
| 1.0.6 | 2026-03-09 | Added Gemini API error classification in `useReportSummary.ts`; added gotcha table for HTTP status mapping; added `useReportSummary.ts` to file organization |
| 1.0.5 | 2026-03-09 | Fixed Report page to use TimeTrackingContext instead of direct localStorage read; added `dayRecordsToArchivedDays()` adapter; added DataService bypass pitfall |
| 1.0.4 | 2026-02-06 | Replaced custom scroll picker with native HTML5 time inputs for better UX and a11y |
| 1.0.3 | 2026-02-06 | Added ScrollTimePicker component, replaced time dropdowns with scroll-wheel UI |
| 1.0.2 | 2026-02-02 | Added auto-open New Task form feature when day starts |
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ TimeTracker Pro is a professional time tracking application that helps you monit
- **Multiple Export Formats** - CSV, JSON, and invoice formats
- **CSV Import** - Import existing time data from other tools
- **Print Support** - Print-friendly archive views for physical records
- **Weekly Report** - AI-generated work summaries from archived days (standup, client, or retrospective tone); works in both guest and authenticated modes; errors surface specific, actionable guidance (rate limit vs. quota exhaustion vs. overload vs. key issues)

### Progressive Web App

Expand Down Expand Up @@ -625,12 +626,14 @@ TimeTrackerPro/
│ │ ├── Archive.tsx # Archived days
│ │ ├── ProjectList.tsx # Project management
│ │ ├── Categories.tsx # Category management
│ │ ├── Report.tsx # AI weekly summary generator
│ │ ├── Settings.tsx # App settings
│ │ └── NotFound.tsx # 404 page
│ ├── services/ # Data Layer
│ │ └── dataService.ts # Persistence abstraction
│ ├── utils/ # Utilities
│ │ └── timeUtil.ts # Time formatting
│ │ ├── timeUtil.ts # Time formatting
│ │ └── reportUtils.ts # Report grouping, formatting, and DayRecord adapter
│ ├── App.tsx # Root component
│ └── main.tsx # Entry point
├── public/
Expand Down Expand Up @@ -1066,6 +1069,8 @@ const MyPage = lazy(() => import("./pages/MyPage"));
For a detailed list of changes, new features, and bug fixes, see [CHANGELOG.md](CHANGELOG.md).

**Recent Updates:**
- Improved Weekly Report error messages to distinguish Gemini API failure modes (rate limit, quota, overload, key issues)
- Fixed Weekly Report for authenticated users (data now sourced from Supabase, not localStorage only)
- Native HTML5 time inputs for intuitive, accessible time selection
- Consistent UX with date inputs across all dialogs
- Mobile-optimized with browser-native time pickers
Expand Down
6 changes: 3 additions & 3 deletions dev-dist/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ if (!self.define) {
const singleRequire = (uri, parentUri) => {
uri = new URL(uri + ".js", parentUri).href;
return registry[uri] || (

new Promise(resolve => {
if ("document" in self) {
const script = document.createElement("script");
Expand All @@ -35,7 +35,7 @@ if (!self.define) {
resolve();
}
})

.then(() => {
let promise = registry[uri];
if (!promise) {
Expand Down Expand Up @@ -79,7 +79,7 @@ define(['./workbox-21a80088'], (function (workbox) { 'use strict';
*/
workbox.precacheAndRoute([{
"url": "index.html",
"revision": "0.n3k1cm3l0gs"
"revision": "0.veugrm1c6no"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
Expand Down
Binary file modified public/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/favicon-96x96.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/favicon.ico
Binary file not shown.
Binary file added public/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 0 additions & 3 deletions public/favicon.svg

This file was deleted.

Binary file added public/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/icons/icon-96x96.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/web-app-manifest-192x192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/web-app-manifest-512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const SiteNavigationMenu = () => {
className="flex items-center text-gray-900 hover:text-blue-700"
>
<img
src="favicon-96x96.png"
src="icon.png"
alt="Logo"
className="w-8 h-8 sm:mr-2"
/>
Expand Down
136 changes: 124 additions & 12 deletions src/hooks/useReportSummary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,100 @@ export interface UseReportSummaryReturn {
// Constants
// ---------------------------------------------------------------------------

const GEMINI_MODEL = "gemini-3-flash-preview";
const GEMINI_MODEL = "gemini-2.5-flash"; // Free-tier Gemini model as of June 2024. Check ai.google.dev for updates.
const GEMINI_API_BASE = "https://generativelanguage.googleapis.com/v1beta/models";

// ---------------------------------------------------------------------------
// Error classification
// ---------------------------------------------------------------------------

interface GeminiErrorBody {
error?: {
code?: number;
message?: string;
status?: string;
};
}

/**
* Maps a Gemini API HTTP status + error body to a human-readable message.
* Distinguishes between quota exhaustion, rate limiting, overload, and
* configuration problems so the user knows what action to take.
*/
function classifyGeminiError(status: number, body: GeminiErrorBody, retryAfter: string | null): string {
const apiStatus = body?.error?.status ?? "";
const apiMessage = body?.error?.message ?? "";
const lower = apiMessage.toLowerCase();

switch (status) {
case 429: {
// RESOURCE_EXHAUSTED covers both per-minute rate limits and daily quota.
// Distinguish by the message text Gemini includes.
const waitHint = retryAfter ? ` Retry-After: ${retryAfter}s.` : " Wait 30–60 seconds and try again.";
if (lower.includes("quota") || lower.includes("exceeded") || lower.includes("billing")) {
return "Daily free-tier quota exhausted for this API key. Usage resets at midnight Pacific Time. Check your quota at aistudio.google.com, or try again tomorrow.";
}
return `Rate limit reached — too many requests in a short period.${waitHint}`;
}

case 503:
// This is the actual "high demand / overloaded" error — distinct from quota.
return "The Gemini service is temporarily overloaded (HTTP 503). This is a server-side capacity issue, not a quota or key problem. Wait a few seconds and try again.";

case 500:
// Often caused by an oversized input context on Gemini's end.
return "Gemini encountered an internal server error (HTTP 500). This may be caused by an unusually large date range. Try a shorter range, or wait a moment and retry.";

case 504:
return "The request timed out before Gemini could respond (HTTP 504). Try a shorter date range or retry in a moment.";

case 403:
if (apiStatus === "PERMISSION_DENIED") {
return "API key invalid or lacks permission for this model. Verify VITE_GEMINI_API_KEY at aistudio.google.com.";
}
return `Access denied (HTTP 403). Check your API key permissions.`;

case 400:
if (apiStatus === "FAILED_PRECONDITION") {
return "This Gemini model is not available on the free tier in your region. Enable billing at console.cloud.google.com or use a different region.";
}
return `Invalid request (HTTP 400): ${apiMessage || "the prompt was malformed"}. Try a different date range.`;

case 404:
return "Gemini model not found (HTTP 404). The model name in useReportSummary.ts may be outdated — check ai.google.dev for current model IDs.";

default:
return apiMessage
? `Gemini error (HTTP ${status}): ${apiMessage}`
: `Unexpected Gemini API error (HTTP ${status}). Try again in a moment.`;
}
}

/**
* Maps a non-STOP finishReason to a human-readable message.
* Returns null if the reason is STOP or absent (normal completion).
*/
function classifyFinishReason(reason: string): string | null {
switch (reason) {
case "SAFETY":
return "The summary was blocked by Gemini safety filters. Try adjusting your task descriptions or selecting a different date range.";
case "RECITATION":
return "Gemini declined to generate the summary due to a potential content policy concern (recitation). Try rephrasing task descriptions.";
case "BLOCKLIST":
case "PROHIBITED_CONTENT":
case "SPII":
return "The summary was blocked by Gemini content policy. Check task descriptions for flagged content and try again.";
case "MAX_TOKENS":
// Handled before this function is called (when partial text exists).
// Falls through to the default for the edge case of MAX_TOKENS + no text.
return "The summary was cut off by the API token limit. Try selecting a shorter date range.";
case "OTHER":
return "Gemini stopped generating for an unspecified reason. Try again.";
default:
return null;
}
}

// ---------------------------------------------------------------------------
// Hook
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -89,42 +180,63 @@ export function useReportSummary(): UseReportSummaryReturn {
},
],
generationConfig: {
maxOutputTokens: 512,
// No maxOutputTokens cap — let the model produce a complete response.
// gemini-2.5-flash is a thinking model: thinking tokens share the same
// output budget, so a low cap (e.g. 512) leaves almost nothing for
// visible text. Disabling thinking via thinkingBudget:0 avoids this
// entirely for a simple summarization task.
temperature: 0.7,
thinkingConfig: {
thinkingBudget: 0,
},
},
}),
});

if (!response.ok) {
const errBody = await response.json().catch(() => ({}));
const message =
errBody?.error?.message ?? `Gemini API error ${response.status}`;
throw new Error(message);
const errBody: GeminiErrorBody = await response.json().catch(() => ({}));
const retryAfter = response.headers.get("Retry-After");
throw new Error(classifyGeminiError(response.status, errBody, retryAfter));
}

const data = await response.json();

// Gemini response shape:
// { candidates: [{ content: { parts: [{ text: "..." }] } }] }
// { candidates: [{ finishReason: string, content: { parts: [{ text: "..." }] } }] }
const candidate = data.candidates?.[0];
const finishReason: string = candidate?.finishReason ?? "STOP";

const text: string =
data.candidates?.[0]?.content?.parts
candidate?.content?.parts
?.filter((p: { text?: string }) => typeof p.text === "string")
.map((p: { text: string }) => p.text)
.join("") ?? "";

// Check MAX_TOKENS before the empty-text guard: a truncated response
// has partial text so !text is false, but it is not a complete summary.
if (finishReason === "MAX_TOKENS") {
throw new Error(
"The summary was cut off by the API token limit. Try selecting a shorter date range and regenerate."
);
}

if (!text) {
const finishMessage = classifyFinishReason(finishReason);
throw new Error(
"No text returned from Gemini. The prompt may have been blocked by safety filters."
finishMessage ?? `No text returned from Gemini (finishReason: ${finishReason || "unknown"}).`
);
}

setSummary(text.trim());
setState("success");
} catch (err) {
setState("error");
setError(
err instanceof Error ? err.message : "An unexpected error occurred."
);
// Distinguish network/connectivity failures from API errors
if (err instanceof TypeError && err.message.toLowerCase().includes("fetch")) {
setError("Network error — could not reach Gemini. Check your internet connection and try again.");
} else {
setError(err instanceof Error ? err.message : "An unexpected error occurred.");
}
}
},
[]
Expand Down
9 changes: 7 additions & 2 deletions src/pages/Report.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import {
loadArchivedDays,
dayRecordsToArchivedDays,
groupByCalendarWeek,
groupByDateRange,
getMostRecentCompleteWeek,
Expand All @@ -35,6 +35,7 @@ import {
ReportTone
} from '@/utils/reportUtils';
import { useReportSummary } from '@/hooks/useReportSummary';
import { useTimeTracking } from '@/hooks/useTimeTracking';
import SiteNavigationMenu from '@/components/Navigation';

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -387,7 +388,11 @@ function ErrorState({
// ---------------------------------------------------------------------------

export default function Report() {
const archivedDays = useMemo(() => loadArchivedDays(), []);
const { archivedDays: rawArchivedDays } = useTimeTracking();
const archivedDays = useMemo(
() => dayRecordsToArchivedDays(rawArchivedDays),
[rawArchivedDays]
);
const calendarWeeks = useMemo(
() => groupByCalendarWeek(archivedDays),
[archivedDays]
Expand Down
Loading
Loading