Home

Work Log

ADHD-friendly work tracker — single HTML file, runs locally in your browser.

Screenshots

Overview

Kanban board

Today's Flow — Log & Timeblock

Pomodoro

Focus Mode

How to use

Important: The tracker needs to run via a local server (not opened directly as a file) so your data saves correctly between sessions.

Windows

  1. Download work-log.html, launch.bat, and start-server.ps1 into the same folder
  2. Double-click launch.bat
  3. The tracker opens automatically in your browser

Linux / Mac

  1. Download work-log.html and launch.sh into the same folder
  2. Open a terminal in that folder and run:
    bash launch.sh
    
  3. The tracker opens automatically in your browser

Your data is stored locally in your browser — nothing is sent anywhere.

Data lives per-origin, so keep the port stable. Your entries are stored in your browser's localStorage, which is scoped to the exact origin:port you loaded the page from. Both launchers use a fixed port (8080) for this reason — if you ever load the tracker from a different port, your previous data won't show up (it's not gone, it's just stranded on the old port). Reload http://localhost:8080/work-log.html to get it back.

Features

Time tracking

  • Live timer — start a timer on any task, pause and resume, add a handoff note when stopping
  • Billable tracking — mark epics and individual entries as billable; totals shown per task and per day
  • Quick pick — recent tasks shown as chips for fast re-logging
  • Long-running timer warning — dismissible alert once a running timer passes 4 hours, with a one-click stop; also asks whether to stop if you return to the tab after it's sat hidden 30+ minutes mid-session

Tasks

  • Kanban board — drag tasks between To Do / In Progress / Done columns with epic colour coding
  • WIP warning — amber highlight and dismissable banner when more than one task is in progress
  • Task subtasks — split any task into child steps; completing all steps completes the parent
  • Deadlines — date picker with overdue (red) and due-today (amber) highlighting
  • Auto-carry — unfinished tasks roll over to the next day automatically
  • Jira import — paste a Jira CSV export to bulk-add tickets as tasks
  • Weekly plan review — a dismissible checklist appears once a new week begins for any task planned ahead ("upcoming") and dated within it, so you can confirm it's still accurate before the week starts

Today's Flow

  • Flow view — chronological feed merging time entries, log notes, and task updates for the day
  • Log view — editable time log with duration, epic, billable flag, and ad-hoc entry row
  • Proof links & notes — attach a link (Confluence page, Zephyr key, filename, or a URL) and a one-line note to any entry; shown as 🔗/📝 indicators and carried into exports
  • Blocks view — visual 08:00–18:00 timeblock grid auto-filled from logged entries; drag to rearrange
  • Month view — heatmap of hours per day with intensity colouring; monthly summary and task inventory
  • Rolling Summary — AI-ready summary of recent activity across configurable days

Focus mode

  • Focus screen — one-click distraction-free view showing only the active task and next steps
  • Parked thoughts — capture stray thoughts mid-focus without leaving the screen
  • Pomodoro timer — 5 / 10 / 20 min ring timer with session log; visible in focus mode

Planning

  • Today's meetings — fetched live from Outlook calendar (Windows); shows time, duration, Teams join link. Only your own personal calendars are read — your mailbox and its archive, including secondary calendars nested inside another folder — not shared/delegate mailboxes or public folders, which are excluded automatically. A meeting counts as today's if it overlaps today at all — so multi-day events and meetings that run past midnight appear too. ✕ hides a single meeting for the rest of the day, not every meeting sharing its title. If long-running recurring meetings are missing, raise $CalendarLookBackYears in config.local.ps1 (see config.local.example.ps1). If a calendar you don't want still shows up — e.g. a shared calendar that reports as an ordinary folder — name it in $CalendarExcludeNames. http://localhost:8080/api/calendar?debug=1 reports what the collector found.
  • Day streak — consecutive days with logged work

Export & review

  • End the day — one-click summary with test areas and tomorrow's notes, exported as .txt
  • Daily hours entry draft — the End of Day modal shows a draft of the day's hours entry: total hours plus an activity (ticket, ticket) description built from your log. Edit it and press "copy description" to paste into your time-tracking system. One-click submit (Claude in Chrome first, Playwright as fallback) is built but switched off: set EOD_SUBMIT_ENABLED = true in src/js/00-config.local.js to show the submit button, and $EodSubmitEnabled = $true in config.local.ps1 to let the server accept it. It needs start-server.ps1, the Claude Code CLI with the Claude in Chrome extension, and/or a one-time npm run eod:login (Microsoft SSO) for the Playwright fallback, whose page selectors are unverified defaults — override them in scripts/eod-submit.selectors.json.
  • Auto-backup — JSON backup saved automatically on end-of-day to a local JSON backups/ folder
  • Weekly report — copy-to-clipboard draft summarising the calendar week's tracked time grouped by Jira ticket, for writing status updates without reconstructing "what did I touch" by hand
  • Gap report — flags this week's finished, billable entries that are missing both a note and a proof link, with a one-click jump to fix each one

Bullet Journal (BuJo) features

  • Rapid logging — ✏️ button opens a floating capture panel; Enter starts the timer immediately
  • Signifiers — clickable symbol on each entry cycles through meeting / flagged / migrated / cancelled / overtime; cancelled entries are excluded from totals
  • Daily Log — tab view merging time entries, log notes, and task updates in a single chronological feed
  • Monthly Log — heatmap of hours per day with intensity colouring; monthly summary and task inventory
  • Migration — end-of-month close-out flow: carry forward, reschedule, or drop each open task
  • Trackers — custom 28-day progress grids with a daily target and streak counter
  • Sprints — intention-first focus sessions: declare an outcome, run a Pomodoro, then record yes / partly / no

Info widgets

  • Weather — current conditions, rain forecast, sunrise/sunset (Helsinki)
  • Moon phase — current phase, illumination %, and zodiac sign
  • Finnish nameday — fetched live from nimipaivat.fi

Hours submission — security model

  • Off by default. EOD_SUBMIT_ENABLED is false in src/js/00-config.js; nothing is sent until you set it to true in your own src/js/00-config.local.js and $EodSubmitEnabled = $true in config.local.ps1. The browser flag only shows the submit button; the server enforces its own switch and answers POST /api/eod-submit with 403 while $EodSubmitEnabled is not a real $true (the default).
  • Cross-origin requests are refused. start-server.ps1 no longer sends Access-Control-Allow-Origin: *, and the endpoint rejects any request whose Origin is not this server's own http://localhost|127.0.0.1|[::1]:<port>, or whose Host is not a local name (DNS-rebinding), with 403. A web page open in your browser therefore cannot trigger a submission through your signed-in session. Requests with no Origin (e.g. curl on your own machine) are still accepted when submission is enabled.
  • Everything runs on your machine. There is no remote service of ours. scripts/eod-submit.mjs is started by the local server and drives your signed-in Chrome (claude --chrome) or a Playwright profile against $EodSubmitUrl. The date, hours and description go over stdin, never argv. The Chrome route runs the Claude Code CLI, so that entry text is also seen by Claude (Anthropic API); the Playwright route does not involve it.
  • No credentials in the repo. Sign-in is Microsoft SSO done by hand (npm run eod:login); the session cookies live in .timesheet-profile/. scripts/eod-submit.selectors.json holds only optional CSS selector overrides ({} by default).
  • Gitignored local files: config.local.ps1, src/js/00-config.local.js, .timesheet-profile/. Copy config.local.example.ps1 / src/js/00-config.local.example.js and keep the copies untracked.

Troubleshooting

My entries are gone after reopening the page. Entries live in localStorage, scoped to the exact origin:port. Reload http://localhost:8080/work-log.html (see the note under How to use). If you still see nothing, check that your browser isn't set to clear site data on exit, then restore from the newest file in JSON backups/ (written automatically at end-of-day).

The timer didn't save. Same cause as above — a different port or cleared site data. Log the missed time manually from the Log tab.

Project documentation

Document Purpose
ARCHITECTURE.md Module map, data flow, and design decisions
DATA.md localStorage schema dictionary
CONTRIBUTING.md Dev setup, branching strategy, and quality standard
ROADMAP.md Planned and in-progress features
CHANGELOG.md Full release history
QA.md QA checklist and stakeholder sign-offs per release
JSDoc reference Generated API docs for every exported function, rebuilt on every push to main

Testing

A smoke test suite is included. Requires Node.js ≥ 22.22.1 (matches engines in package.json — the actual floor imposed by lint-staged's own minimum).

node smoke-tests.cjs

Or double-click run-tests.bat on Windows.

If your environment has a pre-provisioned Chromium binary at a revision Playwright's own installer can't reach (e.g. an offline sandbox), point the smoke tests at it with PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/path/to/chromium node smoke-tests.cjs instead of running npx playwright install.

To schedule tests to run automatically each morning, run schedule-tests.bat once as Administrator.

To set up automated weekly releases every Friday, run setup-scheduler.ps1 once as Administrator.

00-config.js

00-config.js — App configuration defaults and runtime setters.

Static defaults used when the app is opened directly as a file, or when the local server is not running.

When the local server IS running, location values are overridden at startup by /api/config (via setWeatherConfig), which reads from the gitignored config.local.ps1. Edit config.local.ps1 (copy from config.local.example.ps1) to set your actual location without touching source code.

JIRA_BASE can be overridden without editing this file:

  1. Copy src/js/00-config.local.example.js → src/js/00-config.local.js
  2. Call setJiraBase('https://your-instance.atlassian.net/browse') in that file.
  3. Run npm run build. That file is gitignored and will never be committed.
Source:

01b-migrate.js

01b-migrate.js — Automatic localStorage schema migration.

Runs once per page load, before load() reads from the versioned keys. Each migration entry maps an old key name to the new one. If the old key has data and the new key is empty, the data is copied across and the old key is removed. Already-migrated users are unaffected (the old key simply won't exist).

Adding a new migration: append an entry to MIGRATIONS. Never remove old entries — a user might be upgrading across multiple versions.

It also sweeps out RETIRED_KEYS: keys whose owning feature has been removed, so they do not linger in the browser forever.

Retiring a key: when you delete a feature, append its key to RETIRED_KEYS and drop the key from DATA.md in the same change. Same rule as MIGRATIONS — never remove an entry once added, or users who skipped that version keep the orphan forever.

Source:
See:
  • DATA.md for the full localStorage schema reference.

04b-render-stats.js

04b-render-stats.js — Header stat tiles and sub-stat tiles. Split out of 04-render.js (QA finding: module size). Both render functions only read entries/document and write into the #stat* elements render() already expects to exist; neither depends on render()'s other sections running first or after.

Source:

05b-filesystem.js

05b-filesystem.js — File System Access API helpers for persisting and using a user-granted directory handle for automatic export saves.

Source:

10e-weeklyplan-review.js

10e-weeklyplan-review.js — Weekly plan review checklist.

Surfaces plan tasks explicitly marked 'upcoming' whose date falls within the current ISO week, via the pure findWeeklyPlanReviewTasks() (pure-fns.js), as a dismissible banner once a new week begins — so tasks planned ahead that turned out to already be finished elsewhere get caught before they silently resurface on their date (see 11b-timeblock-carry.js's autoCarryTasks(), which deliberately never touches 'upcoming' tasks). This app has no live Jira connection (14-jira.js is a one-way CSV import), so the checklist can only prompt the user to reconcile manually — it never auto-detects completion.

Source:

12b-changelog-data.js

12b-changelog-data.js Embedded dev-changelog data, split out of 12a-changelog.js: the wl_dev_log storage key (STORE_DEV_LOG), the test-area name lookup table (TEST_AREA_NAMES), and the hardcoded DEV_CHANGES entry array. New changelog entries are appended to DEV_CHANGES here.

A leaf ES module (see LEAF_MODULES in build-config.js) — pure literal data with zero references to shared app state or to functions defined in other src/js files, imported at the top of the built bundle ahead of the concatenated block. That import-first placement is also what satisfies 12c-startup.js's startup block, which calls mergeDevLog() (defined in 12a-changelog.js) — which in turn dereferences STORE_DEV_LOG and DEV_CHANGES — at top level: both are already in scope by the time any concatenated file runs.

Source:

12c-gapreport.js

12c-gapreport.js — End-of-week gap report.

Lists this calendar week's (Mon–Sun) finished, non-cancelled, billable log entries that have neither a proof link nor a note, via the pure findGapReportEntries() (pure-fns.js), so the user can catch missing documentation themselves before a weekly report is due. Non-billable entries are never flagged — there's nothing to bill, so nothing to prove. Clicking a flagged entry jumps straight to it in the Log view with its proof-link/note editor already open — see buildEntryMetaHtml()/bindEntryMetaEvents() in 04a-render-entry-meta.js for the editor itself (added alongside the entry.link/note fields this report reads).

Source:

12c-startup.js

12c-startup.js Top-level app startup orchestration, split out of 12a-changelog.js: loads persisted state, merges the dev changelog, carries tasks forward, runs the initial render/check sequence, and resumes any active timer.

Load-order-sensitive: mergeDevLog() dereferences the top-level consts STORE_DEV_LOG and DEV_CHANGES (12b-changelog-data.js) at call time, so this file must sort AFTER 12b. It must also keep sorting after 12a-changelog.js (whose listener bindings it follows) and before 13-calendar.js's own top-level loadParked()/renderParked() calls and window.__wl test handle.

Source:

12d-weeklyreport.js

12d-weeklyreport.js — Weekly report draft.

Groups this calendar week's (Mon–Sun) finished, non-cancelled, non-utility entries by Jira ticket key via buildWeeklyTicketSummary()/ formatWeeklyTicketSummaryText() (pure-fns-weeklyreport.js), so writing a status report no longer means reconstructing "what did I touch" by hand across Jira/Confluence/memory. Opens a modal with the rendered text and a copy-to-clipboard button — mirrors 12c-gapreport.js's modal wiring and 25-rollingsummary.js's copy-to-clipboard pattern.

Pure helpers (buildWeeklyTicketSummary, formatWeeklyTicketSummaryText) live in pure-fns-weeklyreport.js and are unit-tested there. This module is the localStorage + DOM glue around them.

Source:

22-trackers.js

22-trackers.js — Custom time-goal progress trackers: load/save, rendering, streak calculation, and the new-tracker form lifecycle.

Source:

24-location.js

24-location.js Per-day work location (Remote / Office) shown in the date-nav header where the ISO week number used to sit. The location is stored against each calendar day so navigating with ← → reveals that day's location. Unset days default to Remote (see DEFAULT_WORK_LOCATION in pure-fns.js).

Pure helpers (locationFor, nextLocation, WORK_LOCATIONS) live in pure-fns.js and are unit-tested; this module is the localStorage + DOM glue around them.

The storage key (STORE_LOCATION) is declared in app-constants.js alongside the other wl_*_v1 keys.

Source:

25-rollingsummary.js

25-rollingsummary.js — Rolling 7-day summary tab inside Today's Flow.

Shows per-day rows (date · location · session times · tracked total · top 3 tasks) and a week total. Includes a "Copy" button for standup use.

Pure data calculation lives in buildRollingSummary (pure-fns-rollingsummary.js) and is unit-tested there. This module is the localStorage + DOM glue around it.

Source:

26-eod-entry.js

26-eod-entry.js — End-of-day hours entry form: rendering, clipboard copy, and server submission.

The browser cannot submit directly (CORS + SSO), so submissions go to the local PowerShell server's /api/eod-submit, which drives a saved browser session via scripts/eod-submit.mjs.

Source:

app-constants.js

app-constants.js Static configuration constants: localStorage key names, the built-in category seed list, and the palette offered when creating a new category. Pure literal values with no dependencies on other modules or app state. Extracted from 01-state.js (issue #336) as the first step of the ongoing ES-module extraction — see ARCHITECTURE.md and build-config.js's LEAF_MODULES comment for why this matters.

Source:

cat-utils.js

cat-utils.js — Category lookup helpers (LEAF MODULE).

Stateless category accessors extracted from 02-utils.js (issue #336, extraction #16). All three functions route through a single fallback chain: id → 'other' → hardcoded stub. The returned colour is always sanitised through safeCssColor(), which is the choke point every colour-rendering template in the app relies on (audited in CHANGELOG against XSS alert #2).

Source:

date-labels.js

date-labels.js Stateless date-to-label helpers: is a given date today, and what should it be called ('today' / 'yesterday' / a short locale date). Only depends on dk() (pure-fns) and JS builtins — no module state, no other-file calls. Extracted from 02-utils.js (issue #336) — the rest of that file (getCat*, renderTagRow, viewEntries, calcStreak, etc.) is genuinely entangled with app state and later-loaded render functions and isn't extractable as a whole; see ARCHITECTURE.md's 02-utils.js entry for why.

Source:

entry-billable.js

entry-billable.js — Billable-status helpers for log entries (LEAF MODULE).

Extracted from 05-entries.js and 02-utils.js (issue #336, extraction #19). Both functions depend on category and plan-task lookups, so they are not pure; they live in their own leaf module imported at the top of the built bundle so the concatenated files can call them from shared scope.

Source:

entry-utils.js

entry-utils.js — Entry timestamp and view helpers (LEAF MODULE).

Stateless entry accessors extracted from 02-utils.js (issue #336, extraction #18). Both functions depend only on leaf-module state accessors and pure formatting helpers, making them fully unit-testable without a DOM environment.

Source:

focus-utils.js

focus-utils.js — Keyboard focus trap utility (LEAF MODULE).

Provides trapFocusInOverlay() for WCAG 2.1.2 keyboard accessibility: keeps Tab-key navigation inside an open overlay dialog. Extracted from 02-utils.js (issue #336, extraction #17); 12d-weeklyreport.js previously kept a private copy because it is a leaf ES module and could not import from the concatenated 02-utils.js — that duplication is now resolved.

Source:

pure-fns-backup.js

pure-fns-backup.js Backup retention and JSON-backup payload construction. Split out of pure-fns-export.js (QA 2026-09-07, largest-module finding) — re-exported there via the pure-fns.js barrel. Pure functions with no side-effects and no global state — a leaf ES module (imports only from pure-fns-format.js).

Source:

pure-fns-epics.js

pure-fns-epics.js Pure helpers for epic (category) lifecycle management: deciding which epics have gone stale and should be archived out of the pickers, and filtering a category list down to the ones a user should be offered.

Archiving is deliberately non-destructive. A stale epic keeps its record in the categories array with archived: true, so getCat() (02-utils.js) can still resolve its label and colour for historical log entries, timesheets, and the monthly/weekly reports. Hard-deleting instead would silently repaint every past entry that used it as the grey "other" fallback.

Source:

pure-fns-export.js

pure-fns-export.js Billable-export grouping and merging helpers: entries grouped by category/task, the pasteable timesheet summary line, day bounds, and the per-task note/link lookups the export body renders. The gap report, weekly report draft, rolling summary, and backup helpers that used to live here moved out to their own leaf modules (QA 2026-09-07, largest-module finding) — see pure-fns-gapreport.js, pure-fns-weeklyreport.js, pure-fns-rollingsummary.js, and pure-fns-backup.js. All are re-exported together via the pure-fns.js barrel. Pure functions with no side-effects and no global state — a leaf ES module with no dependencies of its own.

Source:

pure-fns-format.js

pure-fns-format.js CSS/HTML safety, date/time/duration formatting, and billing time rounding. Split out of pure-fns.js (re-exported there as a barrel). Pure functions with no side-effects, no global state, and no imports — a leaf ES module.

Source:

pure-fns-gapreport.js

pure-fns-gapreport.js Gap-report and export-warning helpers: finding undocumented billable work, flagging anomalies (long unbroken blocks, a full day with no break), and rendering the grouped-by-category export body. Split out of pure-fns-export.js (QA 2026-09-07, largest-module finding) — re-exported there via the pure-fns.js barrel. Pure functions with no side-effects and no global state — a leaf ES module (imports only from pure-fns-format.js).

Source:

pure-fns-hours-entry.js

pure-fns-hours-entry.js Builds the single daily entry that "End day" submits to the external hours system. Pure functions with no side-effects and no global state — a leaf ES module with no dependencies of its own.

Source:

pure-fns-rollingsummary.js

pure-fns-rollingsummary.js Rolling per-day summary aggregation, used by the Rolling Summary tab. Split out of pure-fns-export.js (QA 2026-09-07, largest-module finding) — re-exported there via the pure-fns.js barrel. Pure, with no side-effects and no global state — a leaf ES module with no dependencies of its own.

Source:

pure-fns-tasks.js

pure-fns-tasks.js Task/day helpers: rapid-capture inline token grammar, carry-forward status resolution, and per-day work-location helpers. Split out of pure-fns.js (re-exported there as a barrel). Pure functions with no side-effects and no global state — a leaf ES module (imports only from pure-fns-format.js).

Source:

pure-fns-validate.js

pure-fns-validate.js Validators: localStorage schema validators, backup-import validation, and external-API response validators. Split out of pure-fns.js (re-exported there as a barrel). Pure functions with no side-effects, no global state, and no imports — a leaf ES module.

Source:

pure-fns-weeklyreport.js

pure-fns-weeklyreport.js Weekly report draft helpers: grouping a week's entries by Jira ticket and rendering that grouping into text. Split out of pure-fns-export.js (QA 2026-09-07, largest-module finding) — re-exported there via the pure-fns.js barrel. Pure functions with no side-effects and no global state — a leaf ES module.

Source:

pure-fns.js

pure-fns.js Barrel module for the pure-function helpers, which live in themed leaf sub-modules: pure-fns-format.js (CSS/HTML safety, date/time/duration formatting, billing rounding), pure-fns-validate.js (schema, backup, and external-API validators), pure-fns-tasks.js (rapid-capture grammar, carry-forward status, work location), pure-fns-export.js (billable export grouping), pure-fns-gapreport.js (gap report and export warnings), pure-fns-weeklyreport.js (weekly report draft), pure-fns-rollingsummary.js (rolling summary), pure-fns-backup.js (backup retention), and pure-fns-epics.js (epic staleness and archiving) — the last five split out of one former pure-fns-export.js (QA 2026-09-07, largest-module finding). Re-exports every public symbol so existing imports of './pure-fns.js' keep working unchanged.

Source:

signifiers.js

signifiers.js Stateless signifier lookup tables and accessors: what symbol and title an entry's signifier (event/flagged/migrated/cancelled/overtime, or the billable default) displays as. Only depends on JS builtins — no module state, no other-file calls. Extracted from 10b-signifiers.js (issue #336) — the rest of that file (SIG_CYCLE, cycleSignifier, sigHtml, bindSignifierClicks) stays behind: it reads the entries module state and calls save()/render(), and does DOM binding, none of which a standalone ES module can reach.

Source:

state.js

state.js — accessor layer for core shared mutable state; prerequisite for ES-module extraction (#423, #336).

Source: