All notable changes to Timetraked will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Manual save now shows a toast confirming the save succeeded, or reporting failure so it isn't silently swallowed. Applies to all three manual-save entry points that share the same handler: the
Cmd/Ctrl+Skeyboard shortcut, the Command Palette's "Save Changes" action, and the Electron menu's Save item.forceSyncToDatabasenow resolves aboolean(trueon full success,falseif any save failed) instead ofvoid, so callers can react to the outcome —src/contexts/TimeTrackingContext.tsx,src/App.tsx,src/contexts/TimeTrackingContext.forceSync.test.tsx(new)
- Desktop build migrated from Electron to Tauri 2 — the native shell is now a Rust binary (
src-tauri/) instead of a bundled Chromium + Node process, cutting installer size and removing the Node main-process attack surface entirely.src/lib/tauriElectronApiShim.tsbridgeswindow.electronAPIonto Tauri'sinvoke/listen, souseElectronBackup.ts,useElectronMenuActions.ts, andelectron.d.tsneeded no changes and keep their historical names. Disk backups, the native app menu, and the quit-flush handshake are now Rust modules (backup.rs,menu.rs,quit_flush.rs); auto-updates are now frontend-driven and Ed25519-signed viatauri-plugin-updater(src/lib/tauriUpdater.ts) instead ofelectron-updater. New scriptspnpm tauri/pnpm tauri:dev/pnpm tauri:buildreplace the oldelectron:*scripts;.github/workflows/tauri-release.ymlreplaceselectron-release.yml.electron/,vite.electron.config.ts, and all Electron npm dependencies are removed —src-tauri/(new),src/lib/tauriElectronApiShim.ts(new),src/lib/tauriUpdater.ts(new),.github/workflows/tauri-release.yml(new),package.json,electron/(removed),vite.electron.config.ts(removed),.github/workflows/electron-release.yml(removed) ClientSheetandCategorySheetmigrated from hand-rolleduseState-per-field forms (duplicated reset-on-openuseEffect, duplicatedisSavingtry/finally, no per-field validation) to React Hook Form + Zod, matching the patternProjectSheetalready used. Both now show inline field-level error messages instead of relying on HTML5required;ClientSheet's contact email field gained real email-format validation —src/components/ClientSheet.tsx,src/components/CategorySheet.tsx- Archived-day task editing moved from a separate modal (
TaskEditInArchiveDialog) to inline expandable rows (ArchivedTaskRow) withinArchiveEditDialog, and the whole-day Edit/view toggle was replaced with always-editable form fields plus a per-row expand — Save/Cancel are enabled whenever any staged edit (day fields or task rows) differs from the saved day, rather than only after entering a dedicated edit mode. Also fixed a rounding inconsistency where the day's end time was rounded to the nearest 15 minutes but the last task's end time was not, leaving a gap between the two; the last task's end time is now rounded to match, both when staging edits and when displaying the resting (unedited) state, via a sharedroundToNearest15Minuteshelper moved tosrc/utils/timeUtil.ts—src/components/ArchivedTaskRow.tsx(new),src/components/ArchivedTaskRow.test.tsx(new),src/components/ArchiveEditDialog.tsx,src/components/ArchiveEditDialog.test.tsx,src/components/TaskEditInArchiveDialog.tsx(removed),src/utils/timeUtil.ts,src/utils/timeUtil.test.ts,src/contexts/TimeTrackingContext.tsx
- Added
electron/menu.test.tsandelectron/preload.test.ts— the only two files underelectron/without test coverage. CoversbuildApplicationMenu's IPC dispatch per menu item and its macOS-vs-non-macOS Preferences/Settings placement, and thecontextBridge-exposedelectronAPIsurface (writeBackup,listBackups,readBackup,requestFlushBeforeQuit,onMenuAction) —electron/menu.test.ts(new),electron/preload.test.ts(new) - Expanded
supabaseService.test.ts(15 → 30 tests) to covergetCurrentDay/saveCurrentDay, thecheckNewSchemaschema-detection cache, andmigrateFromLocalStorage's no-op/no-existing-data/client-name-reconcile/error-swallow paths — previously untested on a 1100+ line service that had only upsert/delete coverage —src/services/supabaseService.test.ts - Regenerated AGENTS.md's test file inventory table, which was missing 3 test files that already existed on disk (
ArchiveEditDialog.test.tsx,ArchivedTaskRow.test.tsx,TimeTrackingContext.forceSync.test.tsx); now lists 38 files / 395 tests —AGENTS.md
ai-proxyEdge Function accepted requests from any origin (Access-Control-Allow-Origin: "*") with no auth check, letting any site consume the project's Gemini quota. Replaced the wildcard with an origin allowlist (production domain, Cloudflare Pages default + branch-preview domains, local dev, the Electronapp://localhostorigin); requests from other origins now get a 403 before any model call is made. Auth wasn't made a hard requirement since the AI summary feature is also used in guest (unauthenticated) mode —supabase/functions/ai-proxy/index.ts- CSV export (
exportToCSV) wrote task fields directly into quoted cells with no formula-injection guard, so a task title/description/project/client starting with=,+,-, or@would open as a live formula in Excel/Sheets on import. Cells matching that pattern are now prefixed with a leading'before quoting;parseCSVImportstrips the same prefix on re-import so round-tripping an exported CSV restores the original value —src/utils/exportUtils.ts,src/utils/exportUtils.test.ts - Supabase data caches (
getCachedProjects/getCachedCategories/getCachedClients) were keyed only by a TTL, not byuser.id, so a future code path reading the cache outside the auth-gated load flow could serve one user's cached data to another. Cache entries are now tagged with theuser.idthey were populated for and a lookup for a different user is treated as a miss —src/lib/supabase.ts,src/lib/supabase.test.ts(new),src/services/supabaseService.ts,src/services/supabaseService.test.ts .env.exampledocumented client-sideVITE_ANTHROPIC_API_KEY/VITE_GEMINI_API_KEYvariables that are never read anywhere insrc/(the AI proxy reads a server-sideGEMINI_API_KEYSupabase Function secret, not a client env var) — removed the misleading lines and pointed at the correctsupabase secrets setsetup instead —.env.example- Electron auto-updater downloaded and self-installed unsigned binaries from GitHub Releases on every launch (
autoDownload/autoInstallOnAppQuitbothtrue, no electron-builder signing config for either platform). Downloading now requires explicit user consent via a dialog shown on theupdate-availableevent; failed update checks are persisted to a disk-backed state file and back off exponentially (1h, doubling up to a 24h cap) instead of retrying — and failing — on every single launch —electron/updater.ts,electron/updater.test.ts(new) - Electron
BrowserWindowrelied on the version-dependent default forwebPreferences.sandboxinstead of setting it explicitly, despite using acontextBridgepreload with no Node API exposure in the renderer. Also switched the Mac menu's "Window" submenu to Electron'swindowMenurole, matching its own recommended default template —electron/main.ts,electron/menu.ts
-
endDaypersisted the exact clock-out timestamp instead of rounding it to the nearest 15 minutes the waystartDayalready rounds the day's start time, so the archived day'sendTime(and the last task's mirroredendTime) kept its raw seconds-precise value whileArchiveEditDialog's Tasks table and day-summary form independently rounded the same values for display — producing a visible mismatch between the "posted"/persisted end time and what the edit dialog showed.endDaynow rounds the effective end time at the source, so archived data and its display agree —src/contexts/TimeTrackingContext.tsx,src/contexts/TimeTracking.test.tsx -
PWA manifest was duplicated three ways:
public/manifest.jsonwas an orphaned copy nothing linked to, and VitePWA'smanifestoption invite.config.tsgenerated a secondmanifest.webmanifestthat got injected intodist/index.htmlalongside the hand-maintained<link rel="manifest" href="/site.webmanifest">, leaving two manifest links in the built page. The actually-usedsite.webmanifestalso pointed at screenshot files that don't exist (desktop-1.png/mobile-1.png). Deleted the orphaned JSON, setmanifest: falseon the VitePWA plugin, and fixedsite.webmanifest's screenshot list to reference the real files —public/manifest.json(removed),public/site.webmanifest,vite.config.ts -
public/web-app-manifest-192x192.png/512x512.pngwere byte-identical duplicates ofpublic/icons/icon-192x192.png/icon-512x512.png. Removed the top-level duplicates and pointed the manifest's icons aticons/—public/web-app-manifest-192x192.png(removed),public/web-app-manifest-512x512.png(removed),public/site.webmanifest -
Removed dead code found during a PWA/
index.cssaudit:src/components/UpdateNotification.tsxwas never imported (superseded byPwaUpdatePrompt.tsx);public/pwa.csshad its mobile input touch-target rule duplicated three times plus several classes with zero references anywhere in the app (.mobile-nav-ios,.landscape-compact,.touch-target,.pwa-loading/@keyframes pulse,.offline-badge,.browser-only,.mobile-viewport);src/index.csshad an empty@layer utilities {}block and unused utilities referencing undefined custom properties (.text-inverted-foreground/--inverted-foreground,-mx-(--card-spacing)/--card-spacing,.bg-foreground-destructive,.text-foreground-destructive) —src/components/UpdateNotification.tsx(removed),public/pwa.css,src/index.css -
Clicking "Restart Now" on the Electron update-ready dialog silently did nothing on macOS, forcing users to reinstall manually. Root cause: Squirrel.Mac (the native updater
quitAndInstall()hands off to) refuses to apply an update unless the app is code-signed, and these builds are intentionally unsigned (issue #220) — the resulting failure was only logged to console via the genericerrorhandler.quitAndInstall()is now tracked as in-flight, and anerrorevent while installing shows a dialog explaining the automatic install failed with a button to open the GitHub Releases page for a manual install, instead of failing with no visible feedback —electron/updater.ts,electron/updater.test.ts -
Keyboard users had no way to bypass the sidebar navigation to reach main content, requiring a full tab through every nav item on each page. Added a "Skip to content" link, visually hidden until focused, as the first focusable element in the app shell; it jumps to a new
<main id="main-content">wrapper around the routed page content —src/App.tsx -
Tooltip content (
TooltipContent) rendered inline in the DOM instead of through a RadixPortal, so opening a tooltip on hover (e.g. the Edit/Delete buttons) pushed the trigger button sideways. WrappedTooltipContentinTooltipPrimitive.Portalso it renders outside the trigger's layout flow —src/components/ui/tooltip.tsx -
Accessibility gaps in modal, keyboard, and live-region flows:
AuthDialogrebuilt on the shadcnDialogprimitive instead of a raw overlaydiv, giving itrole="dialog",aria-modal, a focus trap, Escape-to-close, and focus return;UserMenu's fake<a onClick>sign-in link (nohref, not keyboard-activatable as a link) replaced with a shadcnButton;TaskItemgained a visually-hiddenaria-live="polite"region announcing only start/stop transitions (not the per-second duration tick) plus a focusable,tabIndex-able long-press context-menu trigger;PlannedTaskCard's long-press context-menu trigger got the sametabIndexfix;Settings's Projects/Clients/Categories/Archived Days nav rows switched from<a href>toreact-router-dom'sLinkso they navigate via the SPA router instead of forcing a full page reload —src/components/AuthDialog.tsx,src/components/UserMenu.tsx,src/components/TaskItem.tsx,src/components/PlannedTaskCard.tsx,src/pages/Settings.tsx,src/pages/Settings.test.tsx -
Cross-device data loss in Supabase sync:
saveProjects/saveCategories/saveTodos/savePlannedTasks/saveClients/saveArchivedDaysused to diff the in-memory array against the server and delete any row missing from it, so a stale snapshot on one device (e.g. after another device added data) silently deleted that data on the next bulk save. These bulk saves are now upsert-only; deletion is an explicit, single-row operation (newdeleteProject,deleteCategory,deleteTodomethods onDataService) triggered only by direct user action, mirroring the existingupsertClient/deletePlannedTaskpattern. Removed the now-invalidverifyArchivedDataIntegritycount check, which assumed the local snapshot was authoritative —src/contexts/TimeTrackingContext.tsx,src/services/dataService.ts,src/services/supabaseService.ts,src/services/localStorageService/categories.ts,src/services/localStorageService/projects.ts,src/services/localStorageService/todos.ts,src/services/localStorageService/index.ts,src/services/supabaseService.test.ts(new) -
Accessibility issues from review:
CardTitlenow renders a semantic<h3>instead of a<div>; key dialogs (StartDayDialog,ArchiveEditDialog,ExportDialog) auto-focus their first interactive field on open; persistent icon-only buttons (task/project edit & delete, archive restore/edit, todo delete/clear, duplicate/remove backdated task row) now have aTooltipalongside their existingaria-label; archive and client list rows show a spinner and disable their action button while an archive/restore mutation is in flight (matching the existing pattern inProjectManagement/CategoryManagement), and CSV import inExportDialogdoes the same;ProjectSheet,ClientSheet, andCategorySheetgained the same saving-state spinner/disabled-button treatment.ProjectSheetandArchiveEditDialog's day-edit form were migrated from HTML5required/alert()validation to React Hook Form + Zod, giving inline field-level error messages —src/components/ui/card.tsx,src/components/StartDayDialog.tsx,src/components/ArchiveEditDialog.tsx,src/components/ExportDialog.tsx,src/components/TaskItem.tsx,src/components/ArchiveItem.tsx,src/components/ProjectManagement.tsx,src/components/TaskTrackingPanel.tsx,src/components/BackdatedEntryDialog.tsx,src/components/ClientManagement.tsx,src/components/ProjectSheet.tsx,src/components/ClientSheet.tsx,src/components/CategorySheet.tsx,src/components/TaskItem.test.tsx -
Electron quit-flush disk backup no longer fires its "done" ack on a fire-and-forget basis —
useElectronBackup'swriteBackupnow returns the IPC write result so the quit-flush callback canawaitit, and a failed write (disk full, permissions, unwritableuserData) surfaces a destructive toast (mirroring the existing localStorage write-failure toast) instead of failing silently. Added regression coverage for thebackup:writeIPC handler's error path and thebefore-quit-flushtimeout fallback closing the window even when the renderer never acks —src/hooks/useElectronBackup.ts,src/contexts/TimeTrackingContext.tsx,electron/main.test.ts(new),vite.config.ts -
Electron disk backup did a full
readdirplus per-filestat/unlinkprune pass on every singlebackup:writecall, and the two fire-and-forget call sites (manual sync, day-archiving) queued a fresh write/prune on every invocation with no coalescing — mashing manual sync could queue overlapping disk writes. Pruning now runs only every 5th write; the two non-quit-flush call sites go through a new debouncedwriteBackupDebouncedthat collapses rapid repeated calls into a single write of the latest snapshot (the quit-flush path is unaffected and still writes/awaits immediately) —electron/main.ts,src/hooks/useElectronBackup.ts,src/contexts/TimeTrackingContext.tsx,electron/main.test.ts,src/hooks/useElectronBackup.test.ts -
Mobile dropdowns (category, project, client, status pickers) now render a native
<select>below the768pxbreakpoint instead of the Radix popover, so iOS/Android show their platform-standard full-screen picker. A newResponsiveSelectwrapper branches onuseIsMobile()and is shared acrossNewTaskForm,ProjectSheet,BackdatedEntryDialog,PlannedTaskDialog, andTaskEditInArchiveDialog; the desktop RadixSelectis unchanged —src/components/ui/responsive-select.tsx(new),src/components/NewTaskForm.tsx,src/components/ProjectSheet.tsx,src/components/BackdatedEntryDialog.tsx,src/components/PlannedTaskDialog.tsx,src/components/TaskEditInArchiveDialog.tsx,src/test-setup.ts -
Release workflow's "Create GitHub Release" step failing with "Bad credentials" on every run, even after regenerating the
RELEASE_PATsecret. Removed the PAT dependency entirely: the release is now created with the defaultGITHUB_TOKEN, andelectron-release.ymlswitched from triggering on therelease: publishedevent (which GitHub Actions blocks forGITHUB_TOKEN-created releases, to prevent recursive workflow runs) to aworkflow_runtrigger that watches the Release workflow's completion and reads the published tag from a build artifact —.github/workflows/release.yml,.github/workflows/electron-release.yml -
Electron Release workflow's build step failing with
Error: GitHub Personal Access Token is not set, neither programmatically, nor using env "GH_TOKEN"—electron-builderauto-detectsCI=trueand, seeing thepublishblock inpackage.json'sbuildconfig, tried to publish directly instead of just producing local artifacts (asset upload is already handled by the workflow's separatesoftprops/action-gh-releasestep). Added--publish neverto theelectron:buildscript —package.json -
Electron app requiring two Cmd+Q (or Quit-menu) attempts to fully close: the quit-flush handler intercepts each window's
closeevent withpreventDefault()so it can flush state before closing, but doing so aborts Electron's in-flight quit sequence, not just the window close. The first quit attempt therefore only closed the window and left the app running (macOS keeps a window-less app alive), requiring a second attempt with no window left to intercept. A newbefore-quitlistener now records whether the close was triggered by a real quit request; once the flush completes, that case resumes with a freshapp.quit()instead ofwin.close(), so the app actually terminates on the first attempt. A plain click on the window's own close button (no quit intent) still just closes the window, preserving normal macOS behavior —electron/main.ts,electron/main.test.ts
-
Planned task titles on
PlannedTaskCardare now clickable, opening the same Edit Task sheet as the existing "Edit" menu action, instead of requiring the dropdown/context menu —src/components/PlannedTaskCard.tsx -
Global keyboard shortcuts for the web/PWA build and native menu accelerators for the Electron build:
N(new task, Dashboard only),Cmd/Ctrl+S(save),Cmd/Ctrl+K(command palette — jump to any page or run an action), and?(keyboard shortcuts help). New Task uses a plainNin the web build rather thanCmd/Ctrl+Nbecause Chrome/Firefox reserve that combination for opening a new browser window and never deliver it to page JS from a regular tab; Electron's native menu still usesCmd/Ctrl+Nsince it isn't subject to that reservation.Nis a no-op off the Dashboard route instead of silently navigating there — a newnotifyMenuAction/addMenuActionListenerpub-sub inelectronMenuActions.tslets the already-mounted Dashboard react to it directly, complementing the existing mount-timeconsumePendingMenuActioncheck used when a menu action navigates in from elsewhere. A newCommandPalette(built on the existing shadcnCommand/cmdkprimitives) andKeyboardShortcutsDialog(using the new shadcnKbdcomponent) surface the shortcuts; a keyboard icon in the page header also opens the help dialog. Electron'ssave/command-palette/shortcuts-helpmenu actions are dispatched over the existingmenu:actionIPC channel and handled by an extendeduseElectronMenuActions—src/hooks/useKeyboardShortcuts.ts(new),src/components/CommandPalette.tsx(new),src/components/KeyboardShortcutsDialog.tsx(new),src/components/ui/kbd.tsx(new),src/lib/platform.ts(new),src/lib/electronMenuActions.ts,src/pages/Index.tsx,src/App.tsx,src/hooks/useElectronMenuActions.ts,electron/menu.ts,src/test-setup.ts -
In-app data recovery UI for guest-mode backups — a new "Data Recovery" item in Settings (guest mode only) lists localStorage schema-mismatch sibling-key backups and, on the desktop build, Electron disk snapshots, with a preview of entity counts (archived days, projects, etc.) before restoring. Restoring writes the backup data back into the live storage keys (re-stamped with the current schema version) and reloads. New
ipcMain.handle("backup:list"/"backup:read")pair (filename-pattern validated against path traversal) exposed viaelectronAPI.listBackups/readBackup—electron/main.ts,electron/preload.ts,src/types/electron.d.ts,src/hooks/useElectronBackup.ts,src/services/localStorageService/recovery.ts(new),src/hooks/useDataRecovery.ts(new),src/components/DataRecoveryDialog.tsx(new),src/pages/Settings.tsx -
Electron disk-based backup snapshots — a separate failure domain from localStorage for guest-mode desktop users. A new preload bridge (
contextBridge) exposeswriteBackup/requestFlushBeforeQuitto the renderer; the main process writes timestamped JSON snapshots touserData/backups/(pruned to the most recent 20) viaipcMain.handle("backup:write", ...). Snapshots are written at the same existing critical-save events (postDay,forceSyncToDatabase) plus once more on the Electronbefore-quitlifecycle event, whichpreventDefault()s and waits (with a 3s timeout fallback) for the renderer to flush — covering force-quit/crash cases the DOMbeforeunloadlistener can't catch. No-ops entirely on web/PWA builds —electron/main.ts,electron/preload.ts(new),src/hooks/useElectronBackup.ts(new),src/types/electron.d.ts(new),src/contexts/TimeTrackingContext.tsx,vite.electron.config.ts -
Undo toast for hard-delete actions — deleting a project, category, archived day, or planned task now shows a toast with an Undo action with an 8–10s grace window before the deletion is final, via a new
useUndoableDeletehook. New context methodsrestoreDeletedProject,restoreDeletedCategory,restoreDeletedArchivedDay,restoreDeletedPlannedTaskreinsert (and, for the two that persist immediately, re-persist) the deleted record with its originalid—src/hooks/useUndoableDelete.tsx(new),src/contexts/TimeTrackingContext.tsx,src/components/ProjectManagement.tsx,src/components/CategoryManagement.tsx,src/components/ArchiveEditDialog.tsx,src/components/PlannedTaskCard.tsx
-
localStorage schema-mismatch handling no longer silently destroys data —
backupStaleKeycopies the stale blob to a sibling${key}_v{old}_backup_{timestamp}key before clearing it, instead of a bareremoveItem—src/services/localStorageService/utils.ts,src/services/localStorageService/currentDay.ts,src/services/localStorageService/archivedDays.ts -
localStorage write failures (e.g.
QuotaExceededError) are now surfaced to the user via a destructive toast instead of being silently swallowed in a console-only catch block. All 7save*functions route through a newwriteVersionedhelper that returns{ ok, error }instead of throwing, with a debounce so a single failed bulk sync doesn't spam multiple toasts —src/services/localStorageService/utils.ts,src/services/localStorageService/currentDay.ts,src/services/localStorageService/archivedDays.ts,src/services/localStorageService/categories.ts,src/services/localStorageService/clients.ts,src/services/localStorageService/plannedTasks.ts,src/services/localStorageService/projects.ts,src/services/localStorageService/todos.ts
-
Framer Motion animations on app-layer components — task list add/delete (
AnimatePresence+layoutreflow), active-task scale/ring transition, PWA update/install prompt slide in/out, and alayoutId-based sliding active indicator on the mobile nav —src/pages/Index.tsx,src/components/TaskItem.tsx,src/components/PwaUpdatePrompt.tsx,src/components/InstallPrompt.tsx,src/components/MobileNav.tsx -
Planned task time tracking —
PlannedTasknow accumulates total time worked via atimeEntries: PlannedTaskTimeEntry[]array and a denormalizedtimeSpent(milliseconds) field. Each time a planned task is pulled to the active day (or resumed via "Add to Active Day"), a newPlannedTaskTimeEntryis appended with the linked timed-task id and date; itsdurationis filled in when the timed task ends —src/contexts/TimeTrackingContext.tsx,src/services/supabaseService.ts -
addPlannedTaskToDaycontext method — creates a new timed task linked to anin_progressplanned task without changing its status, allowing a planned task to accumulate time across multiple day sessions or task segments —src/contexts/TimeTrackingContext.tsx -
"Add to Active Day" action on
PlannedTaskCard— visible forin_progresstasks when the day is started and not stale; available in both the dropdown menu and long-press context menu —src/components/PlannedTaskCard.tsx -
Time spent badge on
PlannedTaskCard— displays accumulated duration (e.g.1h 30m) using a violet badge whentimeSpent > 0; replaces the old "Active" badge that showed onlinkedTaskIdpresence —src/components/PlannedTaskCard.tsx -
Time Spent read-only field in
PlannedTaskDialog— shown in edit mode alongside the Status selector so users can see total time logged against a planned task —src/components/PlannedTaskDialog.tsx -
Auto-stop linked timed task when moving a planned task to "done" — if the currently running timed task is linked to the planned task being marked done,
movePlannedTaskends it, accumulates its duration, and persists the day state before applying the status change —src/contexts/TimeTrackingContext.tsx
- Sanitize
contactWebsiteURLs inClientManagementusingURL()before rendering ashref; onlyhttp:andhttps:schemes produce a clickable link — invalid orjavascript:URLs fall back to plain text display —src/components/ClientManagement.tsx(XSS via stored javascript: URI) - Add
rel="noopener noreferrer"to alltarget="_blank"sidebar links to preventwindow.openerexposure —src/components/AppSidebar.tsx - Tighten Electron CSP
script-srcfrom'self' 'unsafe-inline' 'unsafe-eval'to'self'; production build uses only<script type="module">and requires neither directive —electron/main.ts - Upgrade
vite5.4.21 → 6.4.3 to resolveserver.fs.denybypass and NTLMv2 hash disclosure (Windows dev-server, HIGH) —package.json,pnpm-lock.yaml - Upgrade
vitest1.6.1 → 3.2.6 to resolve arbitrary file read/execute via UI server (CRITICAL) —package.json,pnpm-lock.yaml - Pin
react-router-domfloor to^6.30.4to resolve open-redirect via protocol-relative URL (MEDIUM) —package.json - Pin
honoto4.12.25(direct devDep) to resolve CORS credential reflection, path traversal, and Lambda header issues (HIGH) —package.json,pnpm-lock.yaml - Pin
form-datato4.0.6(direct devDep) to resolve CRLF injection via unescaped multipart field names (HIGH) —package.json,pnpm-lock.yaml - Add
.npmrcto declareonly-built-dependenciesallowlist for pnpm v10 (moved frompackage.json#pnpm) —.npmrc
-
Login/logout race conditions for planned tasks resolved. Auth state change handling consolidated into a single
useEffectwith an asynchandleAuthChange;previousAuthStatestate replaced withprevAuthRef(ref) to avoid extra re-renders. On logout,migrateToLocalStoragenow runs beforesetDataServicesoloadDatareads the freshly-written localStorage snapshot instead of racing the migration write.todoLoadedRef/plannedLoadedRefreset beforesetLoading(true)to block mutations against the incoming service until load completes. —src/contexts/TimeTrackingContext.tsx -
Planned tasks no longer overwritten on login. After migrating guest data into Supabase, planned tasks are reloaded from Supabase so the session reflects what migration just wrote.
migrateFromLocalStorageskips writing planned tasks if Supabase already has any, preventing stale localStorage data (left from a previous logout) from overwriting tasks created on another device. —src/contexts/TimeTrackingContext.tsx,src/services/supabaseService.ts -
ProjectSheetnow awaitsforceSyncToDatabase()before closing — previously the sheet dismissed before the sync completed, causing the save to be skipped on fast interactions. —src/components/ProjectSheet.tsx
-
ContextMenuItemgained avariantprop ("default" | "destructive"). Destructive variant renderstext-destructiveat rest and inverts tofocus:bg-destructive focus:text-destructive-foregroundon focus, replacing ad-hocclassNameoverrides on individual menu items. —src/components/ui/context-menu.tsx,src/components/PlannedTaskCard.tsx -
Destructive button hover changed from
hover:bg-destructive/90tohover:opacity-90acrossDeleteConfirmationDialogandProjectManagement— avoids Tailwind v4 color-mix issues with theoklchdestructive token. —src/components/DeleteConfirmationDialog.tsx,src/components/ProjectManagement.tsx -
Button order in
DeleteConfirmationDialogcorrected: Cancel now precedes Delete per standard dialog convention (was reversed). —src/components/DeleteConfirmationDialog.tsx -
--destructive-foregroundCSS token added for both light and dark themes so destructive context menu items and buttons have a legible foreground color when filled. —src/index.css
-
Self-hosted SQL backend (opt-in, additive) — lets local deployments use their own PostgreSQL or MySQL database instead of Supabase or
localStorage. Browsers can't open a raw SQL connection, so this adds a small Express + Knex REST API (server/) that the frontend talks to over HTTP whenVITE_DATA_BACKEND=sqlis set. Supabase andlocalStoragemodes are unchanged when this var is unset.planned_tasksstorestime_entries/time_spent(Kanban board time tracking) the same waySupabaseServicedoes. —server/(new:db.ts,schema.ts,app.ts,index.ts,migrate.ts,seed.ts,repositories/*.ts,types.ts),src/services/sqlApiService.ts(new),src/services/dataService.ts(factory branch),package.json(db:migrate,db:seed,server:dev,server:startscripts;express,knex,pg,mysql2,cors,dotenv,tsxdeps),.env.example,docs/SQL_BACKEND.md(new) -
AppSidebar— new collapsible sidebar navigation component replacing the topSiteNavigationMenu. Uses shadcn/uiSidebarprimitives; groups nav items into Planning/Manage/Reports sections; shows live session timer in the footer alongside sync status, auth, and export actions. —src/components/AppSidebar.tsx(new),src/App.tsx(wrapped inSidebarProvider, rendersAppSidebar) -
PageTitleContext— new React context that decouples page-level header state (title, badge, actions) from layout.PageLayoutwrites to it viausePageTitle; the sidebar header reads from it to render the current page title and action buttons in the top bar. —src/contexts/PageTitleContext.tsx(new),src/contexts/page-title-context.ts(new),src/hooks/usePageTitle.ts(new)
PageLayout.test.tsxmock forusePageTitlewas missingsetBadge, causing all 8 PageLayout tests to throwTypeError: setBadge is not a functionafter thebadgeprop was added toPageLayout. —src/components/PageLayout.test.tsx
PageLayoutrefactored to usePageTitleContext— no longer renders its own heading or navigation; instead callssetTitle/setBadge/setActionson mount viausePageTitleand cleans up on unmount. Addedbadge?: ReactNodeprop. —src/components/PageLayout.tsx
- Expanded test suite from 93 → 187 tests across 14 files. New files cover all previously untested business logic and two key UI components:
src/utils/calculationUtils.test.ts(18 tests) — billing math:getDayStats,getHoursWorkedForDay,getRevenueForDay,getBillableHoursForDay,getNonBillableHoursForDay,getTotalHoursForPeriod,getRevenueForPeriodsrc/utils/checklistUtils.test.ts(13 tests) — GFM checklist parse/toggle:parseTaskChecklist,toggleDescriptionChecklistItemsrc/utils/exportUtils.test.ts(16 tests) — CSV/JSON/invoice export and CSV import:exportToCSV,exportToJSON,generateInvoiceData,parseCSVImportsrc/contexts/TimeTracking.test.tsx(28 new tests, 47 total) — todo items, planned tasks, project CRUD + archive/restore/reset, category CRUD,discardDay,adjustTaskTime,addBackdatedDay,archiveClient/restoreClient(including active-project blocking guard)src/components/NewTaskForm.test.tsx(9 tests) — form render, submit, empty-title validation, whitespace rejection, post-submit collapse, cancel, FAB togglesrc/components/TaskItem.test.tsx(10 tests) — title/description/badge render, Active badge, delete confirm flow, edit dialog open@testing-library/user-eventadded as dev dependency for component interaction testing
loading: booleanexposed onTimeTrackingContextTypeand context value — signals when asyncloadData()has completed; used as the init gate in context integration tests (await waitFor(() => expect(result.current.loading).toBe(false)))
-
Removed
@radix-ui/themes(separate design system, leftover from earlier scaffold). Its<Theme>wrapper applied a.radix-themesclass that set its own--color-background, overriding the shadcn--color-backgroundtoken from@themeand breakingbg-background/theming app-wide after the Tailwind v4 upgrade.Badge/Flexusages replaced with shadcnBadge+ Radix color-scale utility classes (bg-X-3 text-X-11 border-X-6). —src/main.tsx,src/components/KanbanColumn.tsx,src/components/PlannedTaskCard.tsx,src/components/TaskItem.tsx,src/pages/Archive.tsx,package.json -
Pruned unused
@radix-ui/colorsscale imports and their@thememappings insrc/index.css— kept only the 12 scales referenced by components (gray,mauve,slate,red,purple,violet,indigo,blue,cyan,green,brown,orange), removing 18 unused light/dark scale imports and ~250 lines of--color-X-1..12mappings. No behavior change. —src/index.css
- Upgraded to Tailwind CSS v4 via the official
@tailwindcss/upgradecodemod.tailwind.config.tsremoved — theme (semantic colors, Radix scales, radius, keyframes/animations,container) now lives in@theme/@layer baseblocks insrc/index.css. PostCSS now uses@tailwindcss/postcss(replacestailwindcss+autoprefixer). Deprecated utility renames applied across components (outline-none→outline-hidden,shadow-sm→shadow-xs, etc.).components.jsonupdated to drop the now-deletedtailwind.config.tsreference. —src/index.css,postcss.config.js,package.json,components.json, 42 component files
-
Category add/edit forms moved to a Sheet drawer — both
CategoryManagement.tsx(legacy dialog) and the/categoriespage's inline "Add/Edit Category" cards are replaced by a sharedCategorySheet(mirrorsClientSheet/ProjectSheet), opened via "Add Category" or per-category Edit and pre-filled in edit mode. —src/components/CategorySheet.tsx(new: shared add/edit Sheet form),src/components/CategoryManagement.tsx,src/pages/Categories.tsx(removed inline form cards, added sheet state) -
Project add/edit forms moved to a Sheet drawer — the Project Management dialog's inline "Add/Edit Project" card is replaced by a shared
ProjectSheet(mirrorsClientSheet), opened via the "Add Project" button or per-project Edit action and pre-filled in edit mode. Submitting callsaddProject/updateProjectfollowed byforceSyncToDatabase()(project mutations don't auto-save). —src/components/ProjectSheet.tsx(new: shared add/edit Sheet form),src/components/ProjectManagement.tsx(removed inline form card, added sheet state) -
Client editing — clients can now be edited after creation. An Edit button (pencil icon) appears on each active client card and opens a Sheet drawer pre-filled with the client's current name, address, and contact fields. The same Sheet is reused for "Add Client" (previously an inline card form), making both flows consistent.
updateClient(id, data)added toTimeTrackingContext— merges partial data into the existing client, preservesid/createdAt/archived, updatesclientsRef+ state, and returns the updatedClient; callers persist via the existingpersistClient→upsertClientpath (1 Supabase call). Archived clients show no Edit button (restore first). —src/components/ClientSheet.tsx(new: shared add/edit Sheet form),src/components/ClientManagement.tsx(removed inline form card, added edit button + sheet state),src/contexts/TimeTrackingContext.tsx(updateClientmethod in interface + implementation + context value)
-
Electron desktop build target — the app can now be packaged as a native Mac (DMG) or Windows (NSIS) desktop app via Electron.
electron/main.tscreates aBrowserWindow(1280×800,contextIsolation: true,nodeIntegration: false) with a CSP header that allowsself,data:, andhttps://*.supabase.co. Dev mode loadshttp://localhost:8080; production servesdist/via a customapp://protocol handler (required because the app usesBrowserRouter—file://+ pushState breaks without a real origin).vite.electron.config.tscompiles the main process to CJS (dist-electron/main.cjs) in isolation from the app Vite config. New scripts:electron:build:main,electron:dev,electron:preview,electron:build. CI updated to use pnpm exclusively (pnpm/action-setup@v4,pnpm install --frozen-lockfile);package-lock.jsonremoved in favour ofpnpm-lock.yamlas the single lock file. —electron/main.ts(new),electron/tsconfig.json(new),vite.electron.config.ts(new),package.json(packageManager,main, scripts, electron-builderbuildconfig),.gitignore(dist-electron/,dist-electron-build/),.github/workflows/test.yml(pnpm) -
Clients now persist in a dedicated Supabase
clientstable (id, user_id, name, archived, created_at) with RLS, so they sync across devices for signed-in users instead of living in a per-device localStorage blob. A one-time, idempotent SQL backfill seeds the table from each user's distinct project client names. Guest mode continues to use the localStorage blob.SupabaseService.getClients/saveClientswere rewritten to read/write the table (mirroring the projects delete-missing + upsert-by-id pattern). Client reads/writes share a TTL read-cache (getCachedClients/setCachedClients), so a load is one read on cache miss and free thereafter, and clients are excluded from theforceSyncToDatabasebulk save (persisted only when a client actually changes). Adding a client uses a single-rowupsertClient(1 Supabase call) rather than the full-list reconcile (2 calls). —supabase/migrations/20260530_clients.sql(new: table, indexes, RLS policies, backfill),supabase/schema.sql(clients table + RLS for fresh installs),src/services/supabaseService.ts(table-backed client persistence +upsertClient, removed localStorage-blob delegation),src/services/dataService.ts+src/services/localStorageService/clients.ts(upsertClient),src/lib/supabase.ts(client read-cache),src/contexts/TimeTrackingContext.tsx(persistClient/persistClients,addClientreturns the created client) -
Client management with archive protection (and project archiving scoped in for consistency). Clients are now a managed entity: a dedicated Clients page lists active clients with inline add and per-row archive, plus a collapsed Archived section with restore. The project create/edit form replaces the free-text client input with a client dropdown (active clients only) that includes an inline "+ Add new client" option; client strings that don't match a managed client are shown as disabled "(unmanaged)" options so legacy data stays visible. A client cannot be archived while it still owns active (non-archived) projects — the attempt surfaces an inline
Alert(withAlertTriangle) naming the blocking projects. Project archiving was scoped into this feature (not as a standalone feature) specifically to back the client archive guard: projects gained an optionalarchivedflag (normalized at load withproject.archived ?? false, no migration), and the project list grew per-row Archive/Restore actions with a collapsed Archived section. Clients persist under a newCLIENTSstorage key;SupabaseServicestores the client list as a JSON blob in localStorage to avoid a schema migration, and the client list is seeded once from existing project client names on first load. —src/contexts/TimeTrackingContext.tsx(Clienttype,archivedonProject,clientsstate + seeding shim,addClient/archiveClient/restoreClient/archiveProject/restoreProject, client save inforceSyncToDatabase),src/services/dataService.ts(saveClients/getClientsonDataService),src/services/localStorageService/clients.ts(new),src/services/localStorageService/constants.ts(CLIENTSkey),src/services/localStorageService/index.ts,src/services/supabaseService.ts(JSON-blob client persistence),src/components/ClientManagement.tsx(new),src/pages/Clients.tsx(new),src/App.tsx(/clientsroute, lazy-loaded),src/pages/ProjectList.tsx(clientSelect+ inline add, project Archive/Restore + Archived section),src/components/ProjectManagement.tsx(clientSelect+ inline add),src/pages/Settings.tsx("Manage Clients" link) -
Backdated entry creation — "Add Past Entry" button on the Archive page opens a multi-step dialog to log a full past workday (date picker, day-level start/end times, per-task time pickers with category/project selectors, markdown description support, live duration preview). Persists via
addBackdatedDaywhich callsdataService.saveArchivedDayswith optimistic rollback on failure. —src/components/BackdatedEntryDialog.tsx(new),src/contexts/TimeTrackingContext.tsx(addBackdatedDaymethod),src/pages/Archive.tsx(CirclePlus "Add Past Entry" action button)
- Manual sync failed with a 500 / Postgres
21000("ON CONFLICT DO UPDATE command cannot affect row a second time") when saving after a default project had been renamed (or re-cliented). Default project ids are derived from the project name (default-{index}-{slug}) and are preserved across renames, but the load merge deduped saved projects against defaults purely by name+client — so a renamed default no longer matched and was appended as a second row sharing the original default's id, producing a duplicate-id upsert batch. The merge now matches on id first (falling back to name+client for legacy rows), andSupabaseService.saveProjectsadditionally dedups the upsert batch by id (last write wins) as a safety net. —src/contexts/TimeTrackingContext.tsx(id-first merge),src/services/supabaseService.ts(saveProjectsdedup-by-id) - Clients were silently dropped when switching storage modes because
SupabaseService.migrateFromLocalStorageandmigrateToLocalStoragehandled every entity (projects, categories, current day, archived days, todos, planned tasks) except clients. A guest who added clients and then signed in never had those clients written to Supabase, and an authenticated user's clients were never written back to localStorage on sign-out.migrateFromLocalStoragenow reads guest clients, includes them in the early-return guard, and non-destructively upserts any whose name isn't already on the account (so clients synced from another device aren't deleted);migrateToLocalStoragenow writes the account's clients to localStorage. The direct add/archive/restore paths were already correct — only the cross-mode migration was incomplete. —src/services/supabaseService.ts forceSyncToDatabaseused theprojectsclosure variable, which is stale when called immediately after a project mutation (before React re-renders). Project add, edit, delete, archive, and restore viaProjectListall calledforceSyncToDatabaseright after the mutation, causing the pre-mutation project list to be written to storage while the icon incorrectly turned green. AddedprojectsRef(mirroringclientsRef) updated synchronously by every project mutation and at load;forceSyncToDatabasenow readsprojectsRef.currentso the correct list is always saved. —src/contexts/TimeTrackingContext.tsxaddClient,archiveClient, andrestoreClientcalledsetHasUnsavedChanges(true)even though callers immediately persist the change viapersistClient/persistClients. Since clients are excluded fromforceSyncToDatabase's bulk save, the only path to clear the flag was unrelated to the client write, leaving the save icon orange after every client operation despite the data being safely written. Removed thesetHasUnsavedChanges(true)calls from all three client mutators. —src/contexts/TimeTrackingContext.tsx- The project load merge always started from hardcoded defaults and only appended saved projects whose name+client did not match any default. Any edit to a default project (hourly rate, color,
isBillable) was silently discarded on reload and replaced by the hardcoded default values. Changed the merge to replace the default entry with the saved version when name+client match, so user edits to default projects survive page reloads. —src/contexts/TimeTrackingContext.tsx BackdatedEntryDialogimporteduseTimeTrackingfrom@/contexts/TimeTrackingContext(not exported there) — corrected to import the hook from@/hooks/useTimeTrackingand types (DayRecord,Task) from the context —src/components/BackdatedEntryDialog.tsx
- iOS navigation bar redesigned as a floating pill — rounded-full shape, frosted-glass background (
rgba(255,255,255,0.80)), drop shadow, andmb-2 mx-2margins replacing the full-width border-top bar —src/components/MobileNav.tsx PageLayoutno longer renders an inline heading or navigation — page title/badge/actions are delegated toPageTitleContextand rendered by the sidebar header instead (see above) —src/components/PageLayout.tsx- Homepage stats grid changed from single-column to two-column on mobile (
grid-cols-2) —src/pages/Index.tsx - Todo list item alignment in
TaskTrackingPanelchanged fromitems-starttoitems-center—src/components/TaskTrackingPanel.tsx - Theme accent color changed from
orangetoblue—src/main.tsx
- Apple HIG compliance pass for the native iOS app
- Bottom sheets —
TaskEditDialog,StartDayDialog,ArchiveEditDialog, andDeleteConfirmationDialognow render as swipe-to-dismiss vaulDrawersheets on iOS (snap points tuned per dialog complexity) and fall back to the existing centered RadixDialogon web.DeleteConfirmationDialogreverses button order on iOS (destructive action above Cancel) per UIAlertController convention. —src/components/ui/adaptive-dialog.tsx(new),src/components/ui/drawer.tsx,src/components/TaskEditDialog.tsx,src/components/StartDayDialog.tsx,src/components/ArchiveEditDialog.tsx,src/components/DeleteConfirmationDialog.tsx - Haptic feedback —
useHapticswraps@capacitor/haptics; light impact on tab switches and Edit, medium on Delete, heavy on destructive confirm, success notification on task creation and day archive, error notification on sync failure. No-op on web. —src/hooks/useHaptics.ts(new),src/components/MobileNav.tsx,src/components/TaskItem.tsx,src/components/DeleteConfirmationDialog.tsx,src/contexts/TimeTrackingContext.tsx - App lifecycle persistence —
useAppLifecycleuses@capacitor/app'sappStateChangeevent (fires at the Swift layer before WKWebView freezes) instead ofvisibilitychangefor the emergency localStorage backup, eliminating the race condition on rapid app backgrounding. Falls back tovisibilitychangeon web. —src/hooks/useAppLifecycle.ts(new),src/contexts/TimeTrackingContext.tsx - Status bar theming —
useStatusBarsyncs the iOS status bar text colour (white in dark mode, black in light mode) via@capacitor/status-bar;apple-mobile-web-app-status-bar-styleupdated toblack-translucentso the web view extends behind the status bar region. No-op on web. —src/hooks/useStatusBar.ts(new),src/App.tsx,index.html - iOS navigation header — desktop
SiteNavigationMenuis hidden on iOS builds and replaced withIosPageHeader: a sticky 17px SF-style title bar with safe-area-inset-top padding, back chevron, and right-side action slot.ios-buildclass added to<body>on iOS to prevent double-stacking of safe-area padding. —src/components/IosPageHeader.tsx(new),src/components/PageLayout.tsx,src/main.tsx,public/pwa.css - Keyboard avoidance —
@capacitor/keyboardconfigured withresize: bodyso the viewport shrinks above the keyboard.useKeyboardHeighthook tracks keyboard height and applies it aspaddingBottomonDrawerContentso form fields inside bottom sheets remain accessible.scroll-margin-bottom: 24pxadded for native scroll-into-view on input focus. —src/hooks/useKeyboardHeight.ts(new),capacitor.config.ts,src/components/ui/drawer.tsx,public/pwa.css - Long-press context menus —
useLongPressfires a 500 ms hold callback;TaskItemwraps cards in a RadixContextMenu(right-click on desktop, long-press on iOS) with Edit and Delete actions. Action buttons hidden on iOS builds where context menus serve as the primary affordance. —src/hooks/useLongPress.ts(new),src/components/TaskItem.tsx - Page transition animations — route changes in the iOS build play a subtle 280 ms slide-in from the right (
cubic-bezier(0.25, 0.46, 0.45, 0.94)), scoped to@supports (-webkit-touch-callout: none)so the animation never runs on web. —src/App.tsx(AnimatedRoutescomponent),public/pwa.css - New Capacitor plugins installed:
@capacitor/app,@capacitor/haptics,@capacitor/status-bar,@capacitor/keyboard(all v8.x, matching the existing core/ios versions).
- Bottom sheets —
-
Touch targets —
Buttonsize="sm"raised fromh-9(36 px) toh-10(40 px); mobile CSS now enforcesmin-height: 44pxon all non-hidden buttons at ≤768 px (previously commented out). —src/components/ui/button.tsx,public/pwa.css -
Rubber-band scroll bounce —
overscroll-behavior-yrestored toautoon#rootinside the iOS@supportsblock so the native bounce animation works again (wascontainglobally which suppressed it). vaul drawer elements gainoverscroll-behavior: contain+touch-action: pan-yto prevent scroll bleed through open sheets. —public/pwa.css -
Tasks navigation item added to desktop top nav and mobile bottom nav; mobile nav grid updated to support up to five items for authenticated users —
src/components/Navigation.tsx,src/components/MobileNav.tsx -
Contributing guidelines section in README with commit prefix table and approval requirements —
README.md(documentsmajor/feat/fix/patch/bump/maint/refactor/a11y/docsprefixes and which trigger releases) -
Persistent report summaries — generated summaries are auto-saved to localStorage keyed by week + tone (
ttp_report_{weekKey}_{tone}); a banner prompts users to restore a prior summary when returning to a week/tone combo they've already generated —src/hooks/useReportStorage.ts,src/pages/Report.tsx -
Markdown preview toggle in report output panel — Edit/Preview button pair lets users render the summary as formatted markdown before exporting —
src/components/SummaryOutput.tsx(uses existingMarkdownDisplay) -
Enhanced export actions — Download .txt (slugified filename) and Print/PDF button alongside existing Copy; print mode scopes output to summary region only —
src/components/SummaryOutput.tsx,public/print.css
- Refactored release CI workflow into three separate jobs:
detect,approve-major,release—.github/workflows/release.yml(bump type detection runs in its own job and passes output vianeeds; major releases require approval via themajor-releaseGitHub environment; changelog generation switched from raw git commits togh pr listto include PR titles and descriptions) - Added
majorbump prefix anda11ypatch prefix to release detection; removedchoreas a release trigger —.github/workflows/release.yml
-
Fixed iOS task data loss where tasks entered during a session were not present after closing the app —
src/contexts/TimeTrackingContext.tsx(React batchessetStatecalls asynchronously, sosaveImmediately()was reading a stalelatestStateRefthat did not yet include the newly added task; all mutation functions —startDay,startNewTask,endDay,updateTask,deleteTask— now compute state synchronously and calldataService.saveCurrentDay()directly with the correct snapshot;updateTaskanddeleteTaskwere not persisting at all previously; addedvisibilitychangelistener as a synchronouslocalStoragebackup for iOS app backgrounding sincebeforeunloaddoes not fire reliably in Capacitor/WKWebView; fixed the existingbeforeunloadbackup which was missing_v: SCHEMA_VERSION, causing it to be discarded on next load by the schema version check ingetCurrentDay()) -
Reverted Xcode project filename from
TimeTrackerPro.xcodeprojback toApp.xcodeproj—ios/App/App.xcodeproj/(Capacitor CLI hardcodesApp.xcodeproj; renaming it brokenpm run sync:ioswith ENOENT onproject.pbxproj; the Xcode target/product name remains "TimeTrackerPro") -
Fixed
open:iosnpm script pointing to the old renamed project path —package.json(updated fromTimeTrackerPro.xcodeproj/project.xcworkspacetoApp.xcodeproj/project.xcworkspace) -
Fixed
vite-plugin-pwanot disabling PWA during iOS builds due to wrong option name —vite.config.ts(changeddisabled: isIosBuild→disable: isIosBuild; plugin usesdisable, notdisabled, sosw.js,workbox-*.js, andmanifest.webmanifestwere being generated and synced into the iOS bundle on every build) -
Fixed outdated device capability declaration in iOS Info.plist —
ios/App/App/Info.plist(changedUIRequiredDeviceCapabilitiesfromarmv7toarm64; Apple dropped 32-bit support in iOS 11 and this stale Capacitor template value can fail App Store validation) -
Removed stale
-DCOCOAPODSSwift compiler flag from Xcode Debug build settings —ios/App/App.xcodeproj/project.pbxproj(project uses SPM, not CocoaPods; flag was a leftover from Capacitor's original CocoaPods template with no runtime effect)
- Capacitor iOS native app scaffolding (Phase 2)
—
capacitor.config.ts,ios/Xcode project,package.json(appIdcom.adamjolicoeur.timetrackerpro, iOS 15+ minimum via SPM,sync:iosscript combinesbuild:ios+cap sync ios;ios/App/App/publicgitignored and regenerated on every sync) - Renamed Xcode target and product from "App" to "TimeTrackerPro"
—
ios/App/App.xcodeproj/project.pbxproj(updated target name, productName, product path, and configuration list comments so Xcode shows "TimeTrackerPro"; project file itself remainsApp.xcodeprojper Capacitor's requirement) - Capacitor iOS integration prep (Phase 1)
—
src/App.tsx,src/components/Navigation.tsx,src/pages/Settings.tsx,vite.config.ts,.env.ios,index.html,package.json(BrowserRouter → HashRouter for filesystem loading;VITE_IOS_BUILDflag disables PWA SW and hides auth/sync UI in native builds; CSP updated withcapacitor://localhost;build:iosnpm script added) PageLayoutshared layout component for consistent page chrome —src/components/PageLayout.tsx,src/components/PageLayout.test.tsx(standardizes title + optional actions slot across all six pages; all page components migrated to use it)
- Carry over incomplete GFM checklist items as todo tasks when a day is archived
—
src/contexts/TimeTrackingContext.tsx,src/contexts/TimeTracking.test.tsx(unchecked- [ ]items from task descriptions are now extracted and appended as new todo tasks on archive; unique IDs and safe functional setState ensure no data loss on rollback)
- Added
aria-labelto all icon-only buttons whose visible text label is hidden on mobile viewports: Restore and Edit inArchiveItem, Restore/Delete/Edit inArchiveEditDialogheader, per-task Edit/Delete inArchiveEditDialogtask table, and Edit/Delete inProjectManagement - Replaced
focus:outline-nonewithfocus-visible:outline-none+focus-visible:ring-2 focus-visible:ring-ringon RadixTabsTriggerelements inArchiveItem— the browser focus ring was previously stripped for all input methods; it is now suppressed only for pointer clicks while remaining fully visible for keyboard navigation - Connected
Label/Textareapairs viahtmlFor/idinArchiveEditDialog(day notes field) andTaskEditInArchiveDialog(task description field) so screen readers announce the field label when the input receives focus
- Replaced hardcoded Tailwind gray color classes (
text-gray-900,text-gray-600/500/400) with theme variables (text-foreground,text-muted-foreground) acrossArchiveItem,ArchiveEditDialog,TaskEditInArchiveDialog,ProjectManagement, andExportDialog - Replaced hardcoded red color classes (
text-red-*,bg-red-*,border-red-*) withtext-destructive,bg-destructive/5, andborder-destructive/20in theArchiveEditDialogdelete confirmation card,ExportDialogimport error alert, andTaskEditInArchiveDialogrequired-field indicator; switched icon-only Delete buttons tovariant="destructive"inArchiveEditDialogandProjectManagement - Replaced
data-[state=active]:border-blue-600withdata-[state=active]:border-primaryon tab triggers inArchiveItem; replacedbg-gray-50 dark:bg-gray-800note/summary panels withbg-muted - Fixed inline style violations:
style={{ marginBottom: '1rem' }}→className="mb-4"inArchiveEditDialog;style={{ display: 'none' }}→className="hidden"inExportDialog
- Archive page summary stats (
src/pages/Archive.tsx): Wrapped the four summary-statreduce()calls (total hours, billable hours, non-billable hours, revenue) in a singleuseMemokeyed on[filteredDays, projects, categories]; previously they recomputed on every render regardless of whether the underlying data had changed. Also replaced unstable context method wrappers with stable module-level imports fromcalculationUtilsso the memo invalidates only when data actually changes. - Archive item row rendering (
src/components/ArchiveItem.tsx): Per-day stats (hoursWorked,billableHours,nonBillableHours,revenue) are now computed in auseMemo([day, projects, categories])instead of inline on every render; also eliminates the duplicategetRevenueForDay()call that previously appeared twice in the revenue badge. Project and category lookups in the task table now useMap.get()(O(1)) instead ofArray.find()(O(n)) per row viauseMemo-cached lookup maps. The daily-summarygenerateDailySummary()call is memoized onday.tasksso it only runs when task descriptions change.
- Native HTML5 time picker component (
TimePicker) following web standards and a11y best practices- Uses
<input type="time">for familiar, intuitive UX - 15-minute intervals: Time selection restricted to :00, :15, :30, and :45 using HTML5
stepattribute - Automatic native time pickers on mobile devices (iOS/Android)
- Keyboard-accessible time inputs on desktop
- Full ARIA label support for screen readers
- Styled with shadcn/ui design tokens for consistency
- Uses
- Improved time selection UX: Replaced custom scroll-wheel time picker with native HTML5 time inputs
- Follows standard web conventions for familiar, intuitive user experience
- Better desktop experience with keyboard-accessible inputs
- Mobile browsers provide native time pickers automatically
- Full accessibility (a11y) support with proper ARIA labels and keyboard navigation
- Consistent with existing date input pattern in the application
- Eliminates custom scroll logic in favor of browser-native functionality
- Start Day Dialog: 1 time picker for day start time
- Task Edit Dialog: 2 time pickers for task start/end times
- Archive Edit Dialog: 4 time pickers (2 for day start/end, 2 for task start/end)
- Removed duplicate
generateTimeOptions()helper functions from all dialog components
- Removed unsafe
as stringtype assertions onVITE_SUPABASE_URLandVITE_SUPABASE_ANON_KEYinsrc/lib/supabase.ts— the assertions hid thestring | undefinedtype from TypeScript, preventing the compiler from enforcing the existing null-guard; variables are now correctly typed so the?? ''fallback and early warn are properly enforced - Gated
getDbCallStats,resetDbCallStats, andclearDbCallLogwindow attachments behindimport.meta.env.DEVinsrc/lib/supabase.ts— internal database call telemetry was previously exposed to anyone with DevTools access in production; Vite now tree-shakes the attachment block out of production bundles - Added explicit radix
10toparseInt()call in CSV import (src/utils/exportUtils.ts) — implicit radix is deprecated and could silently misparse duration strings with leading zeros
- Deleted
src/hooks/.useReportSummary-Claude.ts— unused development dotfile that contained a direct Anthropic API key reference - Deleted
src/utils/supabase.ts— duplicate Supabase client that was never imported, creating a redundant connection - Deleted
src/hooks/useRealtimeSync.ts— hook whose body was fully disabled (return;/return () => {}) and was only referenced in a commented-out call - Deleted
src/hooks/useOffline.tsx— context accessor hook that was never called anywhere in the application - Removed
saveCurrentDayReffromTimeTrackingContext— auseRefthat was declared but never assigned - Removed
addToQueue,offlineQueue,processQueue, andSYNC_REQUIRED_EVENTfromOfflineContext— the offline action queue was implemented butaddToQueuewas never called, so the queue was permanently empty and the sync event was never dispatched;OfflineContextnow exposes onlyisOnlinewith online/offline toast notifications
- Fixed online reconnection never triggering a backend sync for authenticated users
—
src/contexts/TimeTrackingContext.tsx(added nativeonlinewindow event listener that callsforceSyncToDatabase()when authenticated; the previous offline-queue mechanism was broken because the queue was never populated, so reconnection silently dropped any pending changes) - Fixed
trackAuthCallstub recording no log details —src/lib/supabase.ts(the function incrementedauthCallCountbut created atimestampvariable it never used and pushed nothing todbCallLog, making auth call debugging impossible; now writes a full log entry matchingtrackDbCallsogetDbCallStats()shows auth calls alongside DB calls) - Fixed
getCurrentDay()crashing and silently discarding the stored day when thetasksfield is absent from a localStorage record —src/services/localStorageService.ts(data.tasks.map()threw a TypeError on a missing/null field; replaced with(data.tasks ?? []).map()so a corrupt or partial record produces an empty task list instead of swallowing the entire day) - Improved Weekly Report error messages to distinguish between distinct Gemini API failure modes
—
src/hooks/useReportSummary.ts(addedclassifyGeminiError()andclassifyFinishReason()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 directlocalStorage.getItem()call withuseTimeTracking().archivedDaysvia newdayRecordsToArchivedDays()adapter; both storage modes now work correctly) - Resolved merge conflicts in
src/index.css
- Daily time tracking with start/stop functionality
- Task management with real-time duration tracking
- Rich text support with GitHub Flavored Markdown
- Projects & clients organization with hourly rates
- Custom categories with color coding
- Archive system for completed work days
- Revenue tracking and automatic calculations
- Invoice generation and export (CSV, JSON)
- CSV import for existing time data
- Progressive Web App with offline support
- Cross-platform compatibility (Windows, Mac, Linux, iOS, Android)
- Dual storage mode (guest/local or authenticated/cloud sync)
- Print-friendly archive views
- Mobile-optimized interface with touch navigation
- Dark mode support
- Authentication via Supabase (optional)
- Cloud sync across devices via Supabase (when authenticated)
- Major (X.0.0): Breaking changes, major feature overhauls
- Minor (0.X.0): New features, significant improvements, non-breaking changes
- Patch (0.0.X): Bug fixes, minor improvements, documentation updates