+
+# ⏳ ZeroClock
+
+**Your time. Your data. Your rules.**
+
+A local-first time tracker for freelancers, artists, and anyone who believes the value of their labor belongs to them - not a cloud platform.
+
+
+
+
+
+
+
+
+
+
+
+
+*No subscriptions. No surveillance. No corporate middleman between you and your work.*
+
+
+
+---
+
+## What is ZeroClock?
+
+ZeroClock is a desktop time tracker that runs entirely on your machine. Every second you track, every invoice you generate, every report you pull - it all lives in a single SQLite database on your own hard drive. No accounts. No sign-ups. No data harvested. No monthly rent for the privilege of knowing how you spend your own time.
+
+It was built for the people who do the work: freelancers juggling five clients, illustrators billing by the hour, developers who want transparency in how their days are spent, and small collectives who refuse to feed their labor data into someone else's growth metrics.
+
+---
+
+## ๐ฏ Core features
+
+### ⏲ Timer
+
+A big, readable timer front and center. Start it, pause it, stop it. Switch projects mid-session without losing a second. The two-column layout keeps the timer always visible while you scroll through recent sessions.
+
+- **Start / Stop / Pause / Resume** with a single click or a global keyboard shortcut
+- **Quick-switch projects** mid-session without stopping the timer
+- **Idle detection** - ZeroClock notices when you step away and asks what to do with that time
+- **App tracking** - optionally detect which applications are running and associate time with projects
+- **Favorites** - save project presets as reorderable chips for one-click tracking
+- **Mini timer** - a tiny, always-on-top floating window so the timer is never out of sight
+
+### ๐ Entries
+
+Every tracked session becomes an entry you fully control.
+
+- **Filter and search** by project, client, date range, billable status, or tags
+- **Bulk operations** - select multiple entries to change projects, toggle billable, or delete
+- **Split entries** - break a long session into multiple entries when you forgot to switch
+- **Duplicate entries** - repeat yesterday's work with one click
+- **Bulk add** - create several entries at once for retroactive logging
+- **Entry templates** - save and reuse common entry patterns
+- **Pagination** with smooth loading for thousands of entries
+
+### ๐ฅ Clients and projects
+
+Organize your work however makes sense for you - not how a product manager decided you should.
+
+- **Clients** with name, email, company, and address
+- **Projects** with hourly rates, budgets, color coding, and task breakdowns
+- **Cascade awareness** - before deleting a client or project, see exactly what depends on it
+- **Archive projects** without losing data
+- **Group projects by client** or view them flat
+
+### ๐ Reports
+
+Understand where your time goes. Spot the clients who drain you. Find the projects that sustain you.
+
+- **Hours breakdown** - bar charts, tables, daily/weekly/monthly summaries
+- **Profitability analysis** - compare hours tracked against budgets, see who pays fairly
+- **Expense reports** - track costs alongside time for a complete picture
+- **Work patterns** - heatmaps and weekday distributions revealing your natural rhythms
+- **Export to CSV or PDF** for sharing with clients or keeping your own records
+
+### ๐งพ Invoicing
+
+Generate invoices from your tracked time without leaving the app.
+
+- **Multiple templates** - choose from several professional invoice designs
+- **Invoice pipeline** - track invoices through Draft, Sent, Paid, and Overdue stages
+- **Automatic line items** from tracked time entries
+- **Recurring invoices** - set up repeating invoices on a schedule
+- **Payment tracking** - record partial or full payments against invoices
+- **Overdue detection** with badge notifications
+- **PDF export** - download polished invoices ready to send
+- **Business identity** - your name, address, and logo on every invoice
+
+### ๐ฐ Expenses
+
+Time is not the only thing worth tracking.
+
+- **Log expenses** against projects with amounts, dates, and categories
+- **Receipt attachments** with a built-in lightbox viewer
+- **Link to invoices** - mark expenses as invoiced so nothing slips through
+- **Filter by date range** with quick presets (this week, last month, this quarter)
+
+---
+
+## ๐ Views
+
+### Calendar
+
+See your tracked time laid out in a familiar day / week / month calendar grid. Import `.ics` files from external calendars to see everything in one place. Click any slot to create an entry.
+
+### Timesheet
+
+A weekly grid for structured time entry, row by row. Lock completed weeks to prevent accidental edits. Copy last week's structure when your schedule repeats. Add rows for new project-task combinations and fill in hours per day.
+
+### Dashboard
+
+A bird's-eye view of your work: today's hours, weekly progress toward goals, recent entries with one-click replay, and a getting-started checklist for new users.
+
+---
+
+## ๐ง Customization
+
+ZeroClock adapts to you. Not the other way around.
+
+| Setting | Options |
+|---------|---------|
+| Theme | Dark, Light, or follow your OS |
+| Accent color | Amber, Blue, Green, Rose, Purple, Teal |
+| UI font | 15+ Google Fonts with live preview |
+| Timer font | 16 curated monospace fonts |
+| UI scale | 80% to 150% zoom |
+| Sound effects | Configurable events, volume, and synthesis mode |
+| Reduce motion | System, Always, or Never |
+| Dyslexia-friendly mode | OpenDyslexic font throughout the interface |
+
+### System tray
+
+- **Close to tray** - the window disappears but ZeroClock keeps running silently
+- **Minimize to tray** - minimize hides to the system tray instead of the taskbar
+- Left-click the tray icon to bring the window back
+
+### Keyboard shortcuts
+
+- **Global hotkeys** - toggle the timer or show the app from anywhere on your desktop
+- **In-app shortcuts** - navigate, start/stop, and manage entries without touching the mouse
+- **Customizable** - remap shortcuts to whatever feels natural
+
+---
+
+## ๐พ Data ownership
+
+This is the part that matters. Your data never leaves your machine.
+
+- **Single SQLite file** - your entire history in one portable database
+- **Auto-backup** - scheduled backups to a folder you choose, with configurable frequency and retention
+- **Manual export** - full JSON export of every table, every entry, every setting
+- **CSV export** - pull reports into a spreadsheet whenever you need to
+- **JSON import** - bring data in from other tools
+- **No cloud sync** - because "we got hacked" emails should not be part of your time-tracking experience
+- **No accounts** - nothing to delete because nothing was ever collected
+
+Your labor, your records, your hard drive.
+
+---
+
+## โฟ Accessibility
+
+ZeroClock is built to be usable by everyone. Not as an afterthought, but as a foundation.
+
+### WCAG 2.2 AAA compliance
+
+- **7:1 contrast ratios** on all text throughout the interface
+- **Focus indicators** visible on every interactive element
+- **Semantic HTML** with proper landmarks, headings, and ARIA roles
+- **Screen reader support** - live regions announce timer state changes, meaningful labels on every control
+- **Tooltips on every button** - hover or focus any icon button for a description of what it does, with proper `aria-describedby` linking and Escape to dismiss
+- **Keyboard navigation** - every feature is reachable without a mouse
+- **Reduce motion** - respect your OS preference or override it manually
+- **Dyslexia-friendly mode** - switch the entire interface to OpenDyslexic with one toggle
+- **UI scaling** - zoom the interface from 80% to 150% without breaking layouts
+
+Accessibility is not a feature. It is a baseline.
+
+---
+
+## ๐ Getting started
+
+### Prerequisites
+
+- [Node.js](https://nodejs.org/) 18+
+- [Rust](https://rustup.rs/) (latest stable)
+- Platform-specific Tauri dependencies - see the [Tauri prerequisites guide](https://v2.tauri.app/start/prerequisites/)
+
+### Build and run
+
+```bash
+# Clone the repository
+git clone https://github.com/your-username/zeroclock.git
+cd zeroclock
+
+# Install frontend dependencies
+npm install
+
+# Run in development mode
+npx tauri dev
+
+# Build for production
+npx tauri build
+```
+
+The database is created automatically on first launch in the same directory as the executable.
+
+---
+
+## ๐๏ธ Architecture
+
+```
+zeroclock/
+ src/ # Vue 3 frontend
+ components/ # Reusable UI components
+ composables/ # Shared composition functions
+ directives/ # Vue directives (tooltips, etc.)
+ stores/ # Pinia state management
+ utils/ # Helpers, formatters, audio
+ views/ # Page-level components
+ styles/ # Tailwind CSS and theme variables
+ src-tauri/ # Rust backend
+ src/
+ commands.rs # Tauri IPC command handlers
+ database.rs # SQLite schema and migrations
+ lib.rs # App setup, tray, plugins
+ seed.rs # Sample data generator
+```
+
+The frontend and backend communicate through Tauri's IPC bridge. The frontend never touches the filesystem directly - all data flows through typed Rust commands that validate, query, and persist to SQLite.
+
+---
+
+## ๐ค Contributing
+
+ZeroClock is built in the open. If you find it useful, you are welcome to help make it better.
+
+- **Report bugs** by opening an issue
+- **Suggest features** - especially if they help workers track and own their time more effectively
+- **Submit patches** - fork, branch, and open a pull request
+- **Improve accessibility** - if something does not work with your assistive technology, that is a bug
+
+Good-faith contributions from people who care about the work are always welcome.
+
+---
+
+## ๐ License
+
+ZeroClock is free software released under the [GNU General Public License v3.0](LICENSE). You can use it, modify it, and share it. The only condition: if you distribute a modified version, you share your changes under the same terms. The commons stay common.
+
+---
+
+
+
+*Built for the people who do the work.*
+
+*No venture capital. No growth metrics. No exit strategy.*
+
+*Just a tool that respects your time.*
+
+
diff --git a/docs/plans/2026-02-20-enhancement-round2-design.md b/docs/plans/2026-02-20-enhancement-round2-design.md
deleted file mode 100644
index 384df49..0000000
--- a/docs/plans/2026-02-20-enhancement-round2-design.md
+++ /dev/null
@@ -1,415 +0,0 @@
-# ZeroClock Enhancement Round 2 - Design Document
-
-## Overview
-
-15 enhancements to existing ZeroClock functionality, balanced across polish/reliability, power-user productivity, and data/insights. All features WCAG 2.2 AAA compliant from the start.
-
-## Global WCAG 2.2 AAA Requirements (apply to every feature)
-
-- 7:1 minimum contrast ratio for all text against backgrounds
-- All interactive elements have `focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-accent`
-- All dialogs/popovers use `useFocusTrap`, `role="dialog"` or `role="alertdialog"`, `aria-modal="true"`, `aria-labelledby`/`aria-describedby`
-- All dynamic content changes announced via `aria-live` regions
-- All animations use `motion-safe:` prefix or respect the global `prefers-reduced-motion` override
-- All information conveyed by color must also be conveyed by text, icon, or pattern (WCAG 1.4.1)
-- All drag operations must have keyboard alternatives (WCAG 2.5.7)
-- All timed interactions must be adjustable or disableable (WCAG 2.2.1)
-- Design tokens used throughout: bg-bg-base, bg-bg-surface, bg-bg-elevated, text-text-primary, text-text-secondary, text-text-tertiary, border-border-subtle, accent, accent-hover
-
----
-
-## Phase 1: Polish and Reliability
-
-### 1. Smart Timer Safety Net
-
-**Problem:** Stopping the timer without a project selected silently discards tracked time. Timers left running for 8+ hours are likely accidental.
-
-**Enhancement:**
-- When `timerStore.stop()` is called with no `selectedProjectId`, show a "Save Entry" dialog instead of discarding
-- Dialog contains: project picker (AppSelect), description field, the tracked duration (read-only), Save and Discard buttons
-- When stopping a timer with elapsed time > 8 hours, show a confirmation: "Timer has been running for X hours. Stop and save?"
-- Both dialogs use focus trap, are keyboard-operable, and announce via aria-live
-
-**Files:**
-- Modify: `src/stores/timer.ts` (stop method)
-- Create: `src/components/TimerSaveDialog.vue`
-- Modify: `src/App.vue` (mount the dialog)
-
-**Accessibility:**
-- `role="alertdialog"` with focus trap
-- `aria-live="assertive"` announcement when dialog opens: "Timer stopped. Select a project to save X hours of tracked time."
-- Duration display uses `aria-label` with full spoken format ("2 hours and 34 minutes")
-
----
-
-### 2. Toast Auto-Dismiss with Undo
-
-**Problem:** Toasts persist indefinitely until manually closed or pushed out by newer ones. No way to undo destructive actions.
-
-**Enhancement:**
-- Success toasts auto-dismiss after 4 seconds, error toasts after 8 seconds, info toasts after 6 seconds
-- WCAG 2.2.1 compliance: toast timers pause on hover and on keyboard focus. Add a setting in General tab to disable auto-dismiss entirely ("Persistent notifications" toggle)
-- Destructive operations (entry delete, expense delete, client delete, project delete) pass an `onUndo` callback to the toast store
-- Toast with undo shows an "Undo" button. Clicking it calls the callback within the timeout window
-- Undo button has `role="button"`, `aria-label="Undo delete"`, keyboard-accessible via Tab
-
-**Files:**
-- Modify: `src/stores/toast.ts` (add duration, onUndo, pause/resume logic)
-- Modify: `src/components/ToastNotification.vue` (auto-dismiss timer, undo button, hover/focus pause)
-- Modify: `src/views/Settings.vue` (add persistent notifications toggle in General tab)
-
-**Accessibility:**
-- Toast timer pauses on `:hover` and `:focus-within` (WCAG 2.2.1 Timing Adjustable)
-- Undo button is keyboard-focusable and announced: "Undo available. Press to reverse."
-- Screen reader announces remaining time: "Toast will dismiss in X seconds"
-- User can disable auto-dismiss entirely in Settings
-
----
-
-### 3. Unified Error Handling with Retry
-
-**Problem:** Error handling is inconsistent across stores - some throw, some return null, some only console.error. Users rarely see actionable feedback.
-
-**Enhancement:**
-- Create a shared `handleInvokeError(error, context)` utility that:
- - Shows a toast with the error context ("Failed to save entry")
- - For transient errors (connection, database busy), adds a "Retry" button to the toast
- - Logs the full error to console for debugging
-- Standardize all store catch blocks to use this utility
-- The retry callback re-executes the failed operation
-
-**Files:**
-- Create: `src/utils/errorHandler.ts`
-- Modify: all 13 stores to use the shared handler in catch blocks
-
-**Accessibility:**
-- Retry button in toast: `role="button"`, `aria-label="Retry failed operation"`, keyboard-accessible
-- Error toasts use `role="alert"` (already the case) for immediate screen reader announcement
-- After retry succeeds, announce "Operation completed successfully" via aria-live
-
----
-
-### 4. Onboarding Detection Resilience
-
-**Problem:** `detectCompletions()` in the onboarding store has all backend calls in a single try/catch - if `get_clients` fails, nothing else runs. No periodic re-check.
-
-**Enhancement:**
-- Wrap each detection call in its own try/catch so failures are independent
-- Add a 5-minute periodic re-check interval (started in App.vue alongside recurring entry checks)
-- Clear the interval in onUnmounted
-
-**Files:**
-- Modify: `src/stores/onboarding.ts` (individual try/catch per detection, no new UI)
-- Modify: `src/App.vue` (add periodic re-check interval)
-
-**Accessibility:**
-- No UI changes - this is purely backend resilience
-- When an item transitions from incomplete to complete, the existing checklist UI already handles aria updates
-
----
-
-### 5. Invoice Items Save Reliability
-
-**Problem:** `saveInvoiceItems` creates items one at a time in a loop - if one fails, the rest are silently lost. No transaction rollback.
-
-**Enhancement:**
-- Add a new Rust command `save_invoice_items_batch` that wraps all inserts in a single transaction (BEGIN/COMMIT/ROLLBACK)
-- Replace the sequential loop in `invoicesStore.saveInvoiceItems` with a single call to the batch command
-- On failure, show a clear error toast with retry option
-
-**Files:**
-- Modify: `src-tauri/src/commands.rs` (add `save_invoice_items_batch` command)
-- Modify: `src-tauri/src/lib.rs` (register command)
-- Modify: `src/stores/invoices.ts` (replace loop with batch call)
-
-**Accessibility:**
-- Error feedback via toast (already accessible)
-- No new UI elements
-
----
-
-## Phase 2: Power User Productivity
-
-### 6. Global Quick Entry
-
-**Problem:** Logging a manual time entry requires navigating to Entries view and filling out a full form. No way to quickly capture time from wherever you are.
-
-**Enhancement:**
-- Register a global keyboard shortcut (configurable, default Ctrl+Shift+N) via Tauri's global shortcut plugin
-- Opens a compact floating dialog (Teleported to body, zoom-aware) with:
- - Project picker (AppSelect)
- - Task picker (AppSelect, filtered by project)
- - Description text input
- - Date picker (defaults to today)
- - Duration input (supports H:MM and decimal formats)
- - Tag picker (AppTagInput)
- - Save and Cancel buttons
-- Pre-fills with last-used project from settingsStore
-- On save, creates entry via entriesStore.createEntry and shows success toast
-- Dialog closes on Escape or Cancel
-
-**Files:**
-- Create: `src/components/QuickEntryDialog.vue`
-- Modify: `src/App.vue` (mount dialog, register shortcut)
-- Modify: `src/views/Settings.vue` (add shortcut recorder for quick entry in Timer tab)
-
-**Accessibility:**
-- `role="dialog"` with `aria-modal="true"`, `aria-label="Quick time entry"`
-- Focus trap activated on open, first focusable element receives focus
-- `aria-live="polite"` announces "Quick entry dialog opened"
-- All form controls have visible labels (not just placeholders)
-- Escape closes and restores focus to previously focused element
-- `aria-keyshortcuts` attribute on the trigger describes the shortcut
-
----
-
-### 7. Entry Templates and Duplication
-
-**Problem:** Favorites only work for starting timers. No way to save and reuse manual entry configurations, and no quick duplicate for existing entries.
-
-**Enhancement:**
-- Add "Save as Template" option when editing an entry (saves project + task + description + tags + duration to a new `entry_templates` table)
-- Add "From Template" button in Entries view header that opens a picker dialog listing saved templates
-- Selecting a template pre-fills the create entry form
-- Add "Duplicate" button on each entry row in the entries table (creates a copy with today's date)
-- Templates manageable (delete) from a section in Settings > Timer tab
-
-**Files:**
-- Modify: `src-tauri/src/commands.rs` (add entry_templates CRUD commands + create table in database.rs)
-- Modify: `src-tauri/src/lib.rs` (register commands)
-- Create: `src/stores/entryTemplates.ts`
-- Create: `src/components/EntryTemplatePicker.vue`
-- Modify: `src/views/Entries.vue` (add From Template button, Duplicate button per row)
-- Modify: `src/views/Settings.vue` (template management section)
-
-**Accessibility:**
-- Template picker: `role="dialog"` with focus trap, `role="listbox"` for template list, `role="option"` per item
-- Duplicate button: `aria-label="Duplicate entry for [project] on [date]"`
-- "From Template" button: `aria-haspopup="dialog"`
-- Template list items keyboard-navigable (ArrowUp/Down, Enter to select)
-- Screen reader announces "Entry duplicated" or "Template applied" on action
-
----
-
-### 8. Timesheet Smart Row Persistence
-
-**Problem:** The timesheet view requires manually adding project/task rows each week. Starting a new week means rebuilding the same row configuration.
-
-**Enhancement:**
-- When opening a week with no timesheet data, auto-populate rows from the previous week's row structure (projects + tasks, but with zero hours)
-- Add a "Copy Last Week" button in the header that copies both structure and values
-- Save row configuration per week in a `timesheet_rows` table so manually added rows persist across navigation
-- "Copy Last Week" shows a confirmation dialog before overwriting any existing data
-
-**Files:**
-- Modify: `src-tauri/src/commands.rs` (add `get_timesheet_rows`, `save_timesheet_rows`, `get_previous_week_structure` commands)
-- Modify: `src-tauri/src/lib.rs` (register commands)
-- Modify: `src/views/TimesheetView.vue` (auto-populate logic, copy button, persist rows)
-
-**Accessibility:**
-- "Copy Last Week" button: `aria-label="Copy last week's timesheet structure"`
-- Confirmation dialog for overwrite: `role="alertdialog"` with focus trap
-- `aria-live="polite"` announces "Timesheet populated with X rows from last week"
-- Auto-populated rows announced: "Loaded X project rows from previous week"
-
----
-
-### 9. Client Cascade Awareness
-
-**Problem:** Deleting a client has no cascade check or warning, unlike projects which now show dependent counts via AppCascadeDeleteDialog.
-
-**Enhancement:**
-- Add `get_client_dependents` backend command that counts related projects, invoices, and expenses
-- Add transactional cascade delete to `delete_client` (delete expenses, invoice items, invoices, tracked_apps, entry_tags, time_entries, favorites, recurring_entries, timeline_events, tasks, projects, then client)
-- Wire AppCascadeDeleteDialog into Clients.vue delete flow (same pattern as Projects.vue)
-
-**Files:**
-- Modify: `src-tauri/src/commands.rs` (add `get_client_dependents`, update `delete_client` to cascade)
-- Modify: `src-tauri/src/lib.rs` (register command)
-- Modify: `src/views/Clients.vue` (import AppCascadeDeleteDialog, add dependency check before delete)
-
-**Accessibility:**
-- Reuses existing AppCascadeDeleteDialog which is already WCAG AAA compliant (focus trap, 3-second countdown, aria-live announcement, role="alertdialog")
-- No new accessibility work needed - the existing component handles everything
-
----
-
-### 10. Expenses Receipt Management
-
-**Problem:** Expenses store a `receipt_path` but there's no way to view receipts inline, and attachment is manual file path entry only.
-
-**Enhancement:**
-- Add inline receipt thumbnail on expense cards (small image preview if receipt_path exists)
-- Add a lightbox component for full-size receipt viewing (focus trap, Escape to close, zoom controls)
-- Add a file picker button for attaching receipts (uses Tauri's dialog plugin for native file picker - the keyboard-accessible alternative to drag-and-drop)
-- Add drag-and-drop zone on the expense edit form as an additional attachment method
-- Show a "No receipt" indicator (icon + text) on expenses without one
-- Keyboard alternative for drag-and-drop: the file picker button serves as the primary accessible method (WCAG 2.5.7 Dragging Movements)
-
-**Files:**
-- Create: `src/components/ReceiptLightbox.vue`
-- Modify: `src/views/Entries.vue` (expense tab: add thumbnail, lightbox trigger, file picker, drop zone)
-
-**Accessibility:**
-- Lightbox: `role="dialog"`, `aria-modal="true"`, `aria-label="Receipt image for [expense description]"`, focus trap, Escape closes
-- Image: `alt="Receipt for [category] expense on [date], [amount]"`
-- File picker button: `aria-label="Attach receipt file"` - this is the primary method, drag-and-drop is the enhancement (WCAG 2.5.7)
-- Drop zone: `role="button"`, `aria-label="Drop receipt file here or press Enter to browse"`, keyboard-activatable (Enter/Space opens file picker)
-- "No receipt" indicator uses both icon and text (not icon alone)
-- Lightbox zoom controls: keyboard-accessible (+/- buttons with aria-labels)
-
----
-
-## Phase 3: Data and Insights
-
-### 11. Dashboard Weekly Comparison
-
-**Problem:** Dashboard shows current stats but gives no sense of trend - no way to know if this week is better or worse than last week.
-
-**Enhancement:**
-- Add comparison indicators on each stat card: "+2.5h vs last week" or "-1.2h vs last week" with directional arrow icon
-- Add a sparkline row below the weekly chart showing the last 4 weeks of daily totals (7 bars each, muted styling)
-- Comparison data fetched via existing `get_reports` command with last week's date range
-
-**Files:**
-- Modify: `src/views/Dashboard.vue` (add comparison computeds, sparkline rendering, fetch last week data)
-
-**Accessibility:**
-- Comparison text is actual text, not just a colored arrow - e.g., "2.5 hours more than last week" or "1.2 hours less than last week"
-- Color conveys reinforcement only (green for increase, red for decrease) - the text and arrow direction carry the meaning (WCAG 1.4.1)
-- Arrow icon has `aria-hidden="true"` since the text already conveys direction
-- Sparkline bars have a `role="img"` wrapper with `aria-label="Last 4 weeks: [week1] hours, [week2] hours, [week3] hours, [week4] hours"`
-- Each individual sparkline bar does NOT need to be interactive - the summary label covers it
-- Comparison values use `sr-only` prefix: "Compared to last week:" for screen readers
-
----
-
-### 12. Project Health Dashboard Cards
-
-**Problem:** Budget progress bars on project cards are basic - no at-a-glance health assessment combining pace, burn rate, and risk level.
-
-**Enhancement:**
-- Add a health badge per project card combining budget burn rate and pace data from `get_project_budget_status`
-- Three health states: "On Track" (green icon + text), "At Risk" (yellow icon + text), "Over Budget" (red icon + text)
-- Health determined by: budget percentage > 100% = over budget, pace = "behind" and > 75% budget used = at risk, else on track
-- Add an "Attention Needed" section at the top of Projects view listing at-risk and over-budget projects
-- Each state uses BOTH a distinct icon (checkmark, warning triangle, x-circle) AND text label - never color alone
-
-**Files:**
-- Modify: `src/views/Projects.vue` (add health badge computation, attention section)
-
-**Accessibility:**
-- Health badge: icon + text label, never color alone (WCAG 1.4.1 Use of Color)
-- Icons: CheckCircle for on-track, AlertTriangle for at-risk, XCircle for over-budget - each with `aria-hidden="true"` since text label is present
-- Badge has `role="status"` so screen readers announce it
-- Attention section: `role="region"` with `aria-label="Projects needing attention"`, `aria-live="polite"` so it updates when data changes
-- Each attention item is a focusable link/button that navigates to the project
-- Text labels: "On Track", "At Risk", "Over Budget" - clear, unambiguous, no jargon
-
----
-
-### 13. Reports Time-of-Day Heatmap
-
-**Problem:** Reports view has no visibility into when work happens - no pattern analysis of productive hours or workday distribution.
-
-**Enhancement:**
-- Add a "Patterns" tab to Reports view
-- Primary visualization: a 7x24 grid (days of week x hours of day) showing work intensity
-- Each cell uses both color intensity AND a numeric label showing hours (e.g., "2.5h") - not color alone (WCAG 1.4.1)
-- Cells with zero hours are empty (no false visual noise)
-- Below the grid, add a "Data Table" toggle that switches to a fully accessible HTML table view of the same data (accessible alternative to the visual grid)
-- Data computed from entry start_time timestamps within the selected date range
-
-**Files:**
-- Modify: `src/views/Reports.vue` (add Patterns tab, heatmap grid, data table alternative)
-
-**Accessibility:**
-- Grid uses `role="grid"` with `role="row"` and `role="gridcell"` for keyboard navigation
-- Each cell has `aria-label="[Day] [Hour]: [X] hours"` (e.g., "Monday 9 AM: 2.5 hours")
-- Arrow key navigation between cells (up/down/left/right)
-- Color intensity reinforces the numeric values but is NOT the sole information channel - every cell shows its numeric value as text
-- "Data Table" toggle button: `aria-pressed` toggle, switches between visual grid and `
` with `
` and `
`
-- Table alternative is a standard HTML table with proper headers, fully navigable by screen readers
-- Summary stats below the grid as text: "Most productive: Monday 9-10 AM (avg 1.8h)", "Quietest: Sunday (avg 0.2h)"
-- `motion-safe:` on any cell hover/focus transitions
-
----
-
-### 14. Rounding Visibility and Preview
-
-**Problem:** Time rounding is configured in Settings but its effect is invisible - users can't see how much time is being rounded.
-
-**Enhancement:**
-- On entry rows in Entries view: show a small "rounded" indicator next to duration when rounding changes the value, with tooltip showing "Actual: 1h 23m, Rounded: 1h 30m"
-- On invoice line items: show both actual and rounded hours when rounding is active
-- In Reports view Hours tab: add a "Rounding Impact" summary line showing total time added/subtracted by rounding across all entries in the date range
-- Rounding calculations use the existing `roundDuration()` utility from `src/utils/rounding.ts`
-
-**Files:**
-- Modify: `src/views/Entries.vue` (add rounded indicator on entry rows)
-- Modify: `src/views/Invoices.vue` (show actual vs rounded on line items)
-- Modify: `src/views/Reports.vue` (add rounding impact summary)
-
-**Accessibility:**
-- Rounded indicator: not just a colored dot - shows text "Rounded" with a clock icon, `aria-label="Duration rounded from [actual] to [rounded]"`
-- Tooltip: accessible via keyboard focus (not hover-only), uses `role="tooltip"` with `aria-describedby`
-- Rounding impact summary: plain text, 7:1 contrast, e.g., "Rounding added 2h 15m across 47 entries"
-- Actual vs. rounded on invoices: both values visible as text, difference highlighted with both color and "+Xm" / "-Xm" text label
-- All diff indicators use 7:1 contrast ratios against their backgrounds
-
----
-
-### 15. Export Completeness and Scheduling
-
-**Problem:** `export_data` only exports clients, projects, entries, and invoices - missing tags, expenses, favorites, recurring entries, tracked apps, timeline events, calendar sources, and settings. No automated backup.
-
-**Enhancement:**
-- Expand `export_data` to include ALL data: tags, entry_tags, expenses, favorites, recurring_entries, tracked_apps, timeline_events, calendar_sources, calendar_events, timesheet_locks, and settings
-- Update `import_json_data` to handle the expanded format (backward compatible with old exports)
-- Add a "Last exported" timestamp display in Settings > Data tab
-- Add an "Auto-backup on close" toggle in Settings > Data tab that saves a backup JSON to a user-configured directory when the app closes
-- Add a "Backup path" file picker (Tauri dialog) for choosing the auto-backup directory
-- Backup files named with timestamp: `zeroclock-backup-YYYY-MM-DD.json`
-- WCAG 2.2.1: the auto-backup happens on app close (user-initiated), not on a timer, so no timing concerns
-
-**Files:**
-- Modify: `src-tauri/src/commands.rs` (expand `export_data`, update `import_json_data`, add `auto_backup` command)
-- Modify: `src-tauri/src/lib.rs` (register commands)
-- Modify: `src/views/Settings.vue` (add last-exported display, auto-backup toggle, path picker)
-- Modify: `src/App.vue` (hook into Tauri window close event for auto-backup)
-
-**Accessibility:**
-- File picker button: `aria-label="Choose backup directory"`
-- Last exported display: `role="status"`, readable text "Last exported: February 20, 2026 at 3:45 PM" (not relative time like "2 hours ago")
-- Auto-backup toggle: standard toggle with label, `aria-checked`
-- Backup path shown as text with a "Change" button, not an editable text field
-- All controls have visible labels and 7:1 contrast
-
----
-
-## Implementation Phases
-
-### Phase 1: Polish and Reliability (Features 1-5)
-Mostly backend and store changes, minimal new UI. Low risk, high impact on data integrity.
-
-### Phase 2: Power User Productivity (Features 6-10)
-Mix of new components and view modifications. Medium complexity, high impact on daily workflow.
-
-### Phase 3: Data and Insights (Features 11-15)
-Primarily view-level changes with some backend expansion. Medium complexity, high impact on decision-making.
-
-## Verification Criteria
-
-Each feature must pass:
-1. `npx vue-tsc --noEmit` - TypeScript type check
-2. `npx vite build` - Production build
-3. `cargo build` - Rust backend build (if backend changed)
-4. Manual WCAG AAA spot check:
- - All interactive elements keyboard-operable
- - All dialogs trap focus and close with Escape
- - All dynamic content announced via aria-live
- - All form controls have visible labels
- - No information conveyed by color alone
- - 7:1 contrast on all text
- - Drag operations have keyboard alternatives
- - Timed interactions are adjustable
diff --git a/docs/plans/2026-02-20-enhancement-round2-plan.md b/docs/plans/2026-02-20-enhancement-round2-plan.md
deleted file mode 100644
index f876545..0000000
--- a/docs/plans/2026-02-20-enhancement-round2-plan.md
+++ /dev/null
@@ -1,1744 +0,0 @@
-# Enhancement Round 2 Implementation Plan
-
-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Implement 15 feature enhancements across polish/reliability, power-user productivity, and data/insights - all WCAG 2.2 AAA compliant.
-
-**Architecture:** Vue 3 Composition API with Pinia stores, Tauri v2 Rust backend with SQLite (rusqlite), Tailwind CSS v4 design tokens. No test framework - verification via `npx vue-tsc --noEmit`, `npx vite build`, and `cargo build`.
-
-**Tech Stack:** Vue 3, TypeScript, Pinia, Tauri v2, Rust, SQLite, Tailwind CSS v4, Chart.js, lucide-vue-next icons.
-
----
-
-## Context for the Implementer
-
-### Design tokens
-
-All UI uses semantic tokens: `bg-bg-base`, `bg-bg-surface`, `bg-bg-elevated`, `bg-bg-inset`, `text-text-primary`, `text-text-secondary`, `text-text-tertiary`, `border-border-subtle`, `border-border-visible`, `text-accent-text`, `bg-accent`, `bg-accent-hover`, `bg-accent-muted`, `text-status-running`, `text-status-error`, `text-status-warning`.
-
-### Typography pattern
-
-Labels: `text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em]`
-Section titles: `text-[0.8125rem] text-text-primary`
-Descriptions: `text-[0.6875rem] text-text-tertiary mt-0.5`
-Headings: `text-[1.25rem] font-semibold font-[family-name:var(--font-heading)] text-text-primary`
-
-### Settings pattern
-
-All settings stored as key-value strings via `settingsStore.updateSetting(key, value)`. Access via `settingsStore.settings.key_name`.
-
-### Focus visible pattern
-
-All interactive elements: `focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent`
-
-### Toggle switch pattern (used in Settings.vue)
-
-```html
-
-```
-
-### Dialog pattern
-
-Overlay: `fixed inset-0 bg-black/70 backdrop-blur-[4px] flex items-center justify-center p-4 z-50`
-Dialog body: `bg-bg-surface border border-border-subtle rounded-lg shadow-[0_1px_3px_rgba(0,0,0,0.3)] w-full max-w-xl p-6`
-Attributes: `role="dialog" aria-modal="true" aria-labelledby="dialog-title-id"`
-Import `useFocusTrap` from `../utils/focusTrap` - call `activate(el, { onDeactivate })` and `deactivate()`.
-
-### Announcer pattern
-
-```ts
-import { useAnnouncer } from '../composables/useAnnouncer'
-const { announce } = useAnnouncer()
-announce('Message for screen reader')
-```
-
-### Backend command registration
-
-Add commands to `src-tauri/src/lib.rs` in the `generate_handler!` macro (lines 43-133). Add new function in `src-tauri/src/commands.rs`. Database migrations in `src-tauri/src/database.rs`.
-
-### Verification commands
-
-After every task: `npx vue-tsc --noEmit && npx vite build`. After Rust changes: `cd src-tauri && cargo build`.
-
----
-
-## Phase 1: Polish and Reliability (Features 1-5)
-
----
-
-### Task 1: Toast auto-dismiss with undo - store changes
-
-**Files:**
-- Modify: `src/stores/toast.ts`
-
-**Step 1:** Read `src/stores/toast.ts` (42 lines).
-
-**Step 2:** Replace the entire file with the enhanced version. Changes:
-- Add `duration`, `onUndo`, `paused`, and `timerId` fields to `Toast` interface
-- Add auto-dismiss logic with configurable timeouts per type
-- Add `pauseToast(id)` and `resumeToast(id)` functions
-- Read persistent_notifications setting from settingsStore
-
-```ts
-import { defineStore } from 'pinia'
-import { ref } from 'vue'
-
-export interface Toast {
- id: number
- message: string
- type: 'success' | 'error' | 'info'
- exiting?: boolean
- duration: number
- onUndo?: () => void
- paused?: boolean
- timerId?: number
-}
-
-const DURATIONS: Record = {
- success: 4000,
- error: 8000,
- info: 6000,
-}
-
-export const useToastStore = defineStore('toast', () => {
- const toasts = ref([])
- let nextId = 0
- let persistentNotifications = false
-
- function setPersistentNotifications(val: boolean) {
- persistentNotifications = val
- }
-
- function addToast(message: string, type: Toast['type'] = 'info', options?: { onUndo?: () => void; duration?: number }) {
- const id = nextId++
- const duration = options?.duration ?? DURATIONS[type]
- const toast: Toast = { id, message, type, duration, onUndo: options?.onUndo }
- toasts.value.push(toast)
-
- if (!persistentNotifications) {
- startDismissTimer(toast)
- }
-
- // Cap at 5, oldest gets exiting state
- if (toasts.value.length > 5) {
- const oldest = toasts.value.find(t => !t.exiting)
- if (oldest) removeToast(oldest.id)
- }
- }
-
- function startDismissTimer(toast: Toast) {
- if (toast.timerId) clearTimeout(toast.timerId)
- toast.timerId = window.setTimeout(() => {
- removeToast(toast.id)
- }, toast.duration)
- }
-
- function pauseToast(id: number) {
- const toast = toasts.value.find(t => t.id === id)
- if (toast && toast.timerId) {
- clearTimeout(toast.timerId)
- toast.timerId = undefined
- toast.paused = true
- }
- }
-
- function resumeToast(id: number) {
- const toast = toasts.value.find(t => t.id === id)
- if (toast && toast.paused && !persistentNotifications) {
- toast.paused = false
- startDismissTimer(toast)
- }
- }
-
- function undoToast(id: number) {
- const toast = toasts.value.find(t => t.id === id)
- if (toast?.onUndo) {
- toast.onUndo()
- removeToast(id)
- }
- }
-
- function removeToast(id: number) {
- const toast = toasts.value.find(t => t.id === id)
- if (toast) {
- if (toast.timerId) clearTimeout(toast.timerId)
- toast.exiting = true
- setTimeout(() => {
- toasts.value = toasts.value.filter(t => t.id !== id)
- }, 150)
- }
- }
-
- function success(message: string, options?: { onUndo?: () => void }) { addToast(message, 'success', options) }
- function error(message: string, options?: { onUndo?: () => void }) { addToast(message, 'error', options) }
- function info(message: string, options?: { onUndo?: () => void }) { addToast(message, 'info', options) }
-
- return { toasts, addToast, removeToast, pauseToast, resumeToast, undoToast, success, error, info, setPersistentNotifications }
-})
-```
-
-**Step 3:** Verify: `npx vue-tsc --noEmit`
-
-**Step 4:** Commit: `git add src/stores/toast.ts && git commit -m "feat: toast auto-dismiss with undo and pause support"`
-
----
-
-### Task 2: Toast component - auto-dismiss UI, undo button, hover/focus pause
-
-**Files:**
-- Modify: `src/components/ToastNotification.vue`
-
-**Step 1:** Read `src/components/ToastNotification.vue` (46 lines).
-
-**Step 2:** Replace with enhanced version that adds:
-- `@mouseenter` / `@mouseleave` for hover pause/resume
-- `@focusin` / `@focusout` for keyboard focus pause/resume
-- Undo button when `toast.onUndo` exists
-- Progress bar showing remaining time (visual only, not critical info)
-
-```vue
-
-
-
-
-
- No project was selected. Choose a project to save your tracked time.
-
-
- The timer has been running for a long time. Would you like to stop and save?
-
-
-
-
- Duration
-
- {{ formattedDuration }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-**Step 2:** Verify: `npx vue-tsc --noEmit`
-
-**Step 3:** Commit: `git add src/components/TimerSaveDialog.vue && git commit -m "feat: timer save dialog for no-project and long-timer scenarios"`
-
----
-
-### Task 11: Smart timer safety net - integration
-
-**Files:**
-- Modify: `src/stores/timer.ts` (stop method, lines 382-447)
-- Modify: `src/App.vue` (mount dialog)
-
-**Step 1:** Read `src/stores/timer.ts` lines 380-450.
-
-**Step 2:** In `timer.ts`, modify the `stop` function to emit an event instead of silently discarding when no project is selected:
-- Add a new ref: `showSaveDialog = ref(false)` and `saveDialogMode = ref<'no-project' | 'long-timer'>('no-project')`
-- Add `pendingStopDuration = ref(0)` to hold the duration for the dialog
-- Before the `if (selectedProjectId.value)` block (line 408), add the safety net check:
-
-```ts
-// Safety net: if no project and there's tracked time, show save dialog
-if (!selectedProjectId.value && duration > 0) {
- pendingStopDuration.value = duration
- saveDialogMode.value = 'no-project'
- showSaveDialog.value = true
- // Don't reset state yet - dialog will handle it
- return
-}
-
-// Safety net: long timer confirmation (8+ hours)
-const LONG_TIMER_THRESHOLD = 8 * 3600
-if (duration > LONG_TIMER_THRESHOLD && !options?.confirmed) {
- pendingStopDuration.value = duration
- saveDialogMode.value = 'long-timer'
- showSaveDialog.value = true
- return
-}
-```
-
-Add to the stop function signature: `options?: { subtractIdleTime?: boolean; confirmed?: boolean }`
-
-Add new functions:
-
-```ts
-async function handleSaveDialogSave(projectId: number, desc: string) {
- showSaveDialog.value = false
- const oldProjectId = selectedProjectId.value
- const oldDescription = description.value
- selectedProjectId.value = projectId
- description.value = desc
- await stop({ confirmed: true })
- selectedProjectId.value = oldProjectId
- description.value = oldDescription
-}
-
-function handleSaveDialogDiscard() {
- showSaveDialog.value = false
- // Reset without saving
- stopDisplayInterval()
- stopMonitorInterval()
- timerState.value = 'STOPPED'
- startTime.value = null
- pausedAt.value = null
- totalPausedMs.value = 0
- idleStartedAt.value = null
- elapsedSeconds.value = 0
- showIdlePrompt.value = false
- showAppPrompt.value = false
- emitTimerSync()
- announce('Timer stopped - entry discarded')
- audioEngine.play('timer_stop')
- const settingsStore = useSettingsStore()
- settingsStore.updateSetting('timer_state_backup', '')
-}
-
-function handleSaveDialogCancel() {
- showSaveDialog.value = false
- // Timer keeps running
-}
-```
-
-Export the new refs and functions.
-
-**Step 3:** In `src/App.vue`, add the TimerSaveDialog mount after the RecurringPromptDialog:
-
-```html
-
-```
-
-Add import: `import TimerSaveDialog from './components/TimerSaveDialog.vue'`
-Add: `const timerStore = useTimerStore()` (may already be available - check line 100).
-
-**Step 4:** Verify: `npx vue-tsc --noEmit && npx vite build`
-
-**Step 5:** Commit: `git add src/stores/timer.ts src/App.vue && git commit -m "feat: smart timer safety net - save dialog on stop without project"`
-
----
-
-### Task 12: Phase 1 verification
-
-**Step 1:** Run: `npx vue-tsc --noEmit`
-**Step 2:** Run: `npx vite build`
-**Step 3:** Run: `cd src-tauri && cargo build`
-**Step 4:** Commit if any fixes needed.
-
----
-
-## Phase 2: Power User Productivity (Features 6-10)
-
----
-
-### Task 13: Client cascade awareness - backend
-
-**Files:**
-- Modify: `src-tauri/src/commands.rs` (add `get_client_dependents`, update `delete_client`)
-- Modify: `src-tauri/src/lib.rs` (register command)
-
-**Step 1:** Read `src-tauri/src/commands.rs` lines 110-120 (existing `delete_client`).
-
-**Step 2:** Add `get_client_dependents` command:
-
-```rust
-#[tauri::command]
-pub fn get_client_dependents(state: State, client_id: i64) -> Result {
- let conn = state.db.lock().map_err(|e| e.to_string())?;
- let project_count: i64 = conn.query_row(
- "SELECT COUNT(*) FROM projects WHERE client_id = ?1", params![client_id], |row| row.get(0)
- ).map_err(|e| e.to_string())?;
- let invoice_count: i64 = conn.query_row(
- "SELECT COUNT(*) FROM invoices WHERE client_id = ?1", params![client_id], |row| row.get(0)
- ).map_err(|e| e.to_string())?;
- let expense_count: i64 = conn.query_row(
- "SELECT COUNT(*) FROM expenses WHERE client_id = ?1", params![client_id], |row| row.get(0)
- ).map_err(|e| e.to_string())?;
- Ok(serde_json::json!({
- "projects": project_count,
- "invoices": invoice_count,
- "expenses": expense_count
- }))
-}
-```
-
-**Step 3:** Update `delete_client` to cascade inside a transaction:
-
-```rust
-#[tauri::command]
-pub fn delete_client(state: State, id: i64) -> Result<(), String> {
- let conn = state.db.lock().map_err(|e| e.to_string())?;
- conn.execute("BEGIN TRANSACTION", []).map_err(|e| e.to_string())?;
-
- // Get all project IDs for this client
- let project_ids: Vec = {
- let mut stmt = conn.prepare("SELECT id FROM projects WHERE client_id = ?1").map_err(|e| e.to_string())?;
- let rows = stmt.query_map(params![id], |row| row.get(0)).map_err(|e| e.to_string())?;
- rows.filter_map(|r| r.ok()).collect()
- };
-
- for pid in &project_ids {
- // Delete dependent data for each project
- let _ = conn.execute("DELETE FROM entry_tags WHERE entry_id IN (SELECT id FROM time_entries WHERE project_id = ?1)", params![pid]);
- let _ = conn.execute("DELETE FROM time_entries WHERE project_id = ?1", params![pid]);
- let _ = conn.execute("DELETE FROM tasks WHERE project_id = ?1", params![pid]);
- let _ = conn.execute("DELETE FROM tracked_apps WHERE project_id = ?1", params![pid]);
- let _ = conn.execute("DELETE FROM favorites WHERE project_id = ?1", params![pid]);
- let _ = conn.execute("DELETE FROM recurring_entries WHERE project_id = ?1", params![pid]);
- let _ = conn.execute("DELETE FROM timeline_events WHERE project_id = ?1", params![pid]);
- }
-
- let _ = conn.execute("DELETE FROM expenses WHERE client_id = ?1", params![id]);
- // Delete invoice items for this client's invoices
- let _ = conn.execute("DELETE FROM invoice_items WHERE invoice_id IN (SELECT id FROM invoices WHERE client_id = ?1)", params![id]);
- let _ = conn.execute("DELETE FROM invoices WHERE client_id = ?1", params![id]);
- let _ = conn.execute("DELETE FROM projects WHERE client_id = ?1", params![id]);
-
- match conn.execute("DELETE FROM clients WHERE id = ?1", params![id]) {
- Ok(_) => {
- conn.execute("COMMIT", []).map_err(|e| e.to_string())?;
- Ok(())
- }
- Err(e) => {
- let _ = conn.execute("ROLLBACK", []);
- Err(e.to_string())
- }
- }
-}
-```
-
-**Step 4:** Register `get_client_dependents` in `lib.rs` near other client commands.
-
-**Step 5:** Verify: `cd src-tauri && cargo build`
-
-**Step 6:** Commit: `git add src-tauri/src/commands.rs src-tauri/src/lib.rs && git commit -m "feat: client cascade delete with dependency counts"`
-
----
-
-### Task 14: Client cascade awareness - frontend
-
-**Files:**
-- Modify: `src/views/Clients.vue` (wire AppCascadeDeleteDialog)
-
-**Step 1:** Read `src/views/Clients.vue` fully. Find the existing `confirmDelete` function and the delete dialog/flow.
-
-**Step 2:** Import `AppCascadeDeleteDialog` and wire it in. Follow the same pattern as Projects.vue. Add:
-- A `deleteCandidate` ref for the client being deleted
-- A `deleteImpacts` ref for the dependency counts
-- Call `invoke('get_client_dependents', { clientId })` before showing the dialog
-- Wire `AppCascadeDeleteDialog` with the impacts
-
-```ts
-import AppCascadeDeleteDialog from '../components/AppCascadeDeleteDialog.vue'
-
-const deleteCandidate = ref(null)
-const deleteImpacts = ref>([])
-const showCascadeDelete = ref(false)
-
-async function confirmDelete(client: any) {
- try {
- const deps = await invoke<{ projects: number; invoices: number; expenses: number }>('get_client_dependents', { clientId: client.id })
- const impacts: Array<{ label: string; count: number }> = []
- if (deps.projects > 0) impacts.push({ label: 'Projects', count: deps.projects })
- if (deps.invoices > 0) impacts.push({ label: 'Invoices', count: deps.invoices })
- if (deps.expenses > 0) impacts.push({ label: 'Expenses', count: deps.expenses })
- deleteCandidate.value = client
- deleteImpacts.value = impacts
- showCascadeDelete.value = true
- } catch (error) {
- handleInvokeError(error, 'Failed to check client dependencies')
- }
-}
-
-async function executeDelete() {
- if (!deleteCandidate.value) return
- try {
- await invoke('delete_client', { id: deleteCandidate.value.id })
- // Remove from local state
- // ... (depends on existing client store pattern)
- toastStore.success('Client deleted')
- } catch (error) {
- handleInvokeError(error, 'Failed to delete client')
- } finally {
- showCascadeDelete.value = false
- deleteCandidate.value = null
- }
-}
-```
-
-Add in template:
-```html
-
-```
-
-**Step 3:** Verify: `npx vue-tsc --noEmit && npx vite build`
-
-**Step 4:** Commit: `git add src/views/Clients.vue && git commit -m "feat: cascade delete dialog for clients with dependency counts"`
-
----
-
-### Task 15: Entry templates - backend
-
-**Files:**
-- Modify: `src-tauri/src/database.rs` (add `entry_templates` table)
-- Modify: `src-tauri/src/commands.rs` (add CRUD commands)
-- Modify: `src-tauri/src/lib.rs` (register commands)
-
-**Step 1:** Read `src-tauri/src/database.rs` to find where to add the migration.
-
-**Step 2:** Add table creation after the last CREATE TABLE (before settings seed):
-
-```rust
-conn.execute(
- "CREATE TABLE IF NOT EXISTS entry_templates (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL,
- project_id INTEGER NOT NULL REFERENCES projects(id),
- task_id INTEGER REFERENCES tasks(id),
- description TEXT,
- duration INTEGER NOT NULL DEFAULT 0,
- billable INTEGER NOT NULL DEFAULT 1,
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
- )",
- [],
-)?;
-```
-
-**Step 3:** Add CRUD commands in `commands.rs`:
-
-```rust
-#[tauri::command]
-pub fn get_entry_templates(state: State) -> Result, String> {
- let conn = state.db.lock().map_err(|e| e.to_string())?;
- let mut stmt = conn.prepare("SELECT id, name, project_id, task_id, description, duration, billable, created_at FROM entry_templates ORDER BY name").map_err(|e| e.to_string())?;
- let rows = stmt.query_map([], |row| {
- Ok(serde_json::json!({
- "id": row.get::<_, i64>(0)?,
- "name": row.get::<_, String>(1)?,
- "project_id": row.get::<_, i64>(2)?,
- "task_id": row.get::<_, Option>(3)?,
- "description": row.get::<_, Option>(4)?,
- "duration": row.get::<_, i64>(5)?,
- "billable": row.get::<_, i64>(6)?,
- "created_at": row.get::<_, String>(7)?,
- }))
- }).map_err(|e| e.to_string())?;
- Ok(rows.filter_map(|r| r.ok()).collect())
-}
-
-#[tauri::command]
-pub fn create_entry_template(state: State, template: serde_json::Value) -> Result {
- let conn = state.db.lock().map_err(|e| e.to_string())?;
- let name = template.get("name").and_then(|v| v.as_str()).unwrap_or("Untitled");
- let project_id = template.get("project_id").and_then(|v| v.as_i64()).ok_or("project_id required")?;
- let task_id = template.get("task_id").and_then(|v| v.as_i64());
- let description = template.get("description").and_then(|v| v.as_str());
- let duration = template.get("duration").and_then(|v| v.as_i64()).unwrap_or(0);
- let billable = template.get("billable").and_then(|v| v.as_i64()).unwrap_or(1);
- conn.execute(
- "INSERT INTO entry_templates (name, project_id, task_id, description, duration, billable) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
- params![name, project_id, task_id, description, duration, billable],
- ).map_err(|e| e.to_string())?;
- Ok(conn.last_insert_rowid())
-}
-
-#[tauri::command]
-pub fn delete_entry_template(state: State, id: i64) -> Result<(), String> {
- let conn = state.db.lock().map_err(|e| e.to_string())?;
- conn.execute("DELETE FROM entry_templates WHERE id = ?1", params![id]).map_err(|e| e.to_string())?;
- Ok(())
-}
-```
-
-**Step 4:** Register all three commands in `lib.rs`.
-
-**Step 5:** Verify: `cd src-tauri && cargo build`
-
-**Step 6:** Commit: `git add src-tauri/ && git commit -m "feat: entry templates CRUD backend"`
-
----
-
-### Task 16: Entry templates - store
-
-**Files:**
-- Create: `src/stores/entryTemplates.ts`
-
-**Step 1:** Create the store:
-
-```ts
-import { defineStore } from 'pinia'
-import { ref } from 'vue'
-import { invoke } from '@tauri-apps/api/core'
-import { handleInvokeError } from '../utils/errorHandler'
-
-export interface EntryTemplate {
- id?: number
- name: string
- project_id: number
- task_id?: number
- description?: string
- duration: number
- billable: number
- created_at?: string
-}
-
-export const useEntryTemplatesStore = defineStore('entryTemplates', () => {
- const templates = ref([])
- const loading = ref(false)
-
- async function fetchTemplates() {
- loading.value = true
- try {
- templates.value = await invoke('get_entry_templates')
- } catch (error) {
- handleInvokeError(error, 'Failed to fetch entry templates')
- } finally {
- loading.value = false
- }
- }
-
- async function createTemplate(template: Omit): Promise {
- try {
- const id = await invoke('create_entry_template', { template })
- templates.value.push({ ...template, id: Number(id) })
- return Number(id)
- } catch (error) {
- handleInvokeError(error, 'Failed to create template')
- return null
- }
- }
-
- async function deleteTemplate(id: number) {
- try {
- await invoke('delete_entry_template', { id })
- templates.value = templates.value.filter(t => t.id !== id)
- } catch (error) {
- handleInvokeError(error, 'Failed to delete template')
- }
- }
-
- return { templates, loading, fetchTemplates, createTemplate, deleteTemplate }
-})
-```
-
-**Step 2:** Verify: `npx vue-tsc --noEmit`
-
-**Step 3:** Commit: `git add src/stores/entryTemplates.ts && git commit -m "feat: entry templates pinia store"`
-
----
-
-### Task 17: Entry templates - picker dialog and Entries.vue integration
-
-**Files:**
-- Create: `src/components/EntryTemplatePicker.vue`
-- Modify: `src/views/Entries.vue` (add "From Template" button, "Save as Template" in edit dialog, duplicate button per row)
-
-**Step 1:** Create the picker dialog. This is a listbox dialog showing all saved templates. Clicking one emits the template data. Uses focus trap, keyboard navigation (ArrowUp/Down, Enter to select), and Escape to close.
-
-**Step 2:** In Entries.vue, add:
-- A "From Template" button in the filters bar (near the "Copy Yesterday" / "Copy Last Week" buttons)
-- When clicked, opens EntryTemplatePicker dialog
-- On template selection, pre-fills the edit dialog in create mode
-- A "Save as Template" button in the edit dialog (visible when editing an existing entry)
-- The existing duplicate button on entry rows (already present per lines 181-185) should have its `aria-label` enhanced to include project name and date
-
-**Step 3:** Verify: `npx vue-tsc --noEmit && npx vite build`
-
-**Step 4:** Commit: `git add src/components/EntryTemplatePicker.vue src/views/Entries.vue && git commit -m "feat: entry template picker and save-as-template in entries view"`
-
----
-
-### Task 18: Entry templates - Settings management
-
-**Files:**
-- Modify: `src/views/Settings.vue` (Timer tab, add template management section)
-
-**Step 1:** Read Settings.vue Timer tab. Find where to insert (after Recurring Entries section, before the end of Timer tab panel, around line 775).
-
-**Step 2:** Add a "Saved Templates" section:
-
-```html
-
-
-
-
-
`, after the duration display, add:
-```html
-
- Rounded
-
-```
-
-Add a tooltip mechanism using `title` attribute or a custom tooltip with `role="tooltip"` and `aria-describedby` for the actual vs rounded values.
-
-**Step 3:** Verify: `npx vue-tsc --noEmit`
-
-**Step 4:** Commit: `git add src/views/Entries.vue && git commit -m "feat: rounding visibility indicators on entry rows"`
-
----
-
-### Task 30: Rounding visibility - Invoices and Reports
-
-**Files:**
-- Modify: `src/views/Invoices.vue` (show actual vs rounded on line items)
-- Modify: `src/views/Reports.vue` (add rounding impact summary in Hours tab)
-
-**Step 1:** In Invoices.vue, where invoice line items are displayed, show both actual and rounded hours when rounding is active. Add a small "+Xm" or "-Xm" text label.
-
-**Step 2:** In Reports.vue Hours tab, after the billable split line (around line 124), add a rounding impact summary:
-
-```html
-