A single-file web application for logging time spent on freelance work. Track tasks with a stopwatch-style timer, annotate each day with a description, browse the month at a glance, and export a printable monthly report or a detailed CSV log.
The app ships with two interfaces in one codebase, selected at runtime:
- Simple mode (default) — A streamlined logging UI for the employer-facing version of the tool. The "Save day" button and per-day description feature are hidden, the month view drops the description column, and the PDF report is a single section: total hours at the top of page 1, then the day-by-day task breakdown.
- Full mode — The complete personal-use feature set. Save Day, per-day descriptions, the saved-day ✓ marker in the month view, and the original two-section PDF (Monthly Summary on page 1, Detailed Log starting on page 2).
Switching modes: click the small dot in the bottom-right corner of the page. The dot is barely visible in simple mode; in full mode it turns blue and shows a small full label so the active mode is obvious. The choice is persisted in localStorage under timetracker_mode (separate from the entries/days state key), so switching never touches your data — descriptions you saved in full mode are still there if you flip back.
Feature parity is the goal. Simple mode is a subset of full mode; every entry, day, tag, edit, import, and export must behave identically in both modes for the data they share. Mode gating only hides UI surface area or trims output — it never forks state, storage format, or core logic. Anything stored by either mode is readable and editable by the other.
- Stopwatch Timer — Enter a task name and start a running timer. The display counts up in
HH:MM:SSand updates every second. Stopping the timer logs the session automatically and shows a "Stopped at" timestamp (e.g., "Stopped at 3:12 PM, 4/8/2026") so you can tell how long ago you paused. - Editable Task Names — Task names can be changed while the timer is running, or after the fact by clicking any task name in the activity log. The "Tracking: ..." label updates live as you type.
- Optional Tags — Comma-separated tags (e.g., "Video Editing, General Chemistry") can be attached to any task. Tags display as pills in the log and can be edited inline.
- Resume Tasks — Each entry has a Resume button. Clicking it restarts the timer for that task with the previously accumulated time pre-loaded (e.g., a task with 5 minutes resumes at
00:05:00). When stopped, the new time is added to the existing entry rather than creating a new one. - Delete Entries — Each entry has an × button to remove it individually. Disabled for the currently-running entry.
- Activity Log — Every completed session appears in the log for the day being viewed, with a running total at the bottom.
- Day View / Month View toggle — A button in the header switches between viewing a single day's activities and a month-at-a-glance view.
- Save Day (full mode only) — A Save day button opens a modal showing a per-task summary, a total, and a free-text "Description for the day" (e.g., "General Chemistry editing; Q28 slide review"). Saving marks the day with a ✓ in the month view.
- Month View — Shows one row per day in the selected month with date, total hours, and (in full mode) the saved description. Click any row to jump into that day. Arrow buttons in the header step through months. Today is highlighted.
- Date Warning — When viewing a past or future day, a banner reminds you that new tracked time will be logged to that date and offers a Jump to today shortcut.
- Clear Day — Wipes all entries and the saved description for the day currently being viewed, after a confirmation prompt.
- Detailed Log CSV Export — From the month view, downloads a CSV with one row per
(date, task), columns:Date,Task,Duration (hours),Duration (HH:MM:SS),Tags,Day Description, plus a trailing Month Total footer row. Filename:time-tracker-detailed-YYYY-MM.csv. - Per-Day CSV Export — From inside the Save Day modal, exports just the current day in the same column format.
- CSV Import — From the month view, loads a previously exported detailed-log CSV. The importer:
- Auto-populates day descriptions from the
Day Descriptioncolumn for days that don't already have one (existing descriptions are never overwritten). - Skips duplicates — entries are aggregated by
(date, task)and compared at 2-decimal-hour precision against what's already tracked; matching rows are skipped and reported in a summary alert. - Ignores the Month Total footer row so re-importing your own export won't add a phantom entry.
- Auto-populates day descriptions from the
- Formatted Report (PDF) — A Report (PDF) button on the month view renders a styled report into a hidden iframe and opens the browser's print dialog. Choose "Save as PDF" to get a file named
Time Report - <Month> <Year>.pdf. The layout depends on the current mode:- Simple mode — Page 1 opens with the total-hours header, then goes straight into the day-by-day log: per-day heading and a Task / Duration / Hours table with a day-total row. No monthly summary table, no descriptions.
- Full mode — Page 1 is the cover with the month total plus a Monthly Summary table (Date, Hours, Description, with unique-tag pills per day). Page 2+ is the Detailed Log, one block per day: date heading, italic description, and the same task table.
- In full mode, if any day has a description but zero tracked time, the user is asked to confirm before exporting (this usually means logs were lost or the description is a manual note). Simple mode skips this check since descriptions are not in scope.
The app has two interface modes, toggled by a subtle dot in the bottom-right corner of the window. The choice persists in localStorage and never alters stored entry or day data — switching modes only changes what's shown and how the PDF report is laid out.
- Simple mode (default) — The streamlined interface. Hides the Save day button and per-day description field, and the PDF report is a slimmer "total hours + day-by-day breakdown" with no monthly summary table.
- Full mode — The complete interface. All features are visible: Save Day modal with per-day descriptions, the full PDF report (cover + Monthly Summary on page 1, Detailed Log from page 2 onward), and every import/export option.
Feature parity is a hard requirement. Every tracked entry, tag, duration edit, CSV import/export, and underlying piece of state must round-trip cleanly between modes — toggling the dot is a display choice, never a data choice. Any new feature should work in both modes unless it is inherently tied to a Full-only artifact (e.g., day descriptions). When in doubt, default to building it for both.
- Auto-Save — All entries, per-day descriptions, the selected date, the current view, the month cursor, and any running timer state are persisted to
localStorageafter every change. Reopeningindex.htmlrestores everything, including a timer that was running when the tab closed (elapsed time keeps accruing in the background).
A zero-dependency, single-file implementation. HTML, CSS, and JavaScript all live in index.html — no build step, no framework, no external libraries.
- HTML — Semantic structure: timer section, activity log (day view), month grid (month view), Save Day modal, and a hidden printable iframe for the PDF report.
- CSS — Dark theme (
#0f1117background), CSS grid for the month rows,font-variant-numeric: tabular-numsfor stable digit widths, and a separate print-only stylesheet inlined into the report iframe (Letter portrait, page counters, alternating row tint). - JavaScript — Vanilla JS.
setIntervaldrives the display tick;Date.now()is the source of truth for elapsed time. State is held in two top-level structures (entries,days) and serialized tolocalStorage. CSV is generated/parsed in-place with a custom RFC 4180-aware parser. The PDF report is produced by writing styled HTML into a hidden<iframe>and callingcontentWindow.print().
- Open
index.htmlin any modern web browser. - Type a task name (e.g., "Chapter 3-4 video editing") and, optionally, tags.
- Click Start (or press Enter) to begin the stopwatch.
- Click Stop when you're done. The session is saved to the activity log.
- Use Resume on any entry to keep accumulating time on that task.
- Click any task name to edit it or its tags.
- When the day's done, click Save day, review the summary, write a description, and Confirm. (Or Export day to CSV straight from the modal.)
- Switch to Month View to browse days, jump into any of them, Import a past CSV, export the Detailed Log CSV, or generate the Report (PDF).
TimeTracker/
├── Design Specs.txt # Original product requirements
├── README.md # This file
└── index.html # The complete application (HTML + CSS + JS)
The timer uses Date.now() timestamps rather than incrementing a counter, so elapsed time stays accurate even if the browser throttles setInterval (e.g., when the tab is in the background). The interval fires every 1000 ms to update the display.
Two top-level structures hold all state:
entries: { id, date, task, ms, tags }[]— every logged session.dateis a localYYYY-MM-DDstring so days don't shift with timezone changes.days: { [YYYY-MM-DD]: { description, savedAt } }— per-day descriptions and the timestamp the day was saved.
The currently-running timer also tracks resumingId (the entry being resumed, if any), timerOffset (the pre-loaded ms when resuming), and timerEntryDate (the date the running session will log to, which can differ from the day currently being viewed).
A migration pass on load rolls any pre-existing per-entry description field (legacy data) into the corresponding day's description so older saved state isn't lost.
Date,Task,Duration (hours),Duration (HH:MM:SS),Tags,Day Description
2026-05-10,Social Post 5 editing,3.20,03:12:00,Video Editing; Social,General editing pass
2026-05-10,Email and admin,0.53,00:32:10,,General editing pass
,Month Total,3.73,03:44:10,,
Fields containing commas, quotes, or newlines are wrapped in double quotes with internal quotes doubled (RFC 4180). Tags are joined with ; within the Tags column. The trailing Month Total row has no date and is recognized and skipped by the importer.
On import, both the existing entries array and the imported rows are aggregated by (date, task_lowercase). For each key present in both, the totals are rounded to 2 decimal hours (the export's resolution) and compared; if they match, every imported row for that key is skipped. The user gets a summary alert with the import/skip counts.
This means re-importing your own detailed-log CSV is idempotent — no phantom duplicates, and existing entries are preserved as-is.
The report is built as a complete HTML document (inline <style> with @page rules for Letter portrait, page counters, and break-before: page to put the detailed log on page 2). The document is written into a hidden, zero-size <iframe>, the iframe's title is set to Time Report - <Month> <Year> (browsers use this as the default print-dialog filename), and contentWindow.print() is called. CSS break-inside: avoid keeps each day's block from being split across pages where possible.
The full app state is saved under the timetracker_state key after every mutation. The saved state includes entries, days, selectedDate, view, monthCursor, and any running timer (start timestamp, task name, tag input value, resumingId, timerOffset, timerEntryDate). On load, a migration function tolerantly upgrades legacy shapes; on corrupt data, the app starts fresh.
The simple/full mode flag is stored separately under timetracker_mode ('simple' | 'full', defaulting to 'simple'). It is intentionally kept out of timetracker_state so toggling modes never rewrites entry/day data, and so the data store has no concept of which UI rendered it.
Gating is implemented in two places only:
- CSS —
body.mode-simple .full-only { display: none !important; }hides any element taggedfull-only(currently just the Save Day button), andbody.mode-simple .day-rowcollapses the month-view grid to 3 columns (date / hours / arrow). - JS —
if (mode === 'full')branches inrenderMonthView(description column) andbuildReportHTML/exportReport(PDF layout, orphan-description warning).
There is no separate "simple mode" code path for state, storage, timer, entry editing, tags, CSV import/export, or any other core behavior. Adding a new feature that touches the timer, entries, or month-level data should work in both modes by default; only add a mode check if the feature is explicitly UI-only and one mode should not see it.
User-provided text (task names, tags, day descriptions) is escaped before being inserted into the DOM via a textContent/innerHTML roundtrip in escapeHTML(). This applies to every rendering path: the day log, the month grid, the Save Day modal, and the printed report.