max / goingson
23 files changed,
+0 insertions,
-4695 deletions
| @@ -1,165 +0,0 @@ | |||
| 1 | - | # GoingsOn -- Audit History | |
| 2 | - | ||
| 3 | - | Full chronological audit log. See [audit_review.md](./audit_review.md) for current state. | |
| 4 | - | ||
| 5 | - | ## Changes Since Last Audit | |
| 6 | - | ||
| 7 | - | **Previous audit:** 2026-04-22 (Run 15 corrected) | |
| 8 | - | ||
| 9 | - | ### Run 19 (2026-05-04, cross-project) | |
| 10 | - | - **Test count:** 765 (--workspace). 0 clippy warnings. 0 failures. | |
| 11 | - | - **Grade:** A (maintained). v0.3.1. ~68,800 LOC. | |
| 12 | - | - **New code:** Email signatures (041), drafts (042), labels (043), notifications (044) — uncommitted WIP. | |
| 13 | - | - **Cold spots found:** 6 (all low severity). Plugin loader symlink issue (medium, local-only risk). | |
| 14 | - | - **Mandatory surprise:** Symlink canonicalization fallback in plugin loader allows dangling symlink to bypass sandbox check. Fix: remove `.or_else` fallback. | |
| 15 | - | - **All previous items resolved.** No regressions. | |
| 16 | - | - **Methodology note:** Deeper module-level audit with parallel agents. More cold spots surfaced vs previous runs (broader methodology, not quality regression). | |
| 17 | - | ||
| 18 | - | ### Thirteenth audit (2026-04-18, Run 15 cross-project, corrected 2026-04-22) | |
| 19 | - | - **Test count:** 778 (--workspace). 0 clippy warnings. Grade A. | |
| 20 | - | - **False finding corrected:** Test "regression" was due to running without --workspace flag. | |
| 21 | - | - **Items resolved:** FK migration (non-issue), indexes added (040), observability expanded (435 annotations). | |
| 22 | - | ||
| 23 | - | ### Eleventh audit (2026-03-28, Run 12 cross-project) | |
| 24 | - | - **Test count:** ~734 (686 Rust + 48 JS). 0 clippy warnings. 0 failures. | |
| 25 | - | - **Grade:** A (maintained). v0.3.0. | |
| 26 | - | - **Code change:** Kanban board fix — invalid task status now silently dropped instead of crashing. Minor but correct. | |
| 27 | - | - **Dependency advisory:** async-std unmaintained (RUSTSEC-2025-0052, warning) — upstream via async-imap, no alternative available. | |
| 28 | - | - **Mandatory surprise:** None. Previous surprises (dead Validate trait, format!() SQL safety) both resolved. | |
| 29 | - | - **No new findings.** All previous items remain resolved. | |
| 30 | - | ||
| 31 | - | ### Tenth audit (2026-03-18, Run 9 cross-project) | |
| 32 | - | - **Test count:** 725 (677 Rust + 48 JS). 0 clippy warnings. 0 failures. | |
| 33 | - | - **Grade:** A (maintained). v0.3.0. | |
| 34 | - | - **Release build:** macOS DMG signed+notarized, verified with codesign + spctl. | |
| 35 | - | - **No new findings.** All previous items remain resolved. Validate trait wired. Sync engine safe. All cold spots closed. | |
| 36 | - | - **Mandatory surprise:** None. Previous surprises (dead Validate trait, format!() SQL safety) both resolved. | |
| 37 | - | ||
| 38 | - | ### Post-Run 8 fixes (2026-03-17) | |
| 39 | - | - **Clippy:** Fixed 8 warnings across 5 files: `approx_constant` (api.rs), `bool_assert_comparison` (api.rs ×2, registry.rs ×2), `unnecessary_get_then_check` (registry.rs ×2), `items_after_test_module` (imap_client.rs, jmap/email.rs, jmap/session.rs) | |
| 40 | - | - **Deps:** `time` crate bumped 0.3.46 → 0.3.47 (fixes RUSTSEC-2026-0009 stack exhaustion DoS) | |
| 41 | - | ||
| 42 | - | **Earlier audit:** 2026-03-11 (fifth audit) | |
| 43 | - | ||
| 44 | - | ### Concurrency Upgrade (2026-03-13) | |
| 45 | - | - **Concurrency:** A- -> A | |
| 46 | - | - Coordinated shutdown via CancellationToken for 4 async schedulers + AtomicBool for db_watcher threads. All background tasks now stop cleanly on app exit via RunEvent::Exit handler. | |
| 47 | - | ||
| 48 | - | ### Observability Upgrade (2026-03-13) | |
| 49 | - | - **Observability:** A- -> A | |
| 50 | - | - Added 195 `#[instrument(skip_all)]` annotations across 28 files | |
| 51 | - | - Coverage: all 144 Tauri commands (20 command files), 40 MCP tool implementation methods (5 impl files), 3 email_sync background functions, plus existing state/notifications instrumentation | |
| 52 | - | - `use tracing::instrument;` import added to each file | |
| 53 | - | - `cargo check --workspace` passes clean | |
| 54 | - | ||
| 55 | - | ### Adversarial Test Audit (2026-03-13) | |
| 56 | - | ||
| 57 | - | **Test count:** 648 -> 658 (+10 tests) | |
| 58 | - | ||
| 59 | - | Key changes: | |
| 60 | - | - **CRITICAL fix:** Weekly review timeline used `created_at` instead of `completed_at` — timeline showed task creation dates, not completion dates. Fixed in both repository query and test assertions. | |
| 61 | - | - **HIGH fix:** Completed/deleted tasks could be snoozed — added status check at repository level to reject snoozing non-pending/non-started tasks. | |
| 62 | - | - **HIGH fix:** Recurring task completion copied stale urgency from overdue parent — now recalculates urgency with fresh `calculate_urgency()` call instead of inheriting parent's inflated urgency score. | |
| 63 | - | - **HIGH fix:** MCP `create_task` skipped validation — added `new_task.validate()` call to enforce length limits, tag validation, and duration range checks. | |
| 64 | - | - **API improvement:** Search now returns `(Vec<SearchResultItem>, usize)` tuple for accurate total counts, enabling proper pagination UI. | |
| 65 | - | ||
| 66 | - | All fixes committed with test coverage. Zero regressions detected. | |
| 67 | - | ||
| 68 | - | ### Seventh audit (2026-03-16, Run 6 cross-project) | |
| 69 | - | - **Test count:** 658 -> 725 (+67 tests, 677 Rust + 48 JS) | |
| 70 | - | - **Grade:** A (maintained). No new findings above LOW. | |
| 71 | - | - **Rust LOC:** 44,007 (up from ~39K) | |
| 72 | - | - **Mandatory surprise:** ApiError system in commands/error.rs — machine-readable ErrorCode enum, 3 extension traits (OptionNotFound, OptionApiError, ResultApiError), full CoreError->ApiError conversion — Impressive | |
| 73 | - | - **Previous items verified:** All previous remediated items confirmed intact. | |
| 74 | - | ||
| 75 | - | ### Sixth audit (2026-03-13, pre-launch skeptical lens) | |
| 76 | - | - **Grade:** A -> A- -> A. Two must-fix findings found and resolved. | |
| 77 | - | - **Test count:** 485 -> 648 (+163 tests) | |
| 78 | - | - **New findings:** Validate trait never called in production (dead code), open_email_in_browser XSS, ImapClient::new() legacy constructor, email_repo expect in production. | |
| 79 | - | - **Mandatory surprise:** Dead Validate trait — genuine issue, now resolved. | |
| 80 | - | - **Previous items verified:** All 14 prior remediated items confirmed intact. | |
| 81 | - | ||
| 82 | - | **Post-audit remediation (2026-03-13):** | |
| 83 | - | - Grade: A- -> A. Both must-fix items resolved. | |
| 84 | - | - Validate trait wired into command layer (no longer dead code) | |
| 85 | - | - Email HTML sanitization added (CSP + script stripping) | |
| 86 | - | - ImapClient::new() removed, .expect() replaced with .ok_or(), LIMIT/OFFSET parameterized | |
| 87 | - | - All 4 cold spots resolved: JMAP (73 tests), OAuth (59 tests), plugin registry (32 tests), LLM typed errors | |
| 88 | - | - Test count: 485 -> 648 (+163 tests) | |
| 89 | - | ||
| 90 | - | ### What improved (fifth audit) | |
| 91 | - | - Test count: 435 -> 485 (+50 tests) | |
| 92 | - | - Sync service now has 21 unit tests (was 6) covering FK-ordered upsert/delete, trigger suppression, column whitelists, push/pull logic | |
| 93 | - | - Mobile port advanced: iOS simulator running, touch gestures wired, interaction wiring complete | |
| 94 | - | - MCP tool count: 16 -> 41 tools | |
| 95 | - | - Codebase grew from ~35K to 39,183 Rust LOC while maintaining quality | |
| 96 | - | ||
| 97 | - | ### What regressed | |
| 98 | - | - Nothing. All previous fixes remain in place. Zero clippy warnings. Zero test failures. | |
| 99 | - | ||
| 100 | - | ### New issues found (all resolved post-audit) | |
| 101 | - | - ~~JMAP module (854 LOC) has zero tests~~ -- 73 tests added | |
| 102 | - | - ~~OAuth callback server (309 LOC) has zero tests~~ -- 59 tests added | |
| 103 | - | - ~~Plugin registry hot-reload (300 LOC) has only 2 tests~~ -- 32 tests added | |
| 104 | - | - ~~LLM integration uses `Result<String, String>` instead of typed errors~~ -- typed LlmError enum added | |
| 105 | - | - `applying_remote` flag should be verified cleared on startup for crash recovery (cross-project finding from audiofiles audit) | |
| 106 | - | - Sync engine `format!()` SQL pattern is safe but lacks documentation explaining why | |
| 107 | - | ||
| 108 | - | ### Still open (2 items) | |
| 109 | - | - OAuth token manager tests (token refresh flows, keychain storage) | |
| 110 | - | - Full IMAP client integration tests (MIME parsing and folder ops -- pure function helpers tested) | |
| 111 | - | - ~~JS frontend has no automated tests~~ -- 48 JS tests added (AppStateManager, utils, PaginationManager, SelectionManager) | |
| 112 | - | ||
| 113 | - | ### Resolved | |
| 114 | - | - ~~Move `notify-debouncer-mini` to workspace deps~~ -- confirmed already in workspace deps (`Cargo.toml:68`) | |
| 115 | - | - ~~Add explicit timeouts to JMAP/OAuth/IMAP clients~~ -- 30s/15s request + 10s connect timeouts added | |
| 116 | - | ||
| 117 | - | --- | |
| 118 | - | ||
| 119 | - | ## Audit History | |
| 120 | - | ||
| 121 | - | ### First audit (2026-02-27) | |
| 122 | - | - Initial review. 234 tests. Grade: A-. Found body_preview UTF-8 issue (later confirmed safe), list_completed_between bug, stats query concern (later confirmed efficient). | |
| 123 | - | ||
| 124 | - | ### Second audit (2026-02-28) | |
| 125 | - | - Fixed tag search bug. Added 30 tests (contact_repo, search_repo). 289 tests. Grade: A-. | |
| 126 | - | ||
| 127 | - | ### Third audit (2026-02-28) | |
| 128 | - | - Theme system rewrite. Minor findings (smtp_client docs, notify-debouncer-mini). Grade: A-. | |
| 129 | - | ||
| 130 | - | ### Fourth audit (2026-03-01) | |
| 131 | - | - Full fresh audit. MCP tests added, list_completed_between fixed, 49 new tests. Grade: A- -> A. Found sync_service, IMAP, OAuth token manager, plugin API, CSS, sql_column() issues. | |
| 132 | - | ||
| 133 | - | ### Cleanup phase (2026-03-02) | |
| 134 | - | - All fourth-audit findings resolved. +97 tests (435 total). Sync service typed errors, sql_column() relocated, CSS sections, SMTP docs, plugin API tests, IMAP helper tests. | |
| 135 | - | ||
| 136 | - | ### Type safety phase (2026-03-02) | |
| 137 | - | - 11 entity ID newtypes. Stringly-typed fields replaced with enums. Type Safety: A- -> A. | |
| 138 | - | ||
| 139 | - | ### Fifth audit (2026-03-11) | |
| 140 | - | - Full fresh audit. 485 tests. Grade: A (maintained). Mobile port near-complete. New cold spots: JMAP (0 tests), OAuth callback (0 tests), plugin registry (2 tests), LLM string errors. Mandatory surprise: sync engine format!() SQL safety. | |
| 141 | - | ||
| 142 | - | ### Sixth audit (2026-03-13) | |
| 143 | - | - Pre-launch skeptical lens. 648 tests. Grade: A- -> A (post-remediation). Found dead Validate trait and email HTML XSS (both must-fix, both resolved same day). All 4 cold spots from fifth audit resolved. +163 tests. | |
| 144 | - | ||
| 145 | - | --- | |
| 146 | - | ||
| 147 | - | ## Documentation Review | |
| 148 | - | ||
| 149 | - | **Last reviewed:** 2026-03-04 (first doc audit) | |
| 150 | - | ||
| 151 | - | ### Overall Doc Grade: B+ | |
| 152 | - | ||
| 153 | - | Good coverage of architecture and style guide. CLAUDE.md GO section is thorough and accurate. No public-facing docs to worry about. Main gaps: description.md is a placeholder, and MCP_test.md is test scaffolding that could be removed. | |
| 154 | - | ||
| 155 | - | ### Document Heatmap | |
| 156 | - | ||
| 157 | - | | Document | Status | Last Verified | Notes | | |
| 158 | - | |----------|:------:|:-------------:|-------| | |
| 159 | - | | CLAUDE.md (GO section) | Current | 2026-03-04 | Accurate to codebase | | |
| 160 | - | | docs/ARCHITECTURE.md | Current | 2026-03-04 | Accurate to codebase | | |
| 161 | - | | docs/STYLEGUIDE.md | Current | 2026-03-04 | Skeubrute design system | | |
| 162 | - | | docs/description.md | Placeholder | 2026-03-04 | Intentional placeholder | | |
| 163 | - | | docs/competition.md | Current | 2026-03-04 | Competitive analysis | | |
| 164 | - | | docs/human_testing.md | Current | 2026-03-04 | Manual QA checklist | | |
| 165 | - | | docs/MCP_test.md | Low priority | 2026-03-04 | Test artifact, may not need to persist | |
| @@ -1,363 +0,0 @@ | |||
| 1 | - | # GoingsOn -- Audit Review | |
| 2 | - | ||
| 3 | - | **Last audited:** 2026-05-10 (Run 24, Ultra Fuzz) | |
| 4 | - | **Previous audit:** 2026-05-04 (Run 19 cross-project) | |
| 5 | - | **Auditor:** Claude Opus 4.6 (5-axis adversarial audit) | |
| 6 | - | **Scope:** Full workspace (`crates/core`, `crates/db-sqlite`, `crates/plugin-runtime`, `src-tauri`, frontend JS, migrations) | |
| 7 | - | ||
| 8 | - | --- | |
| 9 | - | ||
| 10 | - | ## Overall Grade: A | |
| 11 | - | ||
| 12 | - | 773 tests (all pass, `--workspace`). Zero clippy warnings. v0.3.1. ~73,900 LOC (50.5K Rust + 21.3K JS + 2.1K SQL). All SQL parameterized. Strong type safety. Clean 4-crate architecture. SyncKit E2E encryption with XChaCha20-Poly1305 + Argon2id KDF. | |
| 13 | - | ||
| 14 | - | --- | |
| 15 | - | ||
| 16 | - | ## Critical Findings (fix before launch) | |
| 17 | - | ||
| 18 | - | ### CRITICAL: 24 Commands Missing from Desktop Handler Registration | |
| 19 | - | ||
| 20 | - | **Location:** `src-tauri/src/main.rs:385-585` | |
| 21 | - | **Description:** The desktop `generate_handler![]` in `main.rs` is missing 24 commands that are present in the mobile entry point (`lib.rs`) and called by JS. Affected features on desktop: all file attachments, email blob handling, email drafts, email labels/folders, email account settings (signatures, notifications), VCF/ICS import, manual time logging, and sync subscription status. These commands silently fail — Tauri returns an error but the user sees nothing happen. | |
| 22 | - | **Fix:** Copy the missing entries from `lib.rs` into `main.rs` `generate_handler![]`. | |
| 23 | - | ||
| 24 | - | ### SERIOUS: Bulk Set Project/Priority Sends Incomplete TaskInput | |
| 25 | - | ||
| 26 | - | **Location:** `src-tauri/frontend/js/bulk-actions.js:176-198` | |
| 27 | - | **Description:** `_applyProject` and `_applyPriority` call `GoingsOn.api.tasks.update(id, { projectId })` with only one field. Backend `update_task` requires `description` to be non-empty. Since `description` defaults to `""` via serde, every bulk project/priority update fails with `VALIDATION_ERROR`. Both bulk actions are completely broken. | |
| 28 | - | **Fix:** Create a dedicated bulk-update command accepting partial updates, or fetch each task first and merge. | |
| 29 | - | ||
| 30 | - | ### SERIOUS: Backup Does Not Include Contacts, Daily Notes, Milestones, Time Sessions, or Attachments | |
| 31 | - | ||
| 32 | - | **Location:** `src-tauri/src/backup_scheduler.rs:114-134`, `src-tauri/src/export/backup.rs:17-33` | |
| 33 | - | **Description:** `FullExport` only serializes projects, tasks, events, and emails. Contacts (with child records), daily notes, milestones, time sessions, and attachments are silently excluded. A "full backup" is not full. Restoring from backup would lose contacts and time tracking data. | |
| 34 | - | **Fix:** Add the missing entity types to `FullExport` and corresponding fetch/restore logic. | |
| 35 | - | ||
| 36 | - | --- | |
| 37 | - | ||
| 38 | - | ## Scorecard | |
| 39 | - | ||
| 40 | - | ### Module Heatmap — Data Integrity | |
| 41 | - | ||
| 42 | - | | Module | Query Quality | Tx Safety | Type Safety | State Machines | Migration Safety | Validation | Overall | | |
| 43 | - | |--------|:---:|:---:|:---:|:---:|:---:|:---:|:---:| | |
| 44 | - | | **task_repo** | A | A | A | A- | A | A- | **A-** | | |
| 45 | - | | **email_repo** | A | A | A | A | A | A | **A** | | |
| 46 | - | | **search_repo** | A- | N/A | A | N/A | A | A- | **A-** | | |
| 47 | - | | **event_repo** | A | A | A | A | A | A | **A** | | |
| 48 | - | | **contact_repo** | A | A- | A | N/A | A | A | **A** | | |
| 49 | - | | **project_repo** | A | N/A | A | N/A | A | A | **A** | | |
| 50 | - | | **time_session_repo** | A | A | A | A | A | B+ | **A-** | | |
| 51 | - | | **stats_repo** | A+ | N/A | A | N/A | A | N/A | **A+** | | |
| 52 | - | | **milestone_repo** | A | A | A | A | A | B+ | **A-** | | |
| 53 | - | | **recurrence.rs** | N/A | N/A | A | A | N/A | A | **A** | | |
| 54 | - | | **urgency.rs** | N/A | N/A | A | A | N/A | A | **A** | | |
| 55 | - | | **validation.rs** | N/A | N/A | A | N/A | N/A | A | **A** | | |
| 56 | - | ||
| 57 | - | ### Module Heatmap — Sync & Storage | |
| 58 | - | ||
| 59 | - | | Module | Conflict Handling | Error Recovery | Data Integrity | Resource Mgmt | Architecture | Testing | Overall | | |
| 60 | - | |--------|:---:|:---:|:---:|:---:|:---:|:---:|:---:| | |
| 61 | - | | **sync_service/mod.rs** | A | A | A | A | A | A | **A** | | |
| 62 | - | | **sync_service/pull.rs** | A | A | A | A | A+ | A | **A** | | |
| 63 | - | | **sync_service/push.rs** | A | A- | A | A | A | A- | **A** | | |
| 64 | - | | **sync_service/apply.rs** | N/A | A | A | A | A | A | **A** | | |
| 65 | - | | **sync_service/blob_sync.rs** | N/A | A- | B+ | B | A- | B+ | **B+** | | |
| 66 | - | | **sync_scheduler.rs** | N/A | A | A | A | A | B+ | **A-** | | |
| 67 | - | | **db_watcher.rs** | N/A | A- | N/A | A | A- | A- | **A-** | | |
| 68 | - | | **backup_scheduler.rs** | N/A | A- | B | A | A- | B | **B+** | | |
| 69 | - | | **export/backup.rs** | N/A | A | A- | A | A | A- | **A-** | | |
| 70 | - | | **export/csv.rs** | N/A | A | A | A | A | A- | **A** | | |
| 71 | - | | **export/ics.rs** | N/A | A | A | A | A | A | **A** | | |
| 72 | - | | **external_sync/ical.rs** | N/A | A | A | A | A | A | **A** | | |
| 73 | - | | **external_sync/vcard.rs** | N/A | A | A | A | A | A | **A** | | |
| 74 | - | | **core/backup_restore.rs** | N/A | A- | B+ | A | A | B+ | **A-** | | |
| 75 | - | ||
| 76 | - | ### Module Heatmap — UX Wiring | |
| 77 | - | ||
| 78 | - | | Module | Contract Fidelity | Validation | State Mgmt | XSS Safety | Error Handling | Code Quality | Overall | | |
| 79 | - | |--------|:---:|:---:|:---:|:---:|:---:|:---:|:---:| | |
| 80 | - | | **commands/error.rs** | A+ | A+ | N/A | N/A | A+ | A+ | **A+** | | |
| 81 | - | | **commands/task.rs** | A | A | A | N/A | A | A | **A** | | |
| 82 | - | | **commands/event.rs** | A | A | A | N/A | A | A | **A** | | |
| 83 | - | | **commands/contact.rs** | A | A | A | N/A | A | A | **A** | | |
| 84 | - | | **main.rs (registration)** | B- | N/A | N/A | N/A | N/A | B- | **B-** | | |
| 85 | - | | **api.js** | A | A- | A | N/A | A | A | **A** | | |
| 86 | - | | **utils.js** | A | A | N/A | A+ | A | A | **A** | | |
| 87 | - | | **form-modal.js** | A | A | A- | A | A- | A | **A** | | |
| 88 | - | | **tasks.js** | A | A- | A- | A | A | A- | **A-** | | |
| 89 | - | | **events.js** | A- | A- | B+ | A | A- | A- | **A-** | | |
| 90 | - | | **emails.js** | A | A- | A- | A | A | A- | **A-** | | |
| 91 | - | | **contacts.js** | A | A- | A | A | B+ | A | **A** | | |
| 92 | - | | **bulk-actions.js** | B- | B- | A- | A | B | B+ | **B-** | | |
| 93 | - | | **cache.js** | A | N/A | A | N/A | N/A | A+ | **A** | | |
| 94 | - | | **pagination-manager.js** | A | A | A | N/A | A | A | **A** | | |
| 95 | - | ||
| 96 | - | ### Module Heatmap — Security | |
| 97 | - | ||
| 98 | - | | Module | Credential Mgmt | Encryption | Sandbox Security | Input Validation | Transport Security | Least Privilege | Overall | | |
| 99 | - | |--------|:---:|:---:|:---:|:---:|:---:|:---:|:---:| | |
| 100 | - | | **OAuth (credentials/provider)** | A | N/A | N/A | A | A | A | **A** | | |
| 101 | - | | **OAuth Callback Server** | A- | N/A | N/A | A | A- | A | **A-** | | |
| 102 | - | | **Token Manager** | A | N/A | N/A | A | A | A | **A** | | |
| 103 | - | | **IMAP Client** | A- | N/A | N/A | A | A- | A | **A-** | | |
| 104 | - | | **SMTP Client** | B+ | N/A | N/A | A | B | A | **B+** | | |
| 105 | - | | **JMAP Client** | A | N/A | N/A | A | A | A | **A** | | |
| 106 | - | | **Plugin Runtime** | N/A | N/A | A | A | N/A | A | **A** | | |
| 107 | - | | **SyncKit Integration** | A | A | N/A | A | A | A | **A** | | |
| 108 | - | | **Backup/Export** | N/A | C+ | N/A | A | N/A | A- | **B+** | | |
| 109 | - | | **Tauri Config/Capabilities** | N/A | N/A | N/A | B+ | N/A | A | **A-** | | |
| 110 | - | ||
| 111 | - | ### Module Heatmap — Performance | |
| 112 | - | ||
| 113 | - | | Module | Query Efficiency | Async Correctness | Resource Mgmt | Concurrency | Frontend Perf | Scalability | Overall | | |
| 114 | - | |--------|:---:|:---:|:---:|:---:|:---:|:---:|:---:| | |
| 115 | - | | **db-sqlite pool/init** | A | A | A- | B+ | — | B+ | **A-** | | |
| 116 | - | | **task_repo** | A- | A | A | A | — | B+ | **A-** | | |
| 117 | - | | **email_repo** | B+ | A | B | A | — | B | **B+** | | |
| 118 | - | | **stats_repo** | A+ | A | A | A | — | A | **A+** | | |
| 119 | - | | **search_repo** | A | A | A | A | — | A- | **A** | | |
| 120 | - | | **backup_scheduler** | B | A | B- | A- | — | B- | **B** | | |
| 121 | - | | **commands/export** | B- | A | B- | A | — | C+ | **B-** | | |
| 122 | - | | **commands/import_external** | B | A | A | A | — | B- | **B** | | |
| 123 | - | | **frontend/cache.js** | — | — | A | — | A | A | **A** | | |
| 124 | - | | **frontend/virtual-scroller.js** | — | — | A- | — | A | A | **A** | | |
| 125 | - | | **state.rs** | A | A | A | A | — | A | **A** | | |
| 126 | - | ||
| 127 | - | ### Axis Summary Grades | |
| 128 | - | ||
| 129 | - | | Axis | Overall | Cold Spots | Mandatory Surprise | | |
| 130 | - | |------|:-------:|------------|-------------------| | |
| 131 | - | | **Data Integrity** | A | time_session_repo validation (B+), FTS soft-delete leak | `complete_recurring` textbook transactional recurrence — wraps completion + next-instance in single tx with post-commit fresh fetch | | |
| 132 | - | | **Sync & Storage** | A- | blob_sync.rs (B+), backup_scheduler.rs (B+), incomplete backup | `applying_remote` flag + WAL isolation in pull.rs — triple-layered crash safety with detailed comments explaining SQLite isolation semantics | | |
| 133 | - | | **UX Wiring** | A- | main.rs registration (B-), bulk-actions.js (B-), events.js state key | Error humanization pipeline — `ERROR_CODE_LABELS` + `ERROR_CODE_HINTS` + UUID stripping + field metadata, end-to-end from DB to toast | | |
| 134 | - | | **Security** | A | SMTP plaintext path (B), backup encryption absent (C+), CSP null | Server-side PKCE storage with atomic CSRF consumption — code verifier never leaves Rust, `flows.remove()` validates + consumes in one op | | |
| 135 | - | | **Performance** | A- | backup/export unbounded queries (B-), email bodies in list queries (B), import N+1 | `rows_to_tasks` batch-loading — 3 batch queries replace N+1 for annotations/subtasks/sessions, consistently applied across all task list methods | | |
| 136 | - | ||
| 137 | - | --- | |
| 138 | - | ||
| 139 | - | ## Bug Reports by Axis | |
| 140 | - | ||
| 141 | - | ### Data Integrity | |
| 142 | - | ||
| 143 | - | | # | Severity | Location | Description | | |
| 144 | - | |---|----------|----------|-------------| | |
| 145 | - | | 1 | MINOR | `time_session_repo.rs:329-384` | `log_manual_time` accepts negative/zero minutes | | |
| 146 | - | | 2 | MINOR | `core/validation.rs` | `estimated_minutes` not validated (only `scheduled_duration` is) | | |
| 147 | - | | 3 | MINOR | Multiple repos | `list_all()` queries lack LIMIT (acceptable for single-user desktop) | | |
| 148 | - | | 4 | NOTE | `search_repo.rs:257-271` | FTS task search doesn't filter `status != 'Deleted'` (non-FTS branch does) | | |
| 149 | - | | 5 | NOTE | `migrations/sqlite/023_contacts.sql:58-68` | contacts FTS5 omits `title` (job title) field | | |
| 150 | - | ||
| 151 | - | **0 CRITICAL, 0 SERIOUS, 3 MINOR, 2 NOTE** | |
| 152 | - | ||
| 153 | - | ### Sync & Storage | |
| 154 | - | ||
| 155 | - | | # | Severity | Location | Description | | |
| 156 | - | |---|----------|----------|-------------| | |
| 157 | - | | 1 | **SERIOUS** | `backup_scheduler.rs:114-134`, `export/backup.rs:17-33` | Backup excludes contacts, daily notes, milestones, time sessions, attachments | | |
| 158 | - | | 2 | MEDIUM | `blob_sync.rs:53` | Blob upload reads entire file into memory (no streaming) | | |
| 159 | - | | 3 | MEDIUM | `blob_sync.rs:122` | Blob download reads entire file into memory | | |
| 160 | - | | 4 | LOW | `blob_sync.rs:130-139` | No hash verification after blob download | | |
| 161 | - | | 5 | LOW | `attachment_repo.rs:141-149` | Orphaned blob files on attachment delete (no FS cleanup) | | |
| 162 | - | | 6 | LOW | `backup_scheduler.rs:55,198` | Backup scheduler and manual backup can race (no lock) | | |
| 163 | - | ||
| 164 | - | **0 CRITICAL, 1 SERIOUS, 2 MEDIUM, 3 LOW** | |
| 165 | - | ||
| 166 | - | ### UX Wiring | |
| 167 | - | ||
| 168 | - | | # | Severity | Location | Description | | |
| 169 | - | |---|----------|----------|-------------| | |
| 170 | - | | 1 | **CRITICAL** | `main.rs:385-585` | 24 commands missing from desktop `generate_handler![]` | | |
| 171 | - | | 2 | **SERIOUS** | `bulk-actions.js:176-198` | Bulk project/priority sends incomplete TaskInput, fails validation | | |
| 172 | - | | 3 | MEDIUM | `events.js:471-489` | `deleteEvent` reads `GoingsOn.state.events` (never populated), should read `upcomingEvents`/`pastEvents` | | |
| 173 | - | | 4 | LOW | `bulk-actions.js:23-38` | `Promise.all()` hides partial failure — should use `Promise.allSettled()` | | |
| 174 | - | | 5 | LOW | `contacts.js:63,82` | Bulk error shows raw error object (`+ err` instead of `getErrorMessage()`) | | |
| 175 | - | | 6 | LOW | `events.js:70` | Event bulk delete error shows raw error | | |
| 176 | - | ||
| 177 | - | **1 CRITICAL, 1 SERIOUS, 1 MEDIUM, 3 LOW** | |
| 178 | - | ||
| 179 | - | ### Security | |
| 180 | - | ||
| 181 | - | | # | Severity | Location | Description | | |
| 182 | - | |---|----------|----------|-------------| | |
| 183 | - | | 1 | MEDIUM | `tauri.conf.json:26` | CSP disabled (`null`) — no defense-in-depth against XSS in email HTML | | |
| 184 | - | | 2 | MEDIUM | `smtp_client.rs:202,220` | `use_tls=false` sends credentials in cleartext via `builder_dangerous()` | | |
| 185 | - | | 3 | LOW | `smtp_client.rs:143-151` | Legacy `SmtpClient::new()` reads password from account struct (pre-keychain path) | | |
| 186 | - | | 4 | LOW-MEDIUM | `export/backup.rs` | Backup files are gzip only, no encryption | | |
| 187 | - | | 5 | LOW | `notifications.rs:149-153` | OS notifications leak email sender + subject on lock screen | | |
| 188 | - | | 6 | LOW | `state.rs:84` | SQLite database unencrypted at rest (standard for desktop apps) | | |
| 189 | - | ||
| 190 | - | **0 CRITICAL, 0 SERIOUS, 2 MEDIUM, 4 LOW** | |
| 191 | - | ||
| 192 | - | ### Performance | |
| 193 | - | ||
| 194 | - | | # | Severity | Location | Description | | |
| 195 | - | |---|----------|----------|-------------| | |
| 196 | - | | 1 | MEDIUM | `backup_scheduler.rs:130`, `commands/export.rs:105-108` | Backup loads ALL emails with bodies into memory (unbounded). `get_export_summary` fetches all rows just to return `.len()` | | |
| 197 | - | | 2 | LOW-MEDIUM | `commands/task.rs:261-263` | `list_tasks` returns ALL tasks with annotations/subtasks — no limit | | |
| 198 | - | | 3 | LOW-MEDIUM | `email_repo.rs:113,237,268` | Several email queries fetch full bodies without pagination | | |
| 199 | - | | 4 | LOW | `commands/import_external.rs:143-150` | vCard/ICS import does serial per-record dedup (N+1) | | |
| 200 | - | | 5 | LOW | `db-sqlite/lib.rs:33-34` | Pool size 5 may cause contention with 3 concurrent schedulers | | |
| 201 | - | ||
| 202 | - | **0 CRITICAL, 0 SERIOUS, 1 MEDIUM, 4 LOW** | |
| 203 | - | ||
| 204 | - | --- | |
| 205 | - | ||
| 206 | - | ## Cross-Cutting Concerns | |
| 207 | - | ||
| 208 | - | ### Backup system has both coverage and performance problems | |
| 209 | - | The incomplete backup (Sync axis: missing entity types) and the unbounded memory usage (Performance axis: loading all emails with bodies) are two facets of the same module. When fixing backup coverage, also fix the streaming/pagination issue. | |
| 210 | - | ||
| 211 | - | ### Event delete state key bug affects both UX and data consistency | |
| 212 | - | The `events.js` state key mismatch (UX axis) means optimistic UI removal doesn't work, and undo restoration does nothing. The actual deletion succeeds server-side, so no data loss — but the UI doesn't reflect the change until a refresh. | |
| 213 | - | ||
| 214 | - | --- | |
| 215 | - | ||
| 216 | - | ## Components Successfully Stress-Tested | |
| 217 | - | ||
| 218 | - | ### Data Integrity | |
| 219 | - | - SQL injection via FTS5 — `prepare_fts5_query` strips special chars, wraps in quotes | |
| 220 | - | - Cross-user data leakage — every query includes `user_id = ?` | |
| 221 | - | - Concurrent timer start race — transactional check-then-insert | |
| 222 | - | - Recurrence chain breakage on crash — `complete_recurring` is transactional | |
| 223 | - | - Month-end date drift — `add_months` clamps to target month length | |
| 224 | - | - Foreign key cascades — correct ON DELETE CASCADE/SET NULL per relationship type | |
| 225 | - | ||
| 226 | - | ### Sync & Storage | |
| 227 | - | - Sync conflict resolution — LWW with explicit conflict detection and three-way resolution | |
| 228 | - | - Sync state machine — cannot get stuck; triple-reset of `applying_remote` flag | |
| 229 | - | - FK ordering — `UPSERT_ORDER` and `DELETE_ORDER` are verified reverses (tested) | |
| 230 | - | - Trigger suppression — remote changes don't fire sync changelog triggers (WAL isolation) | |
| 231 | - | - CSV formula injection — `sanitize_csv_field` prefixes dangerous characters | |
| 232 | - | - Backup atomicity — write-to-tmp + rename. Decompression bomb guard (500MB limit) | |
| 233 | - | - SSE reconnection — detected + reconnected after 5s delay | |
| 234 | - | - DB watcher — debounced (500ms) + rate-limited (1s) + trailing edge timer | |
| 235 | - | ||
| 236 | - | ### UX Wiring | |
| 237 | - | - XSS prevention — `escapeHtml`/`escapeAttr` used consistently across all modules | |
| 238 | - | - Pagination — `PaginationManager` handles negative/overflow/empty/count-change | |
| 239 | - | - Form builder — all field types, custom validators, abort controller prevents duplicate submit | |
| 240 | - | - Cache invalidation — generation-based + 30s TTL, mutations invalidate before API call | |
| 241 | - | - State reactivity — pub/sub correctly notifies; virtual scrollers refresh from state | |
| 242 | - | ||
| 243 | - | ### Security | |
| 244 | - | - OAuth PKCE — S256 challenge, code verifier never leaves Rust, CSRF state consumed atomically | |
| 245 | - | - Credential storage — OS keychain via `keyring`, `#[serde(skip_serializing)]` on sensitive fields | |
| 246 | - | - Plugin sandbox — eval disabled, operation limits (100K), stack depth (32), size limits, canonical path checks | |
| 247 | - | - SQL injection — all queries parameterized; sync table/column names from `&'static str` whitelists | |
| 248 | - | - E2E encryption — XChaCha20-Poly1305, Argon2id (64MB/3 iterations), proper key hierarchy, zeroize-on-drop | |
| 249 | - | - Email credential isolation — sync excludes password/OAuth token columns via `EMAIL_ACCOUNT_SYNC_COLS` | |
| 250 | - | - Callback server — `127.0.0.1` only, HTML escaped, non-GET rejected, 5-minute timeout | |
| 251 | - | ||
| 252 | - | ### Performance | |
| 253 | - | - SQLite WAL mode — correctly enabled, mitigates single-writer contention | |
| 254 | - | - Batch loading — `rows_to_tasks` does 3 batch queries instead of N+1 | |
| 255 | - | - Token refresh serialization — per-account Mutex with double-check pattern | |
| 256 | - | - IMAP oversized email filtering — pre-filters by `RFC822.SIZE` (25MB cap), `spawn_blocking` | |
| 257 | - | - Virtual scroller — height caching, overscan buffer, RAF scroll, ResizeObserver cleanup | |
| 258 | - | - FTS5 search — parallel 5-type search via `tokio::join!`, BM25 ranking, per-type caps | |
| 259 | - | ||
| 260 | - | --- | |
| 261 | - | ||
| 262 | - | ## Confidence Assessment | |
| 263 | - | ||
| 264 | - | | Axis | Confidence | Justification | | |
| 265 | - | |------|:----------:|---------------| | |
| 266 | - | | Data Integrity | HIGH | All SQL parameterized, user-scoped, newtype IDs, FK integrity verified | | |
| 267 | - | | Sync & Storage | HIGH | Every file read end-to-end, sync engine well-structured, small attack surface | | |
| 268 | - | | UX Wiring | HIGH | Bugs 1-3 confirmed by diffing/tracing; contract alignment verified across domains | | |
| 269 | - | | Security | HIGH | Full OAuth flow traced, plugin sandbox verified, encryption algorithm audited | | |
| 270 | - | | Performance | HIGH | 19 modules fully traced, query patterns verified, pool config audited | | |
| 271 | - | ||
| 272 | - | --- | |
| 273 | - | ||
| 274 | - | ## Metrics | |
| 275 | - | ||
| 276 | - | - Modules audited: 60+ | |
| 277 | - | - Total cold spots: 7 | |
| 278 | - | - Bugs by severity: 1 critical, 2 serious, 6 medium, 14 minor/low, 2 note | |
| 279 | - | - Axes at A or above: 3/5 (Data A, Sync A-, UX A-, Security A, Performance A-) | |
| 280 | - | ||
| 281 | - | --- | |
| 282 | - | ||
| 283 | - | ## Recommended Priority Order | |
| 284 | - | ||
| 285 | - | 1. **[CRITICAL] Register 24 missing commands in main.rs** — entire feature categories broken on desktop. Minutes to fix, massive blast radius. | |
| 286 | - | 2. **[SERIOUS] Fix bulk project/priority update** — create partial-update command or fetch-then-merge. Users will hit this immediately. | |
| 287 | - | 3. **[SERIOUS] Complete backup entity coverage** — add contacts, daily notes, milestones, time sessions, attachments to `FullExport`. Data loss risk on restore. | |
| 288 | - | 4. **[MEDIUM] Fix event delete state key** — `GoingsOn.state.events` → `upcomingEvents`/`pastEvents`. Simple JS fix. | |
| 289 | - | 5. **[MEDIUM] Add CSP to tauri.conf.json** — defense-in-depth, especially since app renders email HTML. | |
| 290 | - | 6. **[MEDIUM] Stream blob uploads/downloads** — currently loads entire file into memory. Matters for large attachments. | |
| 291 | - | 7. **[MEDIUM] Fix backup memory usage** — `get_export_summary` should use COUNT; backup should stream. | |
| 292 | - | 8. **[LOW] Add SMTP TLS warning** — log warning or show UI indicator when `use_tls=false`. | |
| 293 | - | 9. **[LOW] Validate `estimated_minutes` and `log_manual_time` minutes** — reject negative/zero. | |
| 294 | - | 10. **[LOW] Fix FTS soft-delete leak** — add `AND t.status != 'Deleted'` to FTS task search. | |
| 295 | - | 11. **[LOW] Use `Promise.allSettled()` in bulk-actions.js** — report partial failures. | |
| 296 | - | 12. **[LOW] Fix raw error display in contacts.js and events.js bulk operations**. | |
| 297 | - | 13. **[LOW] Verify blob hash after download** — detect transit corruption. | |
| 298 | - | 14. **[LOW] Add blob GC for orphaned files on attachment delete**. | |
| 299 | - | ||
| 300 | - | --- | |
| 301 | - | ||
| 302 | - | ## Delta Since Run 19 | |
| 303 | - | ||
| 304 | - | ### Fixed from Run 19 | |
| 305 | - | | Item | Status | | |
| 306 | - | |------|--------| | |
| 307 | - | | Symlink canonicalization in plugin loader (loader.rs:118) | **FIXED** — dangling symlinks now skipped (confirmed by security agent) | | |
| 308 | - | ||
| 309 | - | ### Unfixed from Run 19 | |
| 310 | - | | Item | Status | | |
| 311 | - | |------|--------| | |
| 312 | - | | Extract shared date/recurrence parsing from ical.rs/vcard.rs | **DEFERRED** — still separate implementations | | |
| 313 | - | | Add exponential backoff to email_sync_scheduler | **DEFERRED** — still retries every 60s | | |
| 314 | - | | Optimize plugin CSV parser to single-pass | **DEFERRED** — still double-read | | |
| 315 | - | ||
| 316 | - | ### New findings this run | |
| 317 | - | - 1 CRITICAL (24 missing desktop commands) | |
| 318 | - | - 2 SERIOUS (bulk update broken, incomplete backup) | |
| 319 | - | - 6 MEDIUM (event delete state, CSP, blob memory, backup memory, SMTP plaintext, blob download) | |
| 320 | - | - 14 LOW/MINOR/NOTE | |
| 321 | - | ||
| 322 | - | ### Grade changes | |
| 323 | - | | Dimension | Run 19 | Run 24 | Delta | | |
| 324 | - | |-----------|:------:|:------:|:-----:| | |
| 325 | - | | Overall | A | A | — | | |
| 326 | - | | Security | A | A | — | | |
| 327 | - | | Performance | A- | A- | — | | |
| 328 | - | | Architecture | A | A | — | | |
| 329 | - | | Testing | A | A | — | | |
| 330 | - | ||
| 331 | - | ### CHRONIC items (unfixed across 2+ runs) | |
| 332 | - | - **Email sync exponential backoff** — deferred since Run 19. Low severity, no user impact yet. | |
| 333 | - | - **Plugin CSV double-read** — deferred since Run 19. Low severity, plugin system rarely used. | |
| 334 | - | ||
| 335 | - | --- | |
| 336 | - | ||
| 337 | - | ## Metrics Over Time | |
| 338 | - | ||
| 339 | - | | Audit Date | LOC | Tests | Tests/KLOC | Clippy | Cold Spots | Overall | | |
| 340 | - | |------------|-----|-------|-----------|--------|-----------|---------| | |
| 341 | - | | 2026-02-27 | ~30K | 234 | 7.8 | 0 | 3 | A- | | |
| 342 | - | | 2026-02-28 | ~30K | 289 | 9.6 | 0 | 2 | A- | | |
| 343 | - | | 2026-03-01 | ~33K | 338 | 10.2 | 0 | 1 | A | | |
| 344 | - | | 2026-03-02 | ~35K | 435 | 12.4 | 0 | 0 | A | | |
| 345 | - | | 2026-03-11 | 39K | 485 | 12.4 | 0 | 0 | A | | |
| 346 | - | | 2026-03-13 | ~39K | 658 | ~16.9 | 0 | 0 | A | | |
| 347 | - | | 2026-03-16 | 44K | 725 | ~16.5 | 0 | 0 | A | | |
| 348 | - | | 2026-03-28 (Run 12) | ~44K | ~734 | ~16.7 | 0 | 0 | A | | |
| 349 | - | | 2026-04-15 (Run 14) | ~64K | ~762 | ~12 | 0 | 0 | A | | |
| 350 | - | | 2026-04-22 (Run 15) | ~64K | 778 | ~12.1 | 0 | 0 | A | | |
| 351 | - | | 2026-05-04 (Run 19) | ~69K | 765 | ~11.1 | 0 | 6 | A | | |
| 352 | - | | **2026-05-10 (Run 24)** | **~74K** | **773** | **~10.4** | **0** | **7** | **A** | | |
| 353 | - | ||
| 354 | - | ### Delta Since Last Audit | |
| 355 | - | - **LOC:** +4,600 (email features, sync hardening, notifications) | |
| 356 | - | - **Tests:** +8 (773 vs 765) | |
| 357 | - | - **Cold spots:** +1 (deeper 5-axis methodology found bulk-actions.js and backup) | |
| 358 | - | - **Grade:** A (maintained) | |
| 359 | - | - **Key fix:** Symlink canonicalization vulnerability from Run 19 resolved | |
| 360 | - | ||
| 361 | - | --- | |
| 362 | - | ||
| 363 | - | See [audit_history.md](./audit_history.md) for full chronological audit log. |
| @@ -1,310 +0,0 @@ | |||
| 1 | - | # GoingsOn -- Competitive Analysis | |
| 2 | - | ||
| 3 | - | Last updated: 2026-02-27 | |
| 4 | - | ||
| 5 | - | ## Positioning | |
| 6 | - | ||
| 7 | - | GoingsOn is the only app that combines tasks, email, calendar, contacts, and weekly review in a single offline-first native application. Every competitor covers 1-2 of these domains. Built with Tauri 2 (Rust backend, vanilla JS frontend, SQLite), it targets independent workers who want a fast, offline-capable workspace without SaaS lock-in. | |
| 8 | - | ||
| 9 | - | The core advantage is integration depth (tasks + email + calendar + contacts + projects in one local-first app) combined with privacy (no server, no tracking, zero-knowledge sync) and extensibility (Rhai plugins). No single competitor matches this combination. In a market where annual subscription costs range from $48 to $408, GoingsOn ships free and source-available. | |
| 10 | - | ||
| 11 | - | ## Pricing Comparison | |
| 12 | - | ||
| 13 | - | | App | Price | Model | | |
| 14 | - | |-----|-------|-------| | |
| 15 | - | | **GoingsOn** | **Free** | Source-available, no subscription | | |
| 16 | - | | TickTick Premium | ~$28-36/yr | Subscription | | |
| 17 | - | | Fantastical | ~$40/yr | Subscription | | |
| 18 | - | | Todoist Pro | $48/yr | Cloud subscription | | |
| 19 | - | | Obsidian Sync | $48/yr | Optional sync add-on | | |
| 20 | - | | Spark Premium | $60/yr | Subscription | | |
| 21 | - | | Things 3 | ~$80 | One-time purchase (Apple only) | | |
| 22 | - | | Notion Plus | $120/yr | Cloud subscription | | |
| 23 | - | | Sunsama | $192/yr | Cloud subscription | | |
| 24 | - | | Akiflow | $228/yr | Cloud subscription | | |
| 25 | - | ||
| 26 | - | ## Feature Matrix | |
| 27 | - | ||
| 28 | - | | Feature | GO | Todoist | Things 3 | TickTick | Notion | Obsidian | Fantastical | Spark | Sunsama | Akiflow | | |
| 29 | - | |---------|:--:|:------:|:--------:|:--------:|:------:|:--------:|:-----------:|:-----:|:-------:|:-------:| | |
| 30 | - | | **Tasks** | Yes | Yes | Yes | Yes | Yes | Plugin | Basic | No | Yes | Yes | | |
| 31 | - | | **Email client** | Yes | No | No | No | No | No | No | Yes | Partial | Partial | | |
| 32 | - | | **Calendar** | Yes | Partial | Read-only | Yes | Basic | Plugin | Yes | No | Yes | Yes | | |
| 33 | - | | **Contacts** | Yes | No | No | No | No | No | No | No | No | No | | |
| 34 | - | | **Weekly review** | Yes | No | No | No | No | No | No | No | Yes | No | | |
| 35 | - | | **Offline-first** | Yes | No | Yes | No | Limited | Yes | Yes | No | No | No | | |
| 36 | - | | **Local data** | Yes | No | Partial | No | No | Yes | Partial | No | No | No | | |
| 37 | - | | **Source-available** | Yes | No | No | No | No | No | No | No | No | No | | |
| 38 | - | | **Plugin system** | Yes | No | No | No | API | Yes | No | No | No | No | | |
| 39 | - | | **Urgency scoring** | Yes | 4 levels | No | No | No | No | No | No | No | No | | |
| 40 | - | | **Cross-platform** | Yes | Yes | Apple only | Yes | Yes | Yes | Apple+Win | Yes | Yes | Mac/Win | | |
| 41 | - | | **Linux** | Yes | Yes | No | Yes | Yes | Yes | No | No | No | No | | |
| 42 | - | | **Mobile** | In progress | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Web only | | |
| 43 | - | | **AI features** | No | Yes | No | No | Yes | No | No | Yes | Planned | Yes | | |
| 44 | - | | **Team collab** | No | Yes | No | No | Yes | No | No | Yes | Yes | No | | |
| 45 | - | | **Free tier** | Yes | Yes | No | Yes | Yes | Yes | Limited | Yes | No | No | | |
| 46 | - | ||
| 47 | - | ## Competitor Deep Dives | |
| 48 | - | ||
| 49 | - | ### 1. Todoist | |
| 50 | - | ||
| 51 | - | **Task management SaaS with natural language input, collaboration, and AI assistant.** | |
| 52 | - | ||
| 53 | - | Pricing: Free (5 projects) | Pro $4/mo ($48/yr) | Business $6-10/user/mo | |
| 54 | - | ||
| 55 | - | | Feature GoingsOn Lacks | Notes | | |
| 56 | - | |------------------------|-------| | |
| 57 | - | | Kanban board view | Drag tasks between columns; useful for visual workflows | | |
| 58 | - | | Location-based reminders | Triggers reminders when arriving/leaving a location `[DATA-HUNGRY]` | | |
| 59 | - | | Karma gamification (points, streaks, levels) | Productivity scoring from Beginner to Enlightenment `[BLOAT]` | | |
| 60 | - | | Real-time collaboration (shared projects, task comments, assignments) | Multi-user shared projects with task delegation | | |
| 61 | - | | AI voice-to-task (Todoist Ramble) | Speak naturally, AI extracts tasks/dates/details `[DATA-HUNGRY]` | | |
| 62 | - | | AI task suggestions & auto-breakdown | AI suggests subtasks, rewrites vague tasks, recommends next steps `[DATA-HUNGRY]` | | |
| 63 | - | | Activity log / audit history | Full history of all changes across projects | | |
| 64 | - | | Vacation mode (protects streaks) | Pauses gamification during time off `[BLOAT]` | | |
| 65 | - | | Web app | Full browser-based access with no install | | |
| 66 | - | | Template gallery | Pre-built project templates for common workflows | | |
| 67 | - | | Urgent reminders (iOS full-screen alarm) | Persistent alarm that overrides Do Not Disturb `[INVASIVE]` | | |
| 68 | - | ||
| 69 | - | ### 2. Things 3 | |
| 70 | - | ||
| 71 | - | **Elegant GTD-style task manager for Apple platforms. One-time purchase.** | |
| 72 | - | ||
| 73 | - | Pricing: Mac $49.99 | iPhone $9.99 | iPad $19.99 (one-time, no subscription) | |
| 74 | - | ||
| 75 | - | | Feature GoingsOn Lacks | Notes | | |
| 76 | - | |------------------------|-------| | |
| 77 | - | | Areas (high-level life categories) | Group projects under areas like "Work", "Personal", "Health" | | |
| 78 | - | | Today / This Evening split | Separate morning and evening task sections within the day | | |
| 79 | - | | Logbook (completed task archive) | Browsable archive of everything you've finished | | |
| 80 | - | | Someday list | GTD "Someday/Maybe" list for non-urgent ideas | | |
| 81 | - | | Quick Entry with project/heading targeting | System-wide shortcut to add tasks directly into a specific heading | | |
| 82 | - | | Headings within projects | Visual section dividers within a project task list | | |
| 83 | - | | Apple Watch app | Task management from the wrist | | |
| 84 | - | | Apple Shortcuts / URL scheme | Deep automation via Shortcuts and x-callback-url | | |
| 85 | - | | Magic Plus button (context-aware) | "+" button behavior changes based on current view | | |
| 86 | - | | Mail-to-Things (forward emails as tasks) | Forward an email to a special address to create a task | | |
| 87 | - | | Type-to-filter in any list | Start typing to instantly filter the visible list | | |
| 88 | - | ||
| 89 | - | ### 3. TickTick | |
| 90 | - | ||
| 91 | - | **All-in-one task manager with calendar, habits, Pomodoro timer, and Kanban.** | |
| 92 | - | ||
| 93 | - | Pricing: Free (9 lists, 99 tasks/list) | Premium ~$28-36/yr ($2.80/mo) | |
| 94 | - | ||
| 95 | - | | Feature GoingsOn Lacks | Notes | | |
| 96 | - | |------------------------|-------| | |
| 97 | - | | Habit tracker | Daily habit tracking with streaks and completion charts | | |
| 98 | - | | Pomodoro / focus timer | Built-in countdown and stopwatch with session tracking | | |
| 99 | - | | White noise / ambient sounds | Background sounds (rain, cafe, nature) during focus sessions `[BLOAT]` | | |
| 100 | - | | Eisenhower Matrix view | Four-quadrant urgent/important grid view | | |
| 101 | - | | Kanban board view | Column-based task organization | | |
| 102 | - | | Multiple calendar views (5 types) | Day, 3-day, week, month, and agenda calendar views | | |
| 103 | - | | Calendar subscription (read external) | Subscribe to external calendars (read-only) | | |
| 104 | - | | Task duration estimates with stats | Estimate time per task, track actual vs estimated | | |
| 105 | - | | Achievement badges | Gamification rewards for productivity milestones `[BLOAT]` | | |
| 106 | - | | Smart date parsing in descriptions | Detects dates within task descriptions | | |
| 107 | - | | Web app | Browser-based access | | |
| 108 | - | | Apple Watch / Wear OS | Wrist-based task management | | |
| 109 | - | | Widgets (iOS/Android/desktop) | Home screen widgets showing tasks and calendar | | |
| 110 | - | ||
| 111 | - | ### 4. Notion | |
| 112 | - | ||
| 113 | - | **Workspace combining docs, databases, wikis, project management, and AI agents.** | |
| 114 | - | ||
| 115 | - | Pricing: Free (personal) | Plus $8-10/mo | Business $20/user/mo | Enterprise custom | |
| 116 | - | ||
| 117 | - | | Feature GoingsOn Lacks | Notes | | |
| 118 | - | |------------------------|-------| | |
| 119 | - | | Freeform documents / pages | Rich-text documents with nested blocks and embeds | | |
| 120 | - | | Relational databases (20+ property types) | Structured data with relations, rollups, formulas, filters | | |
| 121 | - | | 6 database views (table, board, timeline, calendar, gallery, list) | Multiple visual layouts for the same data | | |
| 122 | - | | Wiki / knowledge base | Team wiki with verified pages and breadcrumb navigation | | |
| 123 | - | | AI agents (autonomous multi-step workflows) | Agent performs 20min of autonomous work across hundreds of pages `[DATA-HUNGRY]` | | |
| 124 | - | | Automations (trigger-based workflows) | If-then automations on database changes | | |
| 125 | - | | Forms builder | Create forms that feed into databases | | |
| 126 | - | | Sites (publish pages as website) | Turn Notion pages into public websites | | |
| 127 | - | | Template gallery (thousands) | Massive community template ecosystem | | |
| 128 | - | | Real-time collaboration (multiplayer editing) | Multiple users edit the same page simultaneously | | |
| 129 | - | | Web app | Full browser-based access | | |
| 130 | - | | API (public REST) | Third-party integrations via public API | | |
| 131 | - | | Embeds (50+ services) | Embed Figma, Loom, Google Maps, Miro, etc. inline | | |
| 132 | - | | Comments and mentions | @mention users, threaded comments on blocks | | |
| 133 | - | | Conditional database coloring | Color rows/cells based on formula conditions | | |
| 134 | - | | People directory | Auto-built team directory from workspace members `[BLOAT]` | | |
| 135 | - | | Version history (7-90 days by plan) | Page-level version history with restore | | |
| 136 | - | ||
| 137 | - | ### 5. Spark Mail | |
| 138 | - | ||
| 139 | - | **Smart email client with AI writing, inbox triage, and team collaboration.** | |
| 140 | - | ||
| 141 | - | Pricing: Free (basic) | Premium $4.99/mo ($59.99/yr) | Team $6.99/user/mo | |
| 142 | - | ||
| 143 | - | | Feature GoingsOn Lacks | Notes | | |
| 144 | - | |------------------------|-------| | |
| 145 | - | | Smart Inbox (auto-categorization) | Auto-sorts into Personal, Notifications, Newsletters `[DATA-HUNGRY]` | | |
| 146 | - | | Gatekeeper (unknown sender blocking) | Holds first-time senders in a queue for approval | | |
| 147 | - | | AI email compose / rephrase / translate | AI drafts, rewrites, and translates emails `[DATA-HUNGRY]` | | |
| 148 | - | | AI email summarization | Summarize long threads in one click `[DATA-HUNGRY]` | | |
| 149 | - | | My Writing Style (AI learns your tone) | AI mimics your personal writing style `[DATA-HUNGRY]` | | |
| 150 | - | | Send Later (scheduled send) | Compose now, send at a specified time | | |
| 151 | - | | Email signatures (rich, multiple) | Multiple formatted signatures, auto-switch per account | | |
| 152 | - | | Shared team inbox | Multiple team members manage one inbox | | |
| 153 | - | | Shared drafts (real-time co-editing) | Collaborate on email drafts in real time | | |
| 154 | - | | Email assignment to team members | Assign emails to colleagues with status tracking | | |
| 155 | - | | Smart notifications (only for important) | Only notifies for emails from real people, not newsletters `[DATA-HUNGRY]` | | |
| 156 | - | | Priority sender badges | Visual indicator for VIP contacts | | |
| 157 | - | | Newsletter digest | Groups newsletters into a single digest | | |
| 158 | - | | Quick replies (one-tap responses) | Suggested short responses at the bottom of emails | | |
| 159 | - | | Follow-up reminders | Automatic reminders when sent emails get no reply | | |
| 160 | - | | Email pin | Pin important emails to the top of inbox | | |
| 161 | - | ||
| 162 | - | ### 6. Fantastical | |
| 163 | - | ||
| 164 | - | **Premium calendar app with natural language, scheduling proposals, and weather.** | |
| 165 | - | ||
| 166 | - | Pricing: Free (limited) | Premium ~$3.33/mo ($40/yr) | Team pricing available | |
| 167 | - | ||
| 168 | - | | Feature GoingsOn Lacks | Notes | | |
| 169 | - | |------------------------|-------| | |
| 170 | - | | Calendar sets (grouped calendar toggling) | Switch between "Work" and "Home" calendar groups with one click | | |
| 171 | - | | Location-based calendar sets | Auto-switch calendar set when arriving/leaving a location `[DATA-HUNGRY]` | | |
| 172 | - | | Weather forecast in calendar | 10-day AccuWeather forecast inline in calendar views `[BLOAT]` | | |
| 173 | - | | Meeting/scheduling proposals | Send event proposals with multiple time options to invitees | | |
| 174 | - | | Conference call auto-detection | Auto-detects Zoom/Teams/Meet links and adds join buttons | | |
| 175 | - | | Conference call auto-creation | Automatically adds video call links to scheduled events | | |
| 176 | - | | Travel time estimates | Shows travel time between events on the calendar | | |
| 177 | - | | Interesting calendars (sports, TV, holidays) | Subscribe to curated event calendars `[BLOAT]` | | |
| 178 | - | | Apple Watch app | Calendar on the wrist | | |
| 179 | - | | Apple Vision Pro support | Spatial calendar in visionOS | | |
| 180 | - | | Multiple calendar views (day/week/month/quarter/year) | More granular view options including quarter and year | | |
| 181 | - | | Availability sharing (Openings) | Share available time slots via link for others to book | | |
| 182 | - | | Task integration (Apple Reminders, Todoist) | Show tasks from external task apps on the calendar | | |
| 183 | - | | Forward-to-Fantastical (email to event) | Forward an email to create a calendar event automatically | | |
| 184 | - | | Date & time proposals in templates | Scheduling templates with reusable configurations | | |
| 185 | - | ||
| 186 | - | ### 7. Obsidian | |
| 187 | - | ||
| 188 | - | **Local-first markdown knowledge base with graph view and 2700+ community plugins.** | |
| 189 | - | ||
| 190 | - | Pricing: Free (core app) | Sync $4/mo | Publish $8/site/mo | Catalyst $25 one-time | |
| 191 | - | ||
| 192 | - | | Feature GoingsOn Lacks | Notes | | |
| 193 | - | |------------------------|-------| | |
| 194 | - | | Freeform markdown documents | Long-form writing with wiki-style linking | | |
| 195 | - | | Graph view (knowledge visualization) | Interactive visual map of note connections | | |
| 196 | - | | Backlinks (bidirectional linking) | Every note shows what links to it automatically | | |
| 197 | - | | Canvas (infinite whiteboard) | Spatial arrangement of notes, images, and links | | |
| 198 | - | | Daily notes | Auto-created daily journal entries | | |
| 199 | - | | 2700+ community plugins | Massive extensibility ecosystem | | |
| 200 | - | | Dataview (database queries on markdown) | SQL-like queries across your notes | | |
| 201 | - | | Templater (advanced templates) | Dynamic templates with JavaScript logic | | |
| 202 | - | | Local markdown files (open format) | Plain .md files in a folder, readable by any editor | | |
| 203 | - | | Publish (notes as website) | Publish notes as a static website | | |
| 204 | - | | Excalidraw (drawing/diagramming) | Built-in whiteboard via plugin | | |
| 205 | - | | Vim mode | Native Vim keybindings in the editor | | |
| 206 | - | | Community themes (hundreds) | Massive theme ecosystem | | |
| 207 | - | | CLI tool | Command-line access to vaults (new in 2026) | | |
| 208 | - | ||
| 209 | - | ### 8. Sunsama | |
| 210 | - | ||
| 211 | - | **Guided daily planner that pulls tasks from external tools into a timeboxed schedule.** | |
| 212 | - | ||
| 213 | - | Pricing: $16/mo annually ($192/yr) | $20/mo monthly | No free plan | |
| 214 | - | ||
| 215 | - | | Feature GoingsOn Lacks | Notes | | |
| 216 | - | |------------------------|-------| | |
| 217 | - | | Guided daily planning ritual (multi-step) | Step-by-step morning workflow: review yesterday, pick today's tasks, timebox | | |
| 218 | - | | Daily shutdown ritual | Guided end-of-day reflection and task rollover | | |
| 219 | - | | Focus mode / focus timer | Single-task focus mode with timer (Pomodoro optional) | | |
| 220 | - | | Task time estimates with actuals tracking | Estimate task duration, compare to actual time spent | | |
| 221 | - | | Automatic timeboxing (planned) | AI auto-schedules tasks based on priority and availability `[DATA-HUNGRY]` | | |
| 222 | - | | Pull tasks from external tools | Import from Asana, Trello, Jira, ClickUp, Notion, Linear, GitHub | | |
| 223 | - | | Pull tasks from Slack/Teams messages | Turn chat messages into tasks `[DATA-HUNGRY]` | | |
| 224 | - | | Gmail/Outlook email-to-task (native) | Pull emails as tasks from integrated email accounts | | |
| 225 | - | | Daily Slack/Teams standup post | Auto-post your daily plan to a team channel | | |
| 226 | - | | Workload guardrails | Warns when you've scheduled more than your target hours | | |
| 227 | - | | Drag tasks between days | Move unfinished tasks to future days visually | | |
| 228 | - | | Weekly analytics | Time spent per day/week with trend visualization | | |
| 229 | - | | Theme days | Label days with a focus theme (e.g., "Deep Work Monday") | | |
| 230 | - | | Unified task view across integrations | Single list pulling from 10+ external tools | | |
| 231 | - | ||
| 232 | - | ## Common Missing Features | |
| 233 | - | ||
| 234 | - | Features that appear in 3+ competitors and GoingsOn currently lacks. | |
| 235 | - | ||
| 236 | - | ### Worth Adding | |
| 237 | - | ||
| 238 | - | | Feature | Competitors | Reasoning | | |
| 239 | - | |---------|-------------|-----------| | |
| 240 | - | | **Kanban / board view** | Todoist, TickTick, Notion, Obsidian (plugin) | Visual alternative to list/table views. Low complexity, high utility for project-oriented users. Could be a project-level view option. | | |
| 241 | - | | **Apple Watch app** | Things 3, TickTick, Fantastical | Quick task capture and glanceable agenda from the wrist. Tauri 2 doesn't target watchOS natively, so this would be a separate Swift micro-app. Post-launch. | | |
| 242 | - | | **Task time estimates with tracking** | TickTick, Sunsama, Todoist (duration) | GoingsOn already has time blocking with duration. Adding estimate-vs-actual tracking is a small extension with real value for freelancers billing by the hour. | | |
| 243 | - | | **Widgets (iOS/Android/desktop)** | TickTick, Things 3, Fantastical, Notion | Home screen widgets for today's tasks and upcoming events. Requires native widget code per platform. Post-mobile-launch. | | |
| 244 | - | | **Template gallery / starter templates** | Todoist, Notion, TickTick | Ship a small set of built-in project templates (e.g., "Freelance Project", "Content Pipeline", "Job Search"). Not a marketplace, just bundled starter configs. | | |
| 245 | - | | **External calendar sync (Google/Apple)** | Fantastical, TickTick, Sunsama, Notion | Already planned. Google Calendar, Apple Calendar, and CalDAV sync are on the roadmap. High priority for launch. | | |
| 246 | - | | **Send Later (scheduled email send)** | Spark, (Todoist has scheduled tasks) | Simple to implement -- queue outbound email with a send-at timestamp. Useful for timezone-aware communication. | | |
| 247 | - | | **Multiple calendar views (month/quarter/year)** | Fantastical, TickTick, Notion | GoingsOn has a day plan timeline. A month view is the most obvious gap. Quarter/year views are lower priority. | | |
| 248 | - | ||
| 249 | - | ### Consider | |
| 250 | - | ||
| 251 | - | | Feature | Competitors | Reasoning | | |
| 252 | - | |---------|-------------|-----------| | |
| 253 | - | | **Focus / Pomodoro timer** | TickTick, Sunsama, Obsidian (plugin) | Natural fit alongside time blocking and day plan. Keep it simple: a countdown timer linked to a task, no gamification. | | |
| 254 | - | | **Activity log / history** | Todoist, Notion (version history), Spark | An audit trail of task/project changes. Useful for accountability and undo. Could be implemented locally in SQLite with minimal overhead. | | |
| 255 | - | | **Guided daily planning ritual** | Sunsama, (Things 3 Today/Evening), TickTick | GoingsOn has weekly review but no daily planning ritual. A lightweight morning workflow (review overdue, pick today's tasks, timebox) would complement the existing weekly review without adding bloat. | | |
| 256 | - | ||
| 257 | - | ### Skip | |
| 258 | - | ||
| 259 | - | | Feature | Competitors | Reasoning | | |
| 260 | - | |---------|-------------|-----------| | |
| 261 | - | | **Web app** | Todoist, TickTick, Notion, Sunsama | Contradicts local-first architecture and privacy model. Tauri desktop + mobile covers the target audience. A web app would require a server and undermine the offline-first value prop. | | |
| 262 | - | | **Real-time collaboration** | Todoist, Notion, Spark, Sunsama | GoingsOn targets independent workers, not teams. Collaboration adds massive complexity (CRDT/OT, permissions, presence) with minimal value for the target user. | | |
| 263 | - | | **Freeform documents / notes** | Notion, Obsidian, (Things 3 has task notes) | GoingsOn has task annotations and descriptions. Full document editing is a massive scope expansion that competes with dedicated tools. Better to integrate with Obsidian via plugin than to rebuild a notes system. | | |
| 264 | - | | **AI writing / compose assistance** | Todoist, Spark, Notion | Adding AI compose for emails would require sending email content to a third party, conflicting with the privacy-first model. Not building this. | | |
| 265 | - | ||
| 266 | - | ## What We Offer That Competitors Don't | |
| 267 | - | ||
| 268 | - | - **Only app with all 5 domains** -- tasks + email + calendar + contacts + weekly review in one native app. No other tool does this. Deep cross-linking between all entity types (email-to-task, task-to-event, contact-to-everything). | |
| 269 | - | - **Offline-first with zero cloud dependency** -- SQLite local storage, no account creation, no data on third-party servers. Works fully offline. | |
| 270 | - | - **Zero-knowledge sync** -- when cloud sync ships, all data is end-to-end encrypted client-side (XChaCha20-Poly1305) before leaving the device. The server literally cannot read your data. | |
| 271 | - | - **Pluggable sync providers** -- not locked into one cloud. Choose from GoingsOn Cloud, WebDAV, Dropbox, Google Drive, S3, OneDrive, or a local folder. | |
| 272 | - | - **Source-available** -- unique among all competitors. | |
| 273 | - | - **TaskWarrior-style urgency scoring** -- algorithmic priority considering due date proximity, age, priority level, overdue penalty, started bonus, tag bonuses. More sophisticated than any competitor's priority system. | |
| 274 | - | - **Rhai plugin system** -- user-extensible without forking. Obsidian has plugins but they're JavaScript with no sandboxing. | |
| 275 | - | - **OAuth2 email for 4 providers** -- Fastmail (JMAP), Google, Microsoft, Yahoo with PKCE flow. Most email clients support fewer OAuth providers. | |
| 276 | - | - **Natural-language quick-add** -- type `Fix bug +urgent project:GoingsOn pri:H due:tomorrow recur:weekly` instead of filling out a form. | |
| 277 | - | - **10 built-in themes** -- Neobrute, Catppuccin (Latte/Frappe/Macchiato/Mocha), Dracula, Nord, Tokyo Night, Flatwhite, Ayu Light. | |
| 278 | - | - **Skeubrute design system** -- distinctive visual identity: paper texture, embossed text, debossed inputs, tactile buttons. | |
| 279 | - | - **Single codebase for desktop + mobile** -- same Rust backend and JS frontend for macOS, Windows, Linux, iOS, and Android via Tauri 2. No Electron, no web framework overhead. | |
| 280 | - | - **Keyboard-first UX** -- vim-style navigation (g+t, j/k, q for quick-add), global shortcuts overlay, full keyboard accessibility. | |
| 281 | - | - **No subscription** -- in a market where annual costs range from $48 to $408. | |
| 282 | - | ||
| 283 | - | ## Key Dynamics | |
| 284 | - | ||
| 285 | - | - Every major competitor is shipping AI features. End-user AI (summarize, compose, schedule) is the biggest gap. | |
| 286 | - | - Sunsama ($192/yr) is the closest philosophical match -- daily planning, weekly review, calendar+tasks+email integration -- but cloud-only and expensive. | |
| 287 | - | - The offline-first, local-data, source-available combination is a genuine moat for privacy-conscious users. | |
| 288 | - | - Unified workspace eliminates app-switching (vs. using Todoist + Spark + Fantastical separately). | |
| 289 | - | - No subscription required for core features (vs. Fantastical/Sunsama/Spark subscriptions). | |
| 290 | - | ||
| 291 | - | **Biggest competitive gaps to close:** | |
| 292 | - | 1. Kanban/board view for tasks (table stakes for task apps) | |
| 293 | - | 2. Monthly calendar view (everyone expects it) | |
| 294 | - | 3. External calendar sync (already planned, high priority) | |
| 295 | - | 4. Focus timer alongside time blocking (natural fit) | |
| 296 | - | 5. Guided daily planning ritual (complements weekly review) | |
| 297 | - | ||
| 298 | - | **Strongest defenses:** | |
| 299 | - | - Unified workspace eliminates app-switching | |
| 300 | - | - Local-first with E2E encrypted sync | |
| 301 | - | - No subscription for core features | |
| 302 | - | - Plugin system for user extensibility (only Obsidian competes here) | |
| 303 | - | ||
| 304 | - | ## Target Users | |
| 305 | - | ||
| 306 | - | - Independent workers, freelancers, solopreneurs, and solo creators who juggle multiple projects across different domains (software, writing, art, business) | |
| 307 | - | - Power users who want keyboard-driven workflows, natural-language input, and scriptable automation | |
| 308 | - | - Privacy-conscious professionals who want local data ownership with optional end-to-end encrypted sync | |
| 309 | - | - People frustrated with using 4-5 separate apps (task manager + email client + calendar + contacts + notes) who want a single integrated tool | |
| 310 | - |
| @@ -1,82 +0,0 @@ | |||
| 1 | - | # GoingsOn — Build & Deploy | |
| 2 | - | ||
| 3 | - | See `_meta/docs/deploy.md` for shared infrastructure (machines, git remotes, collection layout, shared deps). | |
| 4 | - | ||
| 5 | - | ## Artifacts | |
| 6 | - | ||
| 7 | - | | Platform | Artifact | Machine | | |
| 8 | - | |----------|----------|---------| | |
| 9 | - | | macOS aarch64 | .dmg | local | | |
| 10 | - | | Linux aarch64 | AppImage, .deb, .rpm | astra | | |
| 11 | - | | Linux x86_64 | AppImage, .deb, .rpm | pop-os | | |
| 12 | - | | Windows x86_64 | .msi, NSIS .exe | windows-x86 | | |
| 13 | - | ||
| 14 | - | ## Build Commands | |
| 15 | - | ||
| 16 | - | ```bash | |
| 17 | - | # Signing env (see _meta/docs/deploy.md § Tauri Updater Signing for setup). | |
| 18 | - | # Linux/macOS prefix: | |
| 19 | - | # . ~/.tauri/passwords.env && \ | |
| 20 | - | # TAURI_SIGNING_PRIVATE_KEY=$HOME/.tauri/goingson.key \ | |
| 21 | - | # TAURI_SIGNING_PRIVATE_KEY_PASSWORD=$GOINGSON_TAURI_PASSWORD \ | |
| 22 | - | # cargo tauri build | |
| 23 | - | ||
| 24 | - | # macOS (local): | |
| 25 | - | cd ~/Code/Apps/goingson && \ | |
| 26 | - | . ~/.tauri/passwords.env && \ | |
| 27 | - | TAURI_SIGNING_PRIVATE_KEY=$HOME/.tauri/goingson.key \ | |
| 28 | - | TAURI_SIGNING_PRIVATE_KEY_PASSWORD=$GOINGSON_TAURI_PASSWORD \ | |
| 29 | - | cargo tauri build | |
| 30 | - | cp target/release/bundle/dmg/*.dmg ~/Dist/goingson/macos/ | |
| 31 | - | ||
| 32 | - | # Linux aarch64 (astra): | |
| 33 | - | ssh astra 'source ~/.cargo/env && cd ~/Code/Apps/goingson && git pull && . ~/.tauri/passwords.env && TAURI_SIGNING_PRIVATE_KEY=$HOME/.tauri/goingson.key TAURI_SIGNING_PRIVATE_KEY_PASSWORD=$GOINGSON_TAURI_PASSWORD cargo tauri build' | |
| 34 | - | scp astra:~/Code/Apps/goingson/target/release/bundle/appimage/*.AppImage ~/Dist/goingson/linux-aarch64/ | |
| 35 | - | scp astra:~/Code/Apps/goingson/target/release/bundle/deb/*.deb ~/Dist/goingson/linux-aarch64/ | |
| 36 | - | ||
| 37 | - | # Linux x86_64 (pop-os): | |
| 38 | - | ssh pop-os 'source ~/.cargo/env && cd ~/Code/Apps/goingson && git pull && . ~/.tauri/passwords.env && TAURI_SIGNING_PRIVATE_KEY=$HOME/.tauri/goingson.key TAURI_SIGNING_PRIVATE_KEY_PASSWORD=$GOINGSON_TAURI_PASSWORD cargo tauri build' | |
| 39 | - | scp pop-os:~/Code/Apps/goingson/target/release/bundle/appimage/*.AppImage ~/Dist/goingson/linux-x86_64/ | |
| 40 | - | scp pop-os:~/Code/Apps/goingson/target/release/bundle/deb/*.deb ~/Dist/goingson/linux-x86_64/ | |
| 41 | - | ||
| 42 | - | # Windows (windows-x86): run via PowerShell — see _meta/docs/deploy.md for the $env pattern. | |
| 43 | - | ssh me@windows-x86 'powershell -Command ". C:\Users\me\.tauri\passwords.ps1; $env:TAURI_SIGNING_PRIVATE_KEY=\"C:\Users\me\.tauri\goingson.key\"; $env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD=$env:GOINGSON_TAURI_PASSWORD; cd C:\Users\me\Code\Apps\goingson; git pull; cargo tauri build"' | |
| 44 | - | scp me@windows-x86:"C:/Users/me/Code/Apps/goingson/target/release/bundle/msi/*.msi" ~/Dist/goingson/windows/ | |
| 45 | - | scp me@windows-x86:"C:/Users/me/Code/Apps/goingson/target/release/bundle/nsis/*-setup.exe" ~/Dist/goingson/windows/ | |
| 46 | - | ``` | |
| 47 | - | ||
| 48 | - | ## iOS / TestFlight | |
| 49 | - | ||
| 50 | - | ```bash | |
| 51 | - | ./dist/release-ios.sh # build + export + upload | |
| 52 | - | ./dist/release-ios.sh --build-only # archive only | |
| 53 | - | ./dist/release-ios.sh --upload-only # export + upload existing archive | |
| 54 | - | ``` | |
| 55 | - | ||
| 56 | - | Requirements: | |
| 57 | - | - App Store Connect API key role must be **App Manager** or **Admin**. Developer-role keys fail at export with `Cloud signing permission error` / `No profiles for 'com.goingson.app' were found`. Roles are not editable — revoke and reissue. | |
| 58 | - | - `xcrun altool` looks for the `.p8` only in `~/.appstoreconnect/private_keys/`, `~/private_keys/`, `~/.private_keys/`, or `<cwd>/private_keys/`. The `--apiKeyPath` flag is not honored. Symlink: `ln -s /Users/max/Code/_private/AuthKey_<KEYID>.p8 ~/.appstoreconnect/private_keys/`. | |
| 59 | - | - Mobile-only target gating: any `use crate::notifications::...` or other desktop-only module imports must be `#[cfg(not(any(target_os = "ios", target_os = "android")))]`, matching the gate in `src-tauri/src/lib.rs`. The Rust compile is invoked from an Xcode build phase, so import errors fail the iOS build silently from Xcode's perspective. | |
| 60 | - | ||
| 61 | - | Current key: `35BZ6XF6GT` (App Manager). Team `93C54W92UP`. Bundle ID `com.goingson.app`. App Store Connect Apple ID `6759975645`. | |
| 62 | - | ||
| 63 | - | ## Project-Specific Notes | |
| 64 | - | ||
| 65 | - | - `beforeBuildCommand` uses `node src-tauri/frontend/build-css.js` (cross-platform). | |
| 66 | - | - OTA updates: signed Tauri updater artifacts are uploaded to S3 via the MNW dashboard. | |
| 67 | - | - Version is in `src-tauri/tauri.conf.json`. | |
| 68 | - | - Windows builds are unsigned (code signing blocked on Azure certificate history requirement). | |
| 69 | - | ||
| 70 | - | ## Troubleshooting | |
| 71 | - | ||
| 72 | - | ### beforeBuildCommand on Windows | |
| 73 | - | ||
| 74 | - | The CSS build step uses `node build-css.js` (not a shell script). If you see a build fail mentioning `build-css.sh`, the `tauri.conf.json` has regressed — it must use the `.js` version for Windows compatibility. Shell scripts cannot run on Windows without WSL. | |
| 75 | - | ||
| 76 | - | ### Node.js not found on Linux | |
| 77 | - | ||
| 78 | - | If `cargo tauri build` fails with "node: not found", ensure Node.js is in PATH for non-login shells. Add to `~/.bashrc` (not just `~/.profile`): | |
| 79 | - | ||
| 80 | - | ```bash | |
| 81 | - | export PATH="$HOME/.nvm/versions/node/v22.x.x/bin:$PATH" | |
| 82 | - | ``` |
| @@ -1,139 +0,0 @@ | |||
| 1 | - | # Flaws | |
| 2 | - | ||
| 3 | - | Code fuzz of GoingsOn, 2026-04-24. All 37 findings fixed. | |
| 4 | - | ||
| 5 | - | ## Serious | |
| 6 | - | ||
| 7 | - | ### 1. `truncate_text` panics on multi-byte UTF-8 -- FIXED | |
| 8 | - | - **File**: `src-tauri/src/notifications.rs` | |
| 9 | - | - Uses `char_indices()` to find last valid char boundary. | |
| 10 | - | ||
| 11 | - | ### 2. Push marks unpushed changelog entries as pushed -- FIXED | |
| 12 | - | - **File**: `src-tauri/src/sync_service/push.rs` | |
| 13 | - | - Tracks actual pushed IDs; UPDATE only those. | |
| 14 | - | ||
| 15 | - | ### 3. Backup restore creates dangling FK references -- FIXED | |
| 16 | - | - **File**: `crates/core/src/backup_restore.rs` | |
| 17 | - | - Builds old-to-new ID map during project restore; remaps `project_id` on tasks and events. | |
| 18 | - | ||
| 19 | - | ### 4. OAuth callback `code` not URL-decoded -- FIXED | |
| 20 | - | - **File**: `src-tauri/src/oauth/callback_server.rs` | |
| 21 | - | - URL-decodes `code` and `state`. Also fixed URL decoder for multi-byte UTF-8. | |
| 22 | - | ||
| 23 | - | ### 5. OAuth callback buffer too small -- FIXED | |
| 24 | - | - **File**: `src-tauri/src/oauth/callback_server.rs` | |
| 25 | - | - Buffer increased to 16 KB. | |
| 26 | - | ||
| 27 | - | ### 6. `UnifiedEmail` discards `is_read` flag -- FIXED | |
| 28 | - | - **File**: `src-tauri/src/email/provider.rs` | |
| 29 | - | - Uses `email.is_read`. | |
| 30 | - | ||
| 31 | - | ### 7. Negative urgency bonus for future `created_at` -- FIXED | |
| 32 | - | - **File**: `crates/core/src/urgency.rs` | |
| 33 | - | - Clamped `age_days` to `max(0.0)`. | |
| 34 | - | ||
| 35 | - | ### 8. "This week" boundary inconsistency -- FIXED | |
| 36 | - | - **File**: `crates/db-sqlite/src/repository/search_repo.rs` | |
| 37 | - | - SQL uses `weekday 1` (next Monday) with `<`, matching Rust's "through Sunday" boundary. | |
| 38 | - | ||
| 39 | - | ### 9. LIKE wildcard injection in tag search -- FIXED | |
| 40 | - | - **Files**: `search_repo.rs`, `contact_repo.rs` | |
| 41 | - | - Added `escape_like()` helper; applied to all 3 LIKE patterns with `ESCAPE '\'`. | |
| 42 | - | ||
| 43 | - | ### 10. Quick-add parser accepts negative relative dates -- FIXED | |
| 44 | - | - **File**: `crates/core/src/parser.rs` | |
| 45 | - | - Rejects `num < 1` in `parse_relative_date`. | |
| 46 | - | ||
| 47 | - | ## Minor | |
| 48 | - | ||
| 49 | - | ### 11. Plugin `read_file` has no size limit -- FIXED | |
| 50 | - | - **File**: `crates/plugin-runtime/src/api.rs` | |
| 51 | - | - Checks `fs::metadata` size; rejects files over 10 MB. | |
| 52 | - | ||
| 53 | - | ### 12. Plugin context not cleared on error -- FIXED | |
| 54 | - | - **File**: `crates/plugin-runtime/src/registry.rs` | |
| 55 | - | - RAII `ContextGuard` ensures cleanup on all paths. | |
| 56 | - | ||
| 57 | - | ### 13. IMAP UID overflow -- FIXED | |
| 58 | - | - **File**: `src-tauri/src/email/imap_client.rs` | |
| 59 | - | - Uses `saturating_add(1)`. | |
| 60 | - | ||
| 61 | - | ### 14. Invalid UID 0 fallback -- FIXED | |
| 62 | - | - **File**: `src-tauri/src/email/imap_client.rs` | |
| 63 | - | - Skips emails with no UID. | |
| 64 | - | ||
| 65 | - | ### 15. Search LIMIT overflow -- FIXED | |
| 66 | - | - **File**: `crates/db-sqlite/src/repository/search_repo.rs` | |
| 67 | - | - Uses `saturating_add()`. | |
| 68 | - | ||
| 69 | - | ### 16. iCal import drops timezone data -- FIXED | |
| 70 | - | - **File**: `src-tauri/src/external_sync/ical.rs` | |
| 71 | - | - Added `chrono-tz`. Resolves IANA timezone names and converts local time to UTC. | |
| 72 | - | ||
| 73 | - | ### 17. vCard quoted-printable decoder produces mojibake -- FIXED | |
| 74 | - | - **File**: `src-tauri/src/external_sync/vcard.rs` | |
| 75 | - | - Collects into `Vec<u8>`, then `String::from_utf8_lossy`. | |
| 76 | - | ||
| 77 | - | ### 18. CSV export: no formula injection protection -- FIXED | |
| 78 | - | - **File**: `src-tauri/src/export/csv.rs` | |
| 79 | - | - `sanitize_csv_field` prepends `'` to cells starting with formula characters. | |
| 80 | - | ||
| 81 | - | ### 19. Backup restore: no decompression size limit -- FIXED | |
| 82 | - | - **File**: `src-tauri/src/export/backup.rs` | |
| 83 | - | - Uses `Read::take(500 MB)`. | |
| 84 | - | ||
| 85 | - | ### 20. No file size limit on vCard/ICS import -- FIXED | |
| 86 | - | - **File**: `src-tauri/src/commands/import_external.rs` | |
| 87 | - | - `read_import_file()` rejects files over 50 MB. | |
| 88 | - | ||
| 89 | - | ### 21. Monthly recurrence day drift -- FIXED | |
| 90 | - | - **File**: `crates/core/src/recurrence.rs` | |
| 91 | - | - End-of-month heuristic targets day 31 for months that ended on their last day. Added `calculate_next_due_with_day()` for explicit control. Also fixed `add_months` to use `rem_euclid` for correctness with negative months. | |
| 92 | - | ||
| 93 | - | ### 22. Negative duration in day planning -- FIXED | |
| 94 | - | - **File**: `src-tauri/src/commands/day_planning.rs` | |
| 95 | - | - Clamps duration to `max(1)`. | |
| 96 | - | ||
| 97 | - | ## Notes | |
| 98 | - | ||
| 99 | - | ### 23. Custom URL decoder mojibake -- FIXED (with #4) | |
| 100 | - | ### 24. IMAP folder name not sanitized for CRLF -- FIXED | |
| 101 | - | - Control character validation on `archive_folder_name` in create/update email account commands. | |
| 102 | - | ||
| 103 | - | ### 25. Predictable temp filename for email preview -- FIXED | |
| 104 | - | - Added UUID v4 random suffix to temp filename. | |
| 105 | - | ||
| 106 | - | ### 26. Dedup DB error swallowed -- FIXED | |
| 107 | - | - Changed `unwrap_or_default()` to `?`. | |
| 108 | - | ||
| 109 | - | ### 27. Sync `total_applied` overcounts -- FIXED | |
| 110 | - | - Counts only `clean` + `resolved_entries` actually applied. | |
| 111 | - | ||
| 112 | - | ### 28. Silent fallback on malformed saved view filters -- FIXED | |
| 113 | - | - Added `tracing::warn!` on deserialization failure. | |
| 114 | - | ||
| 115 | - | ### 29. TOCTOU in task snooze -- FIXED | |
| 116 | - | - Atomic `AND status NOT IN (...)` in UPDATE WHERE clause. | |
| 117 | - | ||
| 118 | - | ### 30. `time_progress()` returns 0-255, documented as 0-100 -- FIXED | |
| 119 | - | - Changed `.min(255.0)` to `.min(100.0)`. | |
| 120 | - | ||
| 121 | - | ### 31. Negative plugin progress wraps to usize::MAX -- FIXED | |
| 122 | - | - Clamps negative values to 0 before cast. | |
| 123 | - | ||
| 124 | - | ### 32. `sort_by` parsing breaks on quotes -- FIXED | |
| 125 | - | - Uses `serde_json::Value::String` instead of manual quote wrapping. | |
| 126 | - | ||
| 127 | - | ### 33. `strip_html` leaves unclosed script/style content -- FIXED | |
| 128 | - | - Truncates from unclosed tag to end of string. | |
| 129 | - | ||
| 130 | - | ### 34. Import sub-collection errors silently discarded -- FIXED | |
| 131 | - | - Replaced `let _ =` with `if let Err(e)` and `tracing::warn!`. | |
| 132 | - | ||
| 133 | - | ### 35. `add_months` uses `%` instead of `rem_euclid` -- FIXED (with #21) | |
| 134 | - | ||
| 135 | - | ### 36. JMAP client silently falls back to no-timeout client -- FIXED | |
| 136 | - | - Changed `unwrap_or_default()` to `expect()`. | |
| 137 | - | ||
| 138 | - | ### 37. `delete_backup` doesn't validate file extension -- FIXED | |
| 139 | - | - Added `.json.gz` extension check before deletion. |
| @@ -1,152 +0,0 @@ | |||
| 1 | - | # GoingsOn — Pre-Launch Manual Testing | |
| 2 | - | ||
| 3 | - | Run before every release. Sign-off table at the bottom. | |
| 4 | - | ||
| 5 | - | ## How to Test | |
| 6 | - | ||
| 7 | - | - Work through P0 first — these are launch-blocking | |
| 8 | - | - Note failures inline and keep going; don't block the whole run | |
| 9 | - | - P1 = core features, P2 = edge cases / platform-specific | |
| 10 | - | - Use a fresh data directory (or a backup) so testing doesn't pollute your real data | |
| 11 | - | ||
| 12 | - | ### Environment Setup | |
| 13 | - | ||
| 14 | - | - [ ] Built binary or `cargo tauri dev` running | |
| 15 | - | - [ ] MNW account available (creator or fan) for sync tests | |
| 16 | - | - [ ] At least one IMAP account credential (or skip the email section) | |
| 17 | - | - [ ] Two devices (or two data dirs) for sync verification | |
| 18 | - | ||
| 19 | - | ### Tips | |
| 20 | - | ||
| 21 | - | - Tauri dev tools: right-click → Inspect for JS console errors | |
| 22 | - | - Server logs: `journalctl --user -u goingson` on Linux, Console.app on macOS | |
| 23 | - | - Stripe test card for sync checkout: `4242 4242 4242 4242` | |
| 24 | - | ||
| 25 | - | --- | |
| 26 | - | ||
| 27 | - | ## P0 — Critical Path | |
| 28 | - | ||
| 29 | - | > If any of these fail, do not ship. | |
| 30 | - | ||
| 31 | - | ### Launch & Persistence | |
| 32 | - | ||
| 33 | - | - [ ] App launches without error on a clean install | |
| 34 | - | - [ ] Window opens at expected size, no console errors | |
| 35 | - | - [ ] Data persists across restart (create a task, quit, relaunch — task is there) | |
| 36 | - | - [ ] Theme loads from embedded default; theme picker switches without restart | |
| 37 | - | - [ ] Import a custom `.toml` theme — applies immediately | |
| 38 | - | ||
| 39 | - | ### Tasks | |
| 40 | - | ||
| 41 | - | - [ ] Create task (description, priority, tags) — appears in active list | |
| 42 | - | - [ ] Start → Complete transitions persist | |
| 43 | - | - [ ] Snooze hides task; un-snooze (or wait) restores it | |
| 44 | - | - [ ] Recurring task: complete one occurrence, next instance generates with correct date | |
| 45 | - | - [ ] Delete task — gone from list and DB | |
| 46 | - | ||
| 47 | - | ### Projects + Calendar | |
| 48 | - | ||
| 49 | - | - [ ] Create project; assign task to it; project detail shows the task | |
| 50 | - | - [ ] Create calendar event with date/time — appears on correct day | |
| 51 | - | - [ ] Edit event time — persists across restart | |
| 52 | - | ||
| 53 | - | ### Contacts | |
| 54 | - | ||
| 55 | - | - [ ] Create contact (name, email, phone) — appears in list | |
| 56 | - | - [ ] Search by name returns the contact | |
| 57 | - | ||
| 58 | - | ### MNW Cloud Sync (Paid Feature) | |
| 59 | - | ||
| 60 | - | - [ ] Without a sync subscription, sync gate UI shows "subscribe to sync" | |
| 61 | - | - [ ] Click subscribe → Stripe Checkout (test card `4242…`) | |
| 62 | - | - [ ] After checkout, sync activates automatically (poll completes) | |
| 63 | - | - [ ] Create task on device A — appears on device B within seconds | |
| 64 | - | - [ ] Edit task on device B — change propagates back to A | |
| 65 | - | - [ ] Sync indicator shows activity during sync | |
| 66 | - | - [ ] Conflict: edit same task on both devices offline, reconnect — last-write-wins, no data loss | |
| 67 | - | ||
| 68 | - | ### OTA Updater | |
| 69 | - | ||
| 70 | - | - [ ] App checks for updates on launch (or via menu) | |
| 71 | - | - [ ] When a newer signed release is published, update prompt appears | |
| 72 | - | - [ ] Apply update → app restarts on new version | |
| 73 | - | - [ ] Tauri signature verification rejects an unsigned artifact | |
| 74 | - | ||
| 75 | - | ### Keyring (Token Persistence) | |
| 76 | - | ||
| 77 | - | - [ ] Log in to sync, quit the app, relaunch — still logged in (token persisted in OS keyring) | |
| 78 | - | - [ ] macOS: token visible in Keychain Access | |
| 79 | - | - [ ] Linux: token visible in `secret-tool lookup` | |
| 80 | - | - [ ] Windows: token visible in Credential Manager | |
| 81 | - | ||
| 82 | - | ### Export / Import | |
| 83 | - | ||
| 84 | - | - [ ] Export data — file is non-empty and parseable | |
| 85 | - | - [ ] On fresh install, import the export — all data restored | |
| 86 | - | ||
| 87 | - | --- | |
| 88 | - | ||
| 89 | - | ## P1 — Core Features | |
| 90 | - | ||
| 91 | - | ### Email (IMAP / OAuth) | |
| 92 | - | ||
| 93 | - | - [ ] Add IMAP account — sync triggers, inbox populates | |
| 94 | - | - [ ] OAuth providers (Gmail, etc.) — auth flow completes, tokens persist | |
| 95 | - | - [ ] Mark email read/unread persists across sync | |
| 96 | - | - [ ] Archive an email | |
| 97 | - | - [ ] Link email to a project; project detail shows the link | |
| 98 | - | ||
| 99 | - | ### Contacts (extended) | |
| 100 | - | ||
| 101 | - | - [ ] Custom fields save and reload | |
| 102 | - | - [ ] Bulk operations (multi-select, tag/delete) work | |
| 103 | - | ||
| 104 | - | ### Search | |
| 105 | - | ||
| 106 | - | - [ ] Global search returns matches across tasks, contacts, events | |
| 107 | - | - [ ] Search is responsive (<200ms on typical dataset) | |
| 108 | - | ||
| 109 | - | --- | |
| 110 | - | ||
| 111 | - | ## P2 — Edge Cases & Platform | |
| 112 | - | ||
| 113 | - | ### Cross-Platform | |
| 114 | - | ||
| 115 | - | - [ ] macOS: Keychain access works; no Gatekeeper warnings on a signed build | |
| 116 | - | - [ ] Linux: Secret Service available; AppImage launches without missing libs | |
| 117 | - | - [ ] Windows: Credential Manager works; .msi installs and uninstalls cleanly | |
| 118 | - | ||
| 119 | - | ### Mobile (iOS + Android, pre-TestFlight/Play submission) | |
| 120 | - | ||
| 121 | - | Polish landed 2026-05-16 in `src-tauri/frontend/css/styles.css`, `js/mobile.js`, `js/app.js`, `js/virtual-scroller.js`, `js/components.js`, `js/navigation.js`. Verify on real devices before submission. | |
| 122 | - | ||
| 123 | - | - [ ] **Safe areas — notched portrait**: tab bar bottom edge clears the home indicator; header isn't clipped by notch | |
| 124 | - | - [ ] **Safe areas — landscape**: content not clipped on notched side; tab bar buttons aren't under the notch | |
| 125 | - | - [ ] **Timer widget**: starts a timer, switches to a tab where the timer widget shows — widget sits above tab bar (not behind it) on notched iPhones | |
| 126 | - | - [ ] **Keyboard scroll**: open a modal with a form (e.g. New Task), tap a field near the bottom — field scrolls into view above the keyboard | |
| 127 | - | - [ ] **Background/foreground**: open app, switch away for >30s, return — view refreshes, sync indicator updates, no stale "current time" line on day plan | |
| 128 | - | - [ ] **Virtual scroller**: scroll through a long task or email list — smooth momentum, no jank or flashing rows | |
| 129 | - | - [ ] **VoiceOver (iOS) / TalkBack (Android)**: tab bar reads as tablist with selected state; "More" button announces expanded/collapsed; action sheet announces as modal dialog; focus returns to trigger on close | |
| 130 | - | - [ ] **Escape key (Bluetooth keyboard)**: closes the action sheet | |
| 131 | - | - [ ] **Long-press menu**: long-press a task row opens the action sheet; cancel returns to list | |
| 132 | - | ||
| 133 | - | ### Robustness | |
| 134 | - | ||
| 135 | - | - [ ] Quit during sync — relaunch resumes, no data corruption | |
| 136 | - | - [ ] Disk full simulated — graceful error, no crash | |
| 137 | - | - [ ] Very long task descriptions (10k chars) — rendered or truncated, no crash | |
| 138 | - | ||
| 139 | - | --- | |
| 140 | - | ||
| 141 | - | ## Sign-Off | |
| 142 | - | ||
| 143 | - | | Field | Value | | |
| 144 | - | |-------|-------| | |
| 145 | - | | Date | | | |
| 146 | - | | Tester | | | |
| 147 | - | | Version | | | |
| 148 | - | | Platform(s) | macOS / Linux / Windows | | |
| 149 | - | | P0 result | pass / fail | | |
| 150 | - | | P1 result | pass / fail | | |
| 151 | - | | P2 result | pass / fail / skipped | | |
| 152 | - | | Notes | | |
| @@ -1,327 +0,0 @@ | |||
| 1 | - | # Schema — GoingsOn | |
| 2 | - | ||
| 3 | - | SQLite database. 38 migrations in `migrations/sqlite/`, applied via Rust app-level migration runner. PRAGMA foreign_keys=ON enforced. | |
| 4 | - | ||
| 5 | - | ## Table Map | |
| 6 | - | ||
| 7 | - | | Domain | Tables | Purpose | | |
| 8 | - | |--------|--------|---------| | |
| 9 | - | | Core | 5 | Projects, tasks, annotations, events, emails | | |
| 10 | - | | Auth | 1 | Local user accounts | | |
| 11 | - | | Email Config | 1 | IMAP/JMAP account credentials | | |
| 12 | - | | Subtasks | 1 | Task checklists with optional task links | | |
| 13 | - | | Contacts | 5 | People + emails, phones, social, custom fields | | |
| 14 | - | | Planning | 3 | Weekly reviews, monthly goals, monthly reflections | | |
| 15 | - | | Project Mgmt | 1 | Milestones | | |
| 16 | - | | Time Tracking | 1 | Pomodoro-style sessions | | |
| 17 | - | | Attachments | 1 | File attachments (blob-hashed) | | |
| 18 | - | | Settings | 4 | LLM, backup, saved views, user config | | |
| 19 | - | | External Sync | 1 | Provider connections (Google, Apple, Microsoft) | | |
| 20 | - | | Search | 5 | FTS5 virtual tables | | |
| 21 | - | | SyncKit | 2 | Changelog + state | | |
| 22 | - | ||
| 23 | - | --- | |
| 24 | - | ||
| 25 | - | ## Core Tables | |
| 26 | - | ||
| 27 | - | ### projects | |
| 28 | - | ||
| 29 | - | | Column | Type | Notes | | |
| 30 | - | |--------|------|-------| | |
| 31 | - | | id | TEXT PK | UUID | | |
| 32 | - | | name | TEXT NOT NULL | | | |
| 33 | - | | description | TEXT | | | |
| 34 | - | | project_type | TEXT | 'personal', 'work', etc. | | |
| 35 | - | | status | TEXT | 'active', 'archived', 'completed' | | |
| 36 | - | | user_id | TEXT FK → users CASCADE | | | |
| 37 | - | | created_at | TEXT | ISO 8601 | | |
| 38 | - | ||
| 39 | - | **Indexes:** idx_projects_status, idx_projects_type, idx_projects_user. | |
| 40 | - | ||
| 41 | - | ### tasks | |
| 42 | - | Central entity. Rich status model with snooze, waiting-for, focus, recurrence. | |
| 43 | - | ||
| 44 | - | | Column | Type | Notes | | |
| 45 | - | |--------|------|-------| | |
| 46 | - | | id | TEXT PK | UUID | | |
| 47 | - | | description | TEXT NOT NULL | | | |
| 48 | - | | status | TEXT | 'pending', 'started', 'completed', 'cancelled' | | |
| 49 | - | | priority | TEXT | 'low', 'medium', 'high', 'urgent' | | |
| 50 | - | | due | TEXT | ISO 8601 datetime or date | | |
| 51 | - | | tags | TEXT | Comma-separated | | |
| 52 | - | | urgency | REAL | Computed priority score | | |
| 53 | - | | project_id | TEXT FK → projects SET NULL | | | |
| 54 | - | | user_id | TEXT FK → users CASCADE | | | |
| 55 | - | | recurrence | TEXT | RRULE-like pattern | | |
| 56 | - | | recurrence_parent_id | TEXT FK → tasks SET NULL | Self-ref for generated occurrences | | |
| 57 | - | | source_email_id | TEXT FK → emails SET NULL | Task created from email | | |
| 58 | - | | contact_id | TEXT FK → contacts SET NULL | Associated contact | | |
| 59 | - | | milestone_id | TEXT FK → milestones SET NULL | | | |
| 60 | - | | snoozed_until | TEXT | Hide until this datetime | | |
| 61 | - | | waiting_for_response | INTEGER | Boolean — blocked on someone | | |
| 62 | - | | waiting_since | TEXT | When waiting started | | |
| 63 | - | | is_focus | INTEGER | Boolean — pinned to focus | | |
| 64 | - | | completed_at | TEXT | When task was completed | | |
| 65 | - | | estimated_minutes | INTEGER | Time estimate | | |
| 66 | - | | actual_minutes | INTEGER | Actual time spent | | |
| 67 | - | ||
| 68 | - | **Indexes:** project, status, priority, due, urgency, user, snoozed, waiting, focus, contact, completed_at, (user_id, status), (user_id, status, due). | |
| 69 | - | ||
| 70 | - | ### annotations | |
| 71 | - | Notes/log entries on tasks. Timestamped. | |
| 72 | - | ||
| 73 | - | | Column | Type | Notes | | |
| 74 | - | |--------|------|-------| | |
| 75 | - | | id | TEXT PK | | | |
| 76 | - | | task_id | TEXT FK → tasks CASCADE | | | |
| 77 | - | | timestamp | TEXT | | | |
| 78 | - | | note | TEXT | | | |
| 79 | - | ||
| 80 | - | ### events | |
| 81 | - | Calendar events with recurrence and external sync support. | |
| 82 | - | ||
| 83 | - | | Column | Type | Notes | | |
| 84 | - | |--------|------|-------| | |
| 85 | - | | id | TEXT PK | | | |
| 86 | - | | title | TEXT NOT NULL | | | |
| 87 | - | | start_time | TEXT NOT NULL | ISO 8601 | | |
| 88 | - | | end_time | TEXT | | | |
| 89 | - | | location | TEXT | | | |
| 90 | - | | project_id | TEXT FK → projects SET NULL | | | |
| 91 | - | | user_id | TEXT FK → users CASCADE | | | |
| 92 | - | | linked_task_id | TEXT FK → tasks SET NULL | | | |
| 93 | - | | recurrence | TEXT | RRULE pattern | | |
| 94 | - | | recurrence_parent_id | TEXT FK → events SET NULL | Self-ref | | |
| 95 | - | | contact_id | TEXT FK → contacts SET NULL | | | |
| 96 | - | | block_type | TEXT | 'focus', 'meeting', 'break', etc. | | |
| 97 | - | | external_source | TEXT | 'google', 'apple', 'microsoft' | | |
| 98 | - | | external_id | TEXT | Provider's event ID | | |
| 99 | - | | is_read_only | INTEGER | Externally-managed events | | |
| 100 | - | ||
| 101 | - | **Indexes:** project, start_time, user, linked_task, recurrence_parent, contact, (external_source, external_id) UNIQUE WHERE NOT NULL. | |
| 102 | - | ||
| 103 | - | ### emails | |
| 104 | - | IMAP/JMAP synced emails. Full message storage with threading. | |
| 105 | - | ||
| 106 | - | | Column | Type | Notes | | |
| 107 | - | |--------|------|-------| | |
| 108 | - | | id | TEXT PK | | | |
| 109 | - | | from_address / to_address | TEXT | | | |
| 110 | - | | subject / body / html_body | TEXT | | | |
| 111 | - | | is_read / is_archived | INTEGER | Booleans | | |
| 112 | - | | message_id | TEXT | RFC 2822 Message-ID | | |
| 113 | - | | thread_id | TEXT | Grouped by In-Reply-To chain | | |
| 114 | - | | in_reply_to | TEXT | Parent message | | |
| 115 | - | | email_account_id | TEXT FK → email_accounts SET NULL | | | |
| 116 | - | | imap_uid | INTEGER | IMAP UID for sync | | |
| 117 | - | | source_folder | TEXT | IMAP folder name | | |
| 118 | - | | snoozed_until | TEXT | Email snooze | | |
| 119 | - | | waiting_for_response | INTEGER | Waiting flag | | |
| 120 | - | | attachment_meta | TEXT | JSON array of attachment info | | |
| 121 | - | | project_id | TEXT FK → projects SET NULL | | | |
| 122 | - | | user_id | TEXT FK → users CASCADE | | | |
| 123 | - | ||
| 124 | - | **Indexes:** project, is_read, received_at, user, is_archived, message_id, account, imap_uid, snoozed, waiting, thread, in_reply_to. | |
| 125 | - | ||
| 126 | - | --- | |
| 127 | - | ||
| 128 | - | ## Auth | |
| 129 | - | ||
| 130 | - | ### users | |
| 131 | - | Local user accounts. Single-user app but multi-user capable for shared setups. | |
| 132 | - | ||
| 133 | - | | Column | Type | Notes | | |
| 134 | - | |--------|------|-------| | |
| 135 | - | | id | TEXT PK | | | |
| 136 | - | | email | TEXT UNIQUE | | | |
| 137 | - | | password_hash | TEXT | | | |
| 138 | - | | display_name | TEXT | | | |
| 139 | - | | last_login_at | TEXT | | | |
| 140 | - | ||
| 141 | - | --- | |
| 142 | - | ||
| 143 | - | ## Email Configuration | |
| 144 | - | ||
| 145 | - | ### email_accounts | |
| 146 | - | IMAP/JMAP/OAuth2 account credentials. Supports IMAP, JMAP, and OAuth2 auth types. | |
| 147 | - | ||
| 148 | - | | Column | Type | Notes | | |
| 149 | - | |--------|------|-------| | |
| 150 | - | | id | TEXT PK | | | |
| 151 | - | | user_id | TEXT FK → users CASCADE | | | |
| 152 | - | | email_address | TEXT | UNIQUE(user_id, email_address) | | |
| 153 | - | | imap_server / imap_port | TEXT / INT | | | |
| 154 | - | | smtp_server / smtp_port | TEXT / INT | | | |
| 155 | - | | auth_type | TEXT | 'password', 'oauth2', 'jmap' | | |
| 156 | - | | oauth2_access_token / refresh_token / token_expires_at | TEXT | | | |
| 157 | - | | jmap_session_url / jmap_account_id | TEXT | | | |
| 158 | - | | archive_folder_name | TEXT | IMAP folder for archiving | | |
| 159 | - | | sync_interval_minutes | INT | Default 5 | | |
| 160 | - | | last_sync_at | TEXT | | | |
| 161 | - | ||
| 162 | - | --- | |
| 163 | - | ||
| 164 | - | ## Subtasks | |
| 165 | - | ||
| 166 | - | ### subtasks | |
| 167 | - | Checklist items within a task. Optional `linked_task_id` promotes a subtask to a full task. | |
| 168 | - | ||
| 169 | - | - **FK:** task_id → tasks CASCADE, linked_task_id → tasks SET NULL | |
| 170 | - | - **Key columns:** text, is_completed, position | |
| 171 | - | ||
| 172 | - | --- | |
| 173 | - | ||
| 174 | - | ## Contacts | |
| 175 | - | ||
| 176 | - | ### contacts | |
| 177 | - | People. Rich fields with external sync provenance. | |
| 178 | - | ||
| 179 | - | | Column | Type | Notes | | |
| 180 | - | |--------|------|-------| | |
| 181 | - | | id | TEXT PK | | | |
| 182 | - | | user_id | TEXT FK → users CASCADE | | | |
| 183 | - | | display_name | TEXT NOT NULL | | | |
| 184 | - | | nickname / company / title | TEXT | | | |
| 185 | - | | notes / tags | TEXT | | | |
| 186 | - | | birthday / timezone | TEXT | | | |
| 187 | - | | external_source | TEXT | 'google', 'apple', etc. | | |
| 188 | - | | external_id | TEXT | UNIQUE(external_source, external_id) WHERE NOT NULL | | |
| 189 | - | ||
| 190 | - | ### contact_emails / contact_phones / contact_social_handles / contact_custom_fields | |
| 191 | - | All have FK → contacts CASCADE. Support multiple entries per contact with `is_primary` flag (emails, phones) or platform/label categorization. | |
| 192 | - | ||
| 193 | - | --- | |
| 194 | - | ||
| 195 | - | ## Planning & Reviews | |
| 196 | - | ||
| 197 | - | ### weekly_reviews | |
| 198 | - | Weekly review sessions. UNIQUE(user_id, week_start_date). | |
| 199 | - | ||
| 200 | - | - **Key columns:** week_start_date, completed_at, notes, vacation_days | |
| 201 | - | ||
| 202 | - | ### monthly_goals | |
| 203 | - | Monthly goal tracking with position ordering and status. | |
| 204 | - | ||
| 205 | - | - **Indexed on:** (user_id, month) | |
| 206 | - | ||
| 207 | - | ### monthly_reflections | |
| 208 | - | Month-end retrospective. UNIQUE(user_id, month). | |
| 209 | - | ||
| 210 | - | - **Key columns:** highlight_text, change_text, completed_at | |
| 211 | - | ||
| 212 | - | --- | |
| 213 | - | ||
| 214 | - | ## Project Management | |
| 215 | - | ||
| 216 | - | ### milestones | |
| 217 | - | Project milestones with target dates and ordering. | |
| 218 | - | ||
| 219 | - | - **FK:** user_id → users CASCADE, project_id → projects CASCADE | |
| 220 | - | - **Key columns:** name, position, target_date, status | |
| 221 | - | ||
| 222 | - | --- | |
| 223 | - | ||
| 224 | - | ## Time Tracking | |
| 225 | - | ||
| 226 | - | ### time_sessions | |
| 227 | - | Pomodoro-style time tracking. `ended_at` NULL = active session. | |
| 228 | - | ||
| 229 | - | - **FK:** task_id → tasks CASCADE, user_id → users CASCADE | |
| 230 | - | - **Key columns:** started_at, ended_at, duration_minutes | |
| 231 | - | - **Index:** idx_time_sessions_active (WHERE ended_at IS NULL) | |
| 232 | - | ||
| 233 | - | --- | |
| 234 | - | ||
| 235 | - | ## Attachments | |
| 236 | - | ||
| 237 | - | ### attachments | |
| 238 | - | File attachments on tasks or projects. Content-hashed for dedup. | |
| 239 | - | ||
| 240 | - | - **FK:** user_id → users CASCADE, task_id → tasks CASCADE, project_id → projects CASCADE, source_email_id → emails SET NULL | |
| 241 | - | - **Key columns:** filename, file_size, mime_type, blob_hash | |
| 242 | - | - **Indexed on:** task, project, blob_hash | |
| 243 | - | ||
| 244 | - | --- | |
| 245 | - | ||
| 246 | - | ## Settings | |
| 247 | - | ||
| 248 | - | ### llm_settings | |
| 249 | - | LLM provider configuration. UNIQUE(user_id). | |
| 250 | - | ||
| 251 | - | ### llm_cache | |
| 252 | - | LLM response cache. UNIQUE(user_id, prompt_hash, context_date). | |
| 253 | - | ||
| 254 | - | ### saved_views | |
| 255 | - | Saved filter/sort configurations (pinnable). | |
| 256 | - | ||
| 257 | - | ### backup_settings | |
| 258 | - | Auto-backup configuration. UNIQUE(user_id). | |
| 259 | - | ||
| 260 | - | --- | |
| 261 | - | ||
| 262 | - | ## External Sync Accounts | |
| 263 | - | ||
| 264 | - | ### sync_accounts | |
| 265 | - | Provider connections for calendar/contact sync (Google, Apple, Microsoft). | |
| 266 | - | ||
| 267 | - | - **FK:** user_id → users CASCADE | |
| 268 | - | - **Key columns:** provider, account_name, email, sync_calendars, sync_contacts, calendar_ids (JSON), sync_interval_minutes, enabled | |
| 269 | - | - **Sync triggers:** INSERT/UPDATE/DELETE → sync_changelog | |
| 270 | - | ||
| 271 | - | --- | |
| 272 | - | ||
| 273 | - | ## Full-Text Search | |
| 274 | - | ||
| 275 | - | Five FTS5 virtual tables for instant search: | |
| 276 | - | ||
| 277 | - | | Table | Indexed Columns | | |
| 278 | - | |-------|----------------| | |
| 279 | - | | tasks_fts | description, tags | | |
| 280 | - | | emails_fts | subject, body, from_address, to_address | | |
| 281 | - | | projects_fts | name, description | | |
| 282 | - | | events_fts | title, description, location | | |
| 283 | - | | contacts_fts | display_name, nickname, company, notes, tags | | |
| 284 | - | ||
| 285 | - | All use external content mode with triggers to keep FTS in sync on INSERT/UPDATE/DELETE. | |
| 286 | - | ||
| 287 | - | --- | |
| 288 | - | ||
| 289 | - | ## SyncKit Infrastructure | |
| 290 | - | ||
| 291 | - | ### sync_state | |
| 292 | - | Key-value store for sync configuration: | |
| 293 | - | - `device_id` — unique device identifier | |
| 294 | - | - `pull_cursor` — last-pulled sequence number | |
| 295 | - | - `auto_sync_enabled` / `sync_interval_minutes` — sync settings | |
| 296 | - | - `applying_remote` — flag to suppress changelog triggers during pull | |
| 297 | - | - `last_sync_at` / `initial_snapshot_done` — sync progress | |
| 298 | - | ||
| 299 | - | ### sync_changelog | |
| 300 | - | Local change log. Every synced table has INSERT/UPDATE/DELETE triggers that write here (when `applying_remote` != '1'). | |
| 301 | - | ||
| 302 | - | | Column | Type | Notes | | |
| 303 | - | |--------|------|-------| | |
| 304 | - | | id | INTEGER PK | | | |
| 305 | - | | table_name | TEXT | | | |
| 306 | - | | op | TEXT | 'INSERT', 'UPDATE', 'DELETE' | | |
| 307 | - | | row_id | TEXT | | | |
| 308 | - | | timestamp | TEXT | | | |
| 309 | - | | data | TEXT | Full row as JSON | | |
| 310 | - | | pushed | INTEGER | 0 = unpushed | | |
| 311 | - | ||
| 312 | - | **Index:** idx_sync_changelog_unpushed (WHERE pushed = 0). | |
| 313 | - | ||
| 314 | - | **Synced tables:** projects, tasks, annotations, events, emails, subtasks, contacts, contact_emails, contact_phones, contact_social_handles, contact_custom_fields, milestones, weekly_reviews, monthly_goals, monthly_reflections, time_sessions, attachments, sync_accounts, saved_views. | |
| 315 | - | ||
| 316 | - | --- | |
| 317 | - | ||
| 318 | - | ## Cascade Rules | |
| 319 | - | ||
| 320 | - | - **CASCADE:** All user_id FKs, task_id on annotations/subtasks, contact_id on sub-tables | |
| 321 | - | - **SET NULL:** project_id on tasks/events (task survives project deletion), source_email_id on tasks/attachments, linked_task_id on subtasks/events | |
| 322 | - | ||
| 323 | - | ## Key Paths | |
| 324 | - | ||
| 325 | - | - `migrations/sqlite/` — all 38 migration files | |
| 326 | - | - `crates/db-sqlite/src/` — repository implementations | |
| 327 | - | - `crates/db-sqlite/tests/` — integration tests (14 repo test files) |
| @@ -1,75 +0,0 @@ | |||
| 1 | - | # Smoke Test Checklist — GoingsOn | |
| 2 | - | ||
| 3 | - | Pre-release manual verification. Run after building a new version. | |
| 4 | - | ||
| 5 | - | ## Launch & Basics | |
| 6 | - | ||
| 7 | - | - [ ] App launches without error | |
| 8 | - | - [ ] Window appears at expected size | |
| 9 | - | - [ ] No console errors on startup (check Tauri dev tools if dev build) | |
| 10 | - | ||
| 11 | - | ## Theme System | |
| 12 | - | ||
| 13 | - | - [ ] Open theme picker | |
| 14 | - | - [ ] Switch to a different theme — colors update immediately | |
| 15 | - | - [ ] Switch back to default theme | |
| 16 | - | - [ ] Import a custom `.toml` theme file | |
| 17 | - | ||
| 18 | - | ## Tasks | |
| 19 | - | ||
| 20 | - | - [ ] Create a new task (description, priority, tags) | |
| 21 | - | - [ ] Start the task (status → Started) | |
| 22 | - | - [ ] Complete the task (status → Completed) | |
| 23 | - | - [ ] Snooze a task — verify it disappears from active list | |
| 24 | - | - [ ] Un-snooze or wait for snooze to expire — task reappears | |
| 25 | - | - [ ] Create a recurring task, complete it — next occurrence generates | |
| 26 | - | - [ ] Delete a task | |
| 27 | - | ||
| 28 | - | ## Projects | |
| 29 | - | ||
| 30 | - | - [ ] Create a new project | |
| 31 | - | - [ ] Assign a task to the project | |
| 32 | - | - [ ] View project detail — task appears | |
| 33 | - | - [ ] Delete the project | |
| 34 | - | ||
| 35 | - | ## Events & Calendar | |
| 36 | - | ||
| 37 | - | - [ ] Create an event with date/time | |
| 38 | - | - [ ] Event appears in calendar view | |
| 39 | - | - [ ] Edit event, verify changes persist | |
| 40 | - | - [ ] Delete event | |
| 41 | - | ||
| 42 | - | ## Contacts | |
| 43 | - | ||
| 44 | - | - [ ] Create a contact (name, email, phone) | |
| 45 | - | - [ ] Add custom fields | |
| 46 | - | - [ ] Search for the contact by name | |
| 47 | - | - [ ] Delete the contact | |
| 48 | - | ||
| 49 | - | ## Email (if IMAP configured) | |
| 50 | - | ||
| 51 | - | - [ ] Add email account (IMAP credentials or OAuth) | |
| 52 | - | - [ ] Sync triggers — emails appear in inbox | |
| 53 | - | - [ ] Mark email read/unread | |
| 54 | - | - [ ] Archive an email | |
| 55 | - | - [ ] Link email to a project | |
| 56 | - | ||
| 57 | - | ## Sync | |
| 58 | - | ||
| 59 | - | - [ ] Log in to sync (MNW account) | |
| 60 | - | - [ ] Create a task on this device | |
| 61 | - | - [ ] Verify task appears on another synced device (or check via API) | |
| 62 | - | - [ ] Edit on the other device — changes sync back | |
| 63 | - | - [ ] Verify sync indicator shows activity | |
| 64 | - | ||
| 65 | - | ## Export / Import | |
| 66 | - | ||
| 67 | - | - [ ] Export data (backup) | |
| 68 | - | - [ ] Verify export file is non-empty and valid | |
| 69 | - | - [ ] Import a previously exported backup (on a fresh install if possible) | |
| 70 | - | ||
| 71 | - | ## Cross-Platform (if building for multiple platforms) | |
| 72 | - | ||
| 73 | - | - [ ] macOS: app opens, Keychain access works (for OAuth tokens) | |
| 74 | - | - [ ] Linux: app opens, Secret Service integration works | |
| 75 | - | - [ ] Windows: app opens, Credential Manager integration works |
| @@ -1,145 +0,0 @@ | |||
| 1 | - | # Test Plan — GoingsOn | |
| 2 | - | ||
| 3 | - | ## Overview | |
| 4 | - | ||
| 5 | - | ~700 tests total: 304 Rust (unit + integration) + 48 JavaScript + ~350 in workspace crates. | |
| 6 | - | ||
| 7 | - | ## Test Architecture | |
| 8 | - | ||
| 9 | - | **Rust tests:** Inline `#[cfg(test)]` modules + integration tests in `crates/db-sqlite/tests/`. All use in-memory SQLite. | |
| 10 | - | ||
| 11 | - | **JavaScript tests:** Custom lightweight runner (`frontend/js/tests/run.js`). No npm dependencies — runs with plain Node.js. | |
| 12 | - | ||
| 13 | - | **DB pattern:** `Database::open_in_memory()` creates a fresh SQLite database with all migrations applied. Each test is fully isolated. | |
| 14 | - | ||
| 15 | - | ## Running Tests | |
| 16 | - | ||
| 17 | - | ```bash | |
| 18 | - | # All Rust tests | |
| 19 | - | cargo test | |
| 20 | - | ||
| 21 | - | # JS tests | |
| 22 | - | node src-tauri/frontend/js/tests/run.js | |
| 23 | - | ||
| 24 | - | # Specific crate | |
| 25 | - | cargo test -p goingson-db-sqlite | |
| 26 | - | cargo test -p goingson-core | |
| 27 | - | ||
| 28 | - | # Specific domain | |
| 29 | - | cargo test task_repo | |
| 30 | - | cargo test sync_service | |
| 31 | - | cargo test oauth | |
| 32 | - | ``` | |
| 33 | - | ||
| 34 | - | ## What's Covered | |
| 35 | - | ||
| 36 | - | ### Repository Integration Tests (`crates/db-sqlite/tests/`) | |
| 37 | - | ||
| 38 | - | 14 test files covering all SQLite repository implementations: | |
| 39 | - | ||
| 40 | - | | File | Domain | Key Tests | | |
| 41 | - | |------|--------|-----------| | |
| 42 | - | | `task_repo_tests.rs` | Tasks | CRUD, status transitions (Pending/Started/Completed), snooze, recurrence, filtering, tags | | |
| 43 | - | | `project_repo_tests.rs` | Projects | CRUD, type/status management | | |
| 44 | - | | `email_repo_tests.rs` | Emails | CRUD, read/unread, archive, project linking | | |
| 45 | - | | `event_repo_tests.rs` | Events | CRUD, upcoming queries, linked tasks, recurrence | | |
| 46 | - | | `contact_repo_tests.rs` | Contacts | CRUD, emails, phones, custom fields, social handles, tags | | |
| 47 | - | | `email_account_repo_tests.rs` | Email accounts | Account CRUD, OAuth providers, credential handling | | |
| 48 | - | | `search_repo_tests.rs` | Search | Full-text search, filtering, ordering, pagination | | |
| 49 | - | | `annotation_repo_tests.rs` | Annotations | CRUD, retrieval | | |
| 50 | - | | `attachment_repo_tests.rs` | Attachments | CRUD, metadata | | |
| 51 | - | | `milestone_repo_tests.rs` | Milestones | CRUD, task association, completion tracking | | |
| 52 | - | | `saved_view_repo_tests.rs` | Saved views | CRUD, filter persistence | | |
| 53 | - | | `subtask_repo_tests.rs` | Subtasks | CRUD, completion, ordering | | |
| 54 | - | | `time_session_repo_tests.rs` | Time sessions | Start, stop, duration calculation | | |
| 55 | - | ||
| 56 | - | **Common test helper** (`crates/db-sqlite/tests/common/mod.rs`): | |
| 57 | - | ```rust | |
| 58 | - | pub async fn setup_test_db() -> SqlitePool // In-memory DB with migrations | |
| 59 | - | pub async fn create_test_user(pool: &SqlitePool) -> UserId | |
| 60 | - | ``` | |
| 61 | - | ||
| 62 | - | ### Command Tests (`src-tauri/src/commands/tests/`) | |
| 63 | - | ||
| 64 | - | 6 test files verifying Tauri command layer: | |
| 65 | - | ||
| 66 | - | | File | Commands Tested | | |
| 67 | - | |------|----------------| | |
| 68 | - | | `task_tests.rs` | Task CRUD, status transitions, filtering | | |
| 69 | - | | `project_tests.rs` | Project CRUD, listing | | |
| 70 | - | | `email_tests.rs` | Email CRUD, read/unread/archive | | |
| 71 | - | | `event_tests.rs` | Event CRUD, scheduling | | |
| 72 | - | | `contact_tests.rs` | Contact CRUD, custom fields | | |
| 73 | - | | `snooze_tests.rs` | Snooze options, time calculations | | |
| 74 | - | ||
| 75 | - | **Test utility** (`src-tauri/src/test_utils.rs`): | |
| 76 | - | ```rust | |
| 77 | - | pub async fn setup_test_state() -> (Arc<AppState>, UserId) | |
| 78 | - | ``` | |
| 79 | - | ||
| 80 | - | ### Inline Unit Tests | |
| 81 | - | ||
| 82 | - | | Module | Tests | Coverage | | |
| 83 | - | |--------|-------|----------| | |
| 84 | - | | `sync_service/tests.rs` | 21+ | FK ordering (upsert + delete), trigger suppression, change application, state management | | |
| 85 | - | | `oauth/provider.rs` | 33 | PKCE code challenge, state generation, authorization URL, token flow | | |
| 86 | - | | `oauth/callback_server.rs` | 8 | Callback handling, error cases | | |
| 87 | - | | `oauth/credentials.rs` | — | Credential storage patterns | | |
| 88 | - | | `db_watcher.rs` | — | File system watching | | |
| 89 | - | | `notifications.rs` | — | Notification logic | | |
| 90 | - | | `export/ics.rs` | — | iCalendar export | | |
| 91 | - | | `export/csv.rs` | — | CSV export | | |
| 92 | - | | `external_sync/vcard.rs` | — | vCard parsing | | |
| 93 | - | | `external_sync/ical.rs` | — | iCal import | | |
| 94 | - | ||
| 95 | - | ### JavaScript Tests (`frontend/js/tests/run.js`) | |
| 96 | - | ||
| 97 | - | 48 tests covering: | |
| 98 | - | - `AppStateManager` — pub/sub state management | |
| 99 | - | - `PaginationManager` — page navigation | |
| 100 | - | - `SelectionManager` — item selection | |
| 101 | - | - HTML escaping / XSS prevention | |
| 102 | - | - Utility functions | |
| 103 | - | ||
| 104 | - | ## What's Not Tested | |
| 105 | - | ||
| 106 | - | | Area | Reason | | |
| 107 | - | |------|--------| | |
| 108 | - | | IMAP client (`email/imap_client.rs`) | Requires live mailbox. Network I/O. | | |
| 109 | - | | JMAP client (`jmap/`) | Requires live JMAP server. Has inline tests for parsing. | | |
| 110 | - | | Backup/restore | File I/O, tested manually | | |
| 111 | - | | Theme loading | Runtime file loading, tested manually | | |
| 112 | - | | Day planning commands | Thin Tauri wrappers | | |
| 113 | - | | Window commands | Tauri window API wrappers | | |
| 114 | - | | Email sync scheduler | Background job scheduling | | |
| 115 | - | | Tauri initialization | App bootstrap | | |
| 116 | - | ||
| 117 | - | ## Adding New Tests | |
| 118 | - | ||
| 119 | - | ### Repository test | |
| 120 | - | ```rust | |
| 121 | - | #[tokio::test] | |
| 122 | - | async fn test_new_feature() { | |
| 123 | - | let pool = common::setup_test_db().await; | |
| 124 | - | let user_id = common::create_test_user(&pool).await; | |
| 125 | - | let repo = SqliteTaskRepository::new(pool); | |
| 126 | - | // Test logic | |
| 127 | - | } | |
| 128 | - | ``` | |
| 129 | - | ||
| 130 | - | ### Command test | |
| 131 | - | ```rust | |
| 132 | - | #[tokio::test] | |
| 133 | - | async fn test_new_command() { | |
| 134 | - | let (state, user_id) = setup_test_state().await; | |
| 135 | - | // Test through AppState | |
| 136 | - | } | |
| 137 | - | ``` | |
| 138 | - | ||
| 139 | - | ## Key Paths | |
| 140 | - | ||
| 141 | - | - `crates/db-sqlite/tests/` — Repository integration tests | |
| 142 | - | - `crates/db-sqlite/tests/common/mod.rs` — Test helpers | |
| 143 | - | - `src-tauri/src/commands/tests/` — Command tests | |
| 144 | - | - `src-tauri/src/test_utils.rs` — AppState test setup | |
| 145 | - | - `src-tauri/frontend/js/tests/` — JS test runner + tests |
| @@ -1,341 +0,0 @@ | |||
| 1 | - | # GoingsOn Todo | |
| 2 | - | ||
| 3 | - | v0.3.3 (iOS-only bump for icon fix; desktop still at 0.3.1). Audit grade A (Run 24). ~773 tests. ~74K LOC. Rust 2024 edition. | |
| 4 | - | ||
| 5 | - | Completed items: [todo_done.md](./todo_done.md) | |
| 6 | - | ||
| 7 | - | --- | |
| 8 | - | ||
| 9 | - | ## Launch (Locked Scope, 2026-05-16) | |
| 10 | - | ||
| 11 | - | Public-launch blockers. Everything else in this file is post-launch. Do not promote items into this section without explicit user decision. | |
| 12 | - | ||
| 13 | - | ### Core | |
| 14 | - | - [ ] Test full checkout flow against live Stripe (subscribe → webhook → sync gate passes) | |
| 15 | - | - [x] Test full sync flow against live MNW server — working (2026-05-11, API key fixed) | |
| 16 | - | - [ ] OAuth provider registration: Fastmail (pending), Google (test), Microsoft (test) | |
| 17 | - | ||
| 18 | - | ### Desktop builds | |
| 19 | - | - [ ] Windows: `cargo tauri build` → verify `.msi`, code-sign with Authenticode, test on Windows | |
| 20 | - | - [ ] Linux: AppImage (x86_64 + aarch64) | |
| 21 | - | ||
| 22 | - | ### Mobile (iOS TestFlight Internal only — external + Android deferred post-launch) | |
| 23 | - | See `todo_mobile.md` for full breakdown. Launch requires: | |
| 24 | - | - [x] iOS TestFlight upload working — v0.3.3 delivered 2026-05-17 (icon fix; v0.3.1 had Tauri default logo) | |
| 25 | - | - [x] iOS internal testing: phone Apple ID invited + added to TestFlight Internal group, build installed on own device (2026-05-19) | |
| 26 | - | - [ ] Physical device testing (iOS) — run mobile P0+P2 checklist in `docs/human_testing.md` | |
| 27 | - | - [ ] Safe area insets across device models, keyboard-doesn't-obscure-inputs, background/foreground transitions — verify on installed build | |
| 28 | - | - [ ] Resolve Missing Compliance prompt on v0.3.3 build (encryption answer — HTTPS-only exempt). **Plist key added 2026-05-18** in `gen/apple/goingson-desktop_iOS/Info.plist` (`ITSAppUsesNonExemptEncryption = false`); future uploads won't re-prompt. The currently-stuck v0.3.3 build still needs the answer set once via App Store Connect UI (TestFlight → build → Encryption: "Uses only exempt encryption"). | |
| 29 | - | ||
| 30 | - | Post-launch (out of launch scope): | |
| 31 | - | - [ ] (post-launch) Android emulator smoke (`cargo tauri android dev`) + CRUD verified on mobile WebView | |
| 32 | - | - [ ] (post-launch) Physical device testing (Android) | |
| 33 | - | - [ ] (post-launch) Add Privacy Policy page to GoingsOn project on MNW (URL: `https://makenot.work/p/goingson#section-privacy-policy`) | |
| 34 | - | - [ ] (post-launch) iOS External Testing: add build, fill Beta App Information, submit for Beta App Review, enable Public Link | |
| 35 | - | - [ ] (post-launch) Android release: Google Play Developer account ($25), release AAB, Play Console listing, submit | |
| 36 | - | ||
| 37 | - | Explicitly **out of launch scope** (defer until after public launch): | |
| 38 | - | - All of Sprint: Backup & Export (current backup works for primary data types) | |
| 39 | - | - All of Sprint: Email Hardening except OAuth registration above | |
| 40 | - | - All of Sprint: Events, Bulk Actions, Task Row Actions, Timer & Focus, Discoverability & Onboarding, Review Polish, Contacts Cleanup, Settings & Sync UX, Notifications & Reminders, Power User Features | |
| 41 | - | - Package managers (Homebrew/Flatpak/winget) — post-beta | |
| 42 | - | - Mobile platform features (push notifications, haptics, share extension, widgets, biometric) | |
| 43 | - | - All Sprint: Data Validation (LOW-severity) | |
| 44 | - | ||
| 45 | - | --- | |
| 46 | - | ||
| 47 | - | ## UX audit sweep (phased, post-launch) | |
| 48 | - | ||
| 49 | - | Audit complete 2026-05-19/20. **124 findings** across 6 surfaces, **4 charter rules** promoted, lint guards enforce 7 rules clean. See `docs/ux-audit/phase-7.md` for the roll-up; per-phase reports at `docs/ux-audit/phase-{0..7}.md`; charter at `docs/design-system.md`. Audit phases archived to `todo_done.md`. | |
| 50 | - | ||
| 51 | - | ### Audit backlog execution (Tier 1–6, per `phase-7.md` Part C) | |
| 52 | - | ||
| 53 | - | - [x] **Tier 1 — safety nets** (5/5). Bulk-undo across tasks/emails/contacts; selection clears on filter change; send-with-delay (undo-send); attachment-size warning at 25 MB; recurring-event scope confirm. | |
| 54 | - | - [x] **Tier 2 — trust-state surfaces** (4/4). Sync indicator label + state-by-color-alone fix; About / version in Settings; pagination + total counts; Account section now surfaces "Signed in as {email} ({username})" inside Cloud Sync via `sync_account_info`. | |
| 55 | - | - [x] **Tier 3 — mobile correctness** (5/5). Hide Kanban on touch; swipe peek-labels; bottom-nav long-press → pill picker; iOS meta tags + theme-color sync; `window.prompt` killed. | |
| 56 | - | - [x] **Tier 4 — URL-mirrored filter state** (1/1). `js/query-state.js` helper; wired into tasks, emails, contacts. Closes Phase 1 #9 / Phase 2 #10 / Phase 3 #8 / Phase 5 search-persistence in one helper. | |
| 57 | - | - [x] **Tier 5 — feature gaps** (4/4). All-day events, event snooze, contact sub-collection edit, and event reminders all wired end-to-end. | |
| 58 | - | - [~] **Tier 6 — architecture** (2.17/3). Side-drawer task detail (`#task-detail-drawer`, J/K keyboard nav, row-active marker) and settings overlay (`#settings-overlay`, Esc-to-close, backdrop dismiss) both landed 2026-05-20. Compose unification staged out in `docs/ux-audit/compose-migration.md`; **stage 1 of 6 landed 2026-05-20** — new `js/compose-form.js` owns the SendEmailInput payload shape, attachment caps, and validation. Both modal (`emails.js`) and desktop window (`compose.html`) call into it; the `to` vs `toAddress` double-emit shim is gone. Stages 2-6 are independently shippable; one per release during soft launch. | |
| 59 | - | ||
| 60 | - | **Backend trio + frontend wirings landed 2026-05-20** (migration 049, +9 Tauri commands, 4 new tests, ~3 frontend surfaces): | |
| 61 | - | - [x] Event snooze: migration adds `snoozed_until` + rebuilt sync triggers. `snooze_event` / `unsnooze_event` / `list_snoozed_events` commands. `EventResponse` exposes `isSnoozed` + `snoozedUntil`. Event detail modal has Snooze / Unsnooze toggle + "Snoozed until …" line. `snooze.js` generalised to three item types via `ITEM_LABEL` / `apiFor()` / `reloadFor()` helpers. | |
| 62 | - | - [x] Contact sub-collection edit: `update_contact_{email,phone,social_handle,custom_field}` commands. `SUB_COLLECTIONS` map extended with `updateCommand` + `prefill`; `buildSubCollectionFormHtml(type, cid, editingId?)` routes add vs edit. Edit-contact modal rows now carry inline Edit + Remove buttons. Pre-existing reload race (`load()` not awaited before `open*()`) fixed inline. | |
| 63 | - | - [x] MNW account info: new `GET /api/v1/sync/account` (server) + `get_account_info()` (synckit-client) + `sync_account_info` Tauri command. Surfaced inline in Cloud Sync (chose not to add a separate sidebar item — the data only exists because of sync). | |
| 64 | - | ||
| 65 | - | **Event reminders landed 2026-05-20** (migration 050, scheduler in `notifications.rs`, 12 new tests): | |
| 66 | - | - [x] Schema: `reminder_offsets_seconds TEXT` JSON column on events + rebuilt sync triggers + `apply.rs` column list. | |
| 67 | - | - [x] Scheduler: `check_event_reminders` runs each 60s tick; tracks `(event_id, offset)` pairs; bootstrap-on-first-tick suppresses backfill spam after app restart; skips snoozed events. | |
| 68 | - | - [x] UI: `REMINDER_PRESETS` (At time / 5m / 15m / 30m / 1h / 1d before) checkbox group in new/edit event forms; "Reminders: …" line on event detail modal. | |
| 69 | - | - [x] `sanitize_reminder_offsets` strips negatives, dedupes, sorts, caps at 8. | |
| 70 | - | - Known limitations (post-launch follow-ups, not blockers): recurring events fire reminders only against the template's anchor `start_time`, not virtual instances; reminder fire state is in-memory (close the app around a fire time and you miss it). | |
| 71 | - | ||
| 72 | - | **Tier 5 follow-up worth doing if you ship more broadly:** | |
| 73 | - | - [ ] Snoozed-events list view (the `list_snoozed_events` endpoint exists; nothing renders it yet — parity with the snoozed-tasks list). | |
| 74 | - | ||
| 75 | - | ### Charter rules now enforced in code (not just docs) | |
| 76 | - | 1. No state-by-color-alone (sync indicator paired with label; swipe peek-labels carry semantic colors + text) | |
| 77 | - | 2. URL-mirrored filter state (tasks/emails/contacts) | |
| 78 | - | 3. Bulk-undo everywhere (7 call sites) | |
| 79 | - | 4. No native browser dialogs (`scripts/lint-frontend.sh` rule #7; `showPromptDialog` helper) | |
| 80 | - | ||
| 81 | - | ### Deferred deliberately | |
| 82 | - | - [ ] **Step 4 — `renderRow` primitive — DEFERRED.** Re-evaluate if a future phase identifies parallel-markup as the real friction; until then, per-kind row renderers are fine (they all consume canonical CSS classes). See `docs/ux-audit/remediation-plan.md`. | |
| 83 | - | ||
| 84 | - | Out of scope for the audit: smart-feature behavior (forbidden by Apps/CLAUDE.md), full WCAG audit, performance. Recommendations are post-launch work; nothing here blocks ship. | |
| 85 | - | ||
| 86 | - | --- | |
| 87 | - | ||
| 88 | - | ## Sprint: Backup & Export | |
| 89 | - | ||
| 90 | - | Backup system has both coverage and performance problems (Run 24 cross-cutting concern). | |
| 91 | - | ||
| 92 | - | - [ ] **[MEDIUM]** Add daily notes, milestones, time sessions, attachments to backup (requires `list_all` on DailyNote/Milestone/Attachment repos) | |
| 93 | - | - [ ] **[MEDIUM]** Fix backup memory usage — backup should stream or paginate (especially email bodies) | |
| 94 | - | - [ ] Backup settings: simplify to On/Off toggle, "Customize" link for frequency/retention | |
| 95 | - | ||
| 96 | - | ### Deferred | |
| 97 | - | - [ ] Add backup encryption (currently gzip only, no confidentiality) | |
| 98 | - | - [ ] Verify blob hash after download; add blob GC for orphaned files on attachment delete | |
| 99 | - | ||
| 100 | - | --- | |
| 101 | - | ||
| 102 | - | ## Sprint: Bulk Actions | |
| 103 | - | ||
| 104 | - | Both bulk ops and their error handling are broken (Run 24). | |
| 105 | - | ||
| 106 | - | - [ ] Add batch project linking — select multiple tasks, link to project in one action | |
| 107 | - | ||
| 108 | - | --- | |
| 109 | - | ||
| 110 | - | ## Sprint: Events | |
| 111 | - | ||
| 112 | - | State key bug + calendar feature gaps. | |
| 113 | - | ||
| 114 | - | - [ ] All-day event support — `isAllDay` flag, full-width strip above timeline | |
| 115 | - | - [ ] Events spanning midnight — handle start before / end after boundary | |
| 116 | - | - [ ] Month-level task grid view (currently events only) | |
| 117 | - | ||
| 118 | - | --- | |
| 119 | - | ||
| 120 | - | ## Sprint: Email Hardening | |
| 121 | - | ||
| 122 | - | Email is the riskiest area — data loss potential + OAuth friction. | |
| 123 | - | ||
| 124 | - | - [ ] Email sync progress indicator during first/full sync | |
| 125 | - | - [ ] OAuth: "Check your browser window" text with visible countdown | |
| 126 | - | - [ ] OAuth failure: actionable hint to retry or switch to IMAP | |
| 127 | - | - [ ] "Recommended" badge on OAuth buttons (vs IMAP) | |
| 128 | - | - [ ] Archive folder auto-detect or folder browser | |
| 129 | - | - [ ] Email label creation UI (read-only badges; backend supports labels) | |
| 130 | - | - [ ] Email folder/label filtering in list UI (backend supports it) | |
| 131 | - | - [ ] Mobile compose: add CC/BCC fields (desktop has them) | |
| 132 | - | ||
| 133 | - | ### Deferred | |
| 134 | - | - [ ] Add SMTP TLS warning when `use_tls=false` (Run 24 — `builder_dangerous()` path) | |
| 135 | - | - [ ] Add exponential backoff to email_sync_scheduler (chronic — unfixed since Run 19) | |
| 136 | - | ||
| 137 | - | --- | |
| 138 | - | ||
| 139 | - | ## Sprint: Data Validation | |
| 140 | - | ||
| 141 | - | Small correctness fixes from Run 24. | |
| 142 | - | ||
| 143 | - | - [ ] **[LOW]** Validate `estimated_minutes` and `log_manual_time` minutes — reject negative/zero | |
| 144 | - | - [ ] **[LOW]** Add `AND t.status != 'Deleted'` to FTS task search branch (`search_repo.rs:257-271`) | |
| 145 | - | ||
| 146 | - | --- | |
| 147 | - | ||
| 148 | - | ## Sprint: Task Row Actions | |
| 149 | - | ||
| 150 | - | Surface key actions directly on task rows instead of burying them in context menus. | |
| 151 | - | ||
| 152 | - | - [ ] Visible Snooze button on task rows (currently context menu / keyboard 's' only) | |
| 153 | - | - [ ] Visible Track Time button on Started task rows (currently tiny icon) | |
| 154 | - | - [ ] Surface Focus/Pomodoro entry point on task rows (buried in detail modal) | |
| 155 | - | - [ ] Show elapsed time directly in task row when timer is active | |
| 156 | - | - [ ] After email-to-task conversion, offer "Start Timer" in success toast | |
| 157 | - | ||
| 158 | - | --- | |
| 159 | - | ||
| 160 | - | ## Sprint: Timer & Focus | |
| 161 | - | ||
| 162 | - | Distinguish and surface the two time features. | |
| 163 | - | ||
| 164 | - | - [ ] Floating timer widget: colored border or animation for prominence | |
| 165 | - | - [ ] Distinguish time tracking vs focus timer in UI (two features, no differentiation) | |
| 166 | - | - [ ] Time tracking reports — per-project breakdown, estimated vs actual | |
| 167 | - | ||
| 168 | - | --- | |
| 169 | - | ||
| 170 | - | ## Sprint: Discoverability & Onboarding | |
| 171 | - | ||
| 172 | - | Help new users find what exists without reading docs. | |
| 173 | - | ||
| 174 | - | - [ ] Document context menus in ? overlay — right-click actions undiscoverable | |
| 175 | - | - [ ] Add "Getting Started" item under Help menu to re-trigger welcome tour | |
| 176 | - | - [ ] First-visit guided walkthrough for day planning | |
| 177 | - | - [ ] Standardize empty-state styling across all views | |
| 178 | - | - [ ] Project list empty state — "No projects yet" message with CTA | |
| 179 | - | - [ ] Dashboard empty sections: add icons + action buttons | |
| 180 | - | - [ ] Quick Add syntax: add "or use Quick Add syntax" link in standard New Task form | |
| 181 | - | - [ ] Surface advanced search syntax (`is:overdue`, `tag:work`) in filter bar and ? overlay | |
| 182 | - | - [ ] Command palette: inline suggestions when typing `is:`, `tag:`, `type:`, `in:` prefixes | |
| 183 | - | ||
| 184 | - | --- | |
| 185 | - | ||
| 186 | - | ## Sprint: Review Polish | |
| 187 | - | ||
| 188 | - | Make the weekly/monthly review flow tighter. | |
| 189 | - | ||
| 190 | - | - [ ] Move reflection textareas above fold (or prompt before Complete) | |
| 191 | - | - [ ] Toggle active-state indication — clearer visual for active pane | |
| 192 | - | - [ ] Focus slot buttons: make visually distinct as selectors, not action buttons | |
| 193 | - | - [ ] Autosave indicator — subtle "Draft saved" feedback | |
| 194 | - | - [ ] "Complete Review" button: tooltip or confirmation explaining what it does | |
| 195 | - | ||
| 196 | - | --- | |
| 197 | - | ||
| 198 | - | ## Sprint: Contacts Cleanup | |
| 199 | - | ||
| 200 | - | The contacts module is functional but rough around the edges. | |
| 201 | - | ||
| 202 | - | - [ ] Contact form: "More options" fold (show Name + Notes, fold Nickname/Company/Title/Tags/Birthday/Timezone) | |
| 203 | - | - [ ] Contact bulk tag: replace `window.prompt()` with proper modal + tag autocomplete | |
| 204 | - | - [ ] Contact duplicate detection — check by email, offer merge on import | |
| 205 | - | ||
| 206 | - | --- | |
| 207 | - | ||
| 208 | - | ## Sprint: Settings & Sync UX | |
| 209 | - | ||
| 210 | - | Small polish items in settings and sync status. | |
| 211 | - | ||
| 212 | - | - [ ] Brief descriptions on each settings section in sidebar | |
| 213 | - | - [ ] Sync indicator: expand to show "Syncing..." / "Sync error" on hover | |
| 214 | - | - [ ] Add "What's New" dialog after OTA updates — surface CHANGELOG.md in-app | |
| 215 | - | ||
| 216 | - | --- | |
| 217 | - | ||
| 218 | - | ## Sprint: Notifications & Reminders | |
| 219 | - | ||
| 220 | - | Shared infrastructure — OS notification API, scheduler, settings UI. | |
| 221 | - | ||
| 222 | - | - [ ] Event reminders — OS notifications before events (5/10/15/30 min configurable) | |
| 223 | - | - [ ] Task due-date reminders — notification when deadlines approach | |
| 224 | - | - [ ] Notification settings UI — per-type enable/disable, quiet hours, sound toggle | |
| 225 | - | ||
| 226 | - | --- | |
| 227 | - | ||
| 228 | - | ## Sprint: Power User Features | |
| 229 | - | ||
| 230 | - | For intermediate-to-advanced users who want more control. | |
| 231 | - | ||
| 232 | - | - [ ] Quick Add ('q'): parse due/project/tag from description text | |
| 233 | - | - [ ] Saved searches / smart filters — persist filter combinations across sessions | |
| 234 | - | - [ ] Task templates for repetitive multi-step tasks | |
| 235 | - | - [ ] Workload guardrails in day planner — warn when scheduled hours exceed target | |
| 236 | - | ||
| 237 | - | --- | |
| 238 | - | ||
| 239 | - | ## Sprint: Desktop Distribution | |
| 240 | - | ||
| 241 | - | Ship on all platforms. | |
| 242 | - | ||
| 243 | - | ### Mobile Port (Tauri 2, pre-beta) | |
| 244 | - | - [ ] `cargo tauri android init` | |
| 245 | - | - [ ] Test all CRUD operations on mobile WebView | |
| 246 | - | - [ ] Physical device testing and polish | |
| 247 | - | - [ ] Touch gesture hints on first mobile use (long-press, swipe) | |
| 248 | - | - [ ] Mobile search entry point (Cmd+K has no touch equivalent) | |
| 249 | - | - See [todo_mobile.md](./todo_mobile.md) for full breakdown | |
| 250 | - | ||
| 251 | - | ### Windows Build | |
| 252 | - | - [ ] `cargo tauri build` on Windows — verify `.msi` installer | |
| 253 | - | - [ ] Test on Windows (VM or physical) | |
| 254 | - | - [ ] Code-sign with Authenticode certificate | |
| 255 | - | ||
| 256 | - | ### Linux Build | |
| 257 | - | - [ ] AppImage (x86_64 + aarch64), .deb, .rpm | |
| 258 | - | ||
| 259 | - | ### Package Managers (post-beta) | |
| 260 | - | - [ ] Homebrew Cask (macOS), Flatpak (Linux), winget (Windows) | |
| 261 | - | ||
| 262 | - | --- | |
| 263 | - | ||
| 264 | - | ## Post-Beta | |
| 265 | - | ||
| 266 | - | ### Plugin System (Rhai) | |
| 267 | - | - [ ] Starter import plugins: Todoist, Things 3, Apple Reminders, Google Tasks | |
| 268 | - | - [ ] Contact import plugins: vCard, Apple, Google, Microsoft | |
| 269 | - | - [ ] Calendar sync: Google, Apple/iCloud CalDAV, generic CalDAV, .ics import | |
| 270 | - | - [ ] Export adapters, custom commands, lifecycle hooks | |
| 271 | - | - [ ] Hot-reload AST cache, install from URL, update checking | |
| 272 | - | - [ ] Optimize plugin CSV parser to single-pass (chronic — unfixed since Run 19) | |
| 273 | - | ||
| 274 | - | ### Passkey Authentication | |
| 275 | - | - [ ] Local biometric unlock (Touch ID, Windows Hello) | |
| 276 | - | - [ ] Hardware security key support, cloud passkey auth, E2EE key protection | |
| 277 | - | ||
| 278 | - | ### Cloud Sync | |
| 279 | - | - [ ] Recovery key generation (printable paper backup for encryption password) | |
| 280 | - | - [ ] Stream blob uploads/downloads instead of loading into memory (`blob_sync.rs`) | |
| 281 | - | ||
| 282 | - | ### Contacts (larger) | |
| 283 | - | - [ ] Contact groups / distribution lists | |
| 284 | - | - [ ] vCard import plugin | |
| 285 | - | - [ ] Relationship types UI (schema exists, no frontend) | |
| 286 | - | - [ ] Contacts export to vCard | |
| 287 | - | ||
| 288 | - | ### Shared Code Extraction (Cross-Project) | |
| 289 | - | - [ ] Updater UI: extract updater.js from GO/BB into shared module | |
| 290 | - | - [ ] Theme loading: deduplicate TOML theme parser across GO/BB/AF | |
| 291 | - | - [ ] Rhai host functions: deduplicate plugin runtime setup | |
| 292 | - | - [ ] Saved queries: unify GO saved views, BB query feeds, AF smart folders | |
| 293 | - | - [ ] FTS5 query building: extract shared SQLite full-text search utilities | |
| 294 | - | ||
| 295 | - | ### Misc | |
| 296 | - | - [ ] Paginate beyond 500-item caps — tasks and emails silently cap | |
| 297 | - | - [ ] Add execution timeout to Rhai engine (in addition to operation limit) | |
| 298 | - | ||
| 299 | - | ### Dependency Pruning | |
| 300 | - | - [x] [dependency-prune] Remove `dirs` workspace dep — zero usage anywhere in the tree | |
| 301 | - | - [x] [dependency-prune] Remove `dotenvy` workspace dep — declared but unused | |
| 302 | - | - [ ] [dependency-prune] Replace `open::that` (4 call sites in commands/sync.rs, email.rs, attachment.rs) with `tauri-plugin-shell` / `AppHandle::shell().open(...)`; drop `open` crate — deferred: tauri-plugin-shell is desktop-only in this project, would require cfg-gating each call site or moving plugin to cross-platform; cost > benefit | |
| 303 | - | - [x] [dependency-prune] Replace `futures` umbrella in `email/imap_client.rs` with `futures-util` (default-features = false) | |
| 304 | - | - [x] [dependency-prune] Replace `parking_lot::{Mutex, RwLock}` in `state.rs` + `test_utils.rs` with `std::sync` equivalents (handle poison at `commands/email_sync.rs:70`) | |
| 305 | - | - [x] [dependency-prune] Bump `thiserror` from 1 to 2 in workspace deps | |
| 306 | - | ||
| 307 | - | --- | |
| 308 | - | ||
| 309 | - | ## Deferred | |
| 310 | - | ||
| 311 | - | - [ ] Co-working feature: E2E encrypted project sharing | |
| 312 | - | - [ ] Portability: publish synckit-client as a crate or use git submodule | |
| 313 | - | - [ ] Apple Watch app | |
| 314 | - | - [ ] Home screen widgets (iOS/Android) | |
| 315 | - | - [ ] Field-level merge / conflict resolution UI (sync) | |
| 316 | - | - [ ] Sync provider plugins (filesystem, WebDAV, S3-compatible) | |
| 317 | - | - [ ] Sync history/log viewer | |
| 318 | - | - [ ] Send Later (cloud-dependent) | |
| 319 | - | - [ ] Activity log | |
| 320 | - | ||
| 321 | - | --- | |
| 322 | - | ||
| 323 | - | ## Key Paths | |
| 324 | - | ||
| 325 | - | ``` | |
| 326 | - | crates/core/ Domain types, urgency calc, parser, repository traits | |
| 327 | - | crates/db-sqlite/ SQLite repository implementations | |
| 328 | - | crates/plugin-runtime/ Rhai plugin system | |
| 329 | - | plugins/ Bundled reference plugins | |
| 330 | - | src-tauri/ Desktop app (Tauri 2 + vanilla JS) | |
| 331 | - | src-tauri/src/commands/ Tauri command layer (24 modules) | |
| 332 | - | src-tauri/frontend/js/ Frontend JS (46 IIFE modules under GoingsOn namespace) | |
| 333 | - | src-tauri/frontend/css/ Styles (Skeubrute design system) | |
| 334 | - | migrations/sqlite/ SQLite migrations (44 files) | |
| 335 | - | docs/architecture.md Crate structure and data flow | |
| 336 | - | docs/audit_review.md Audit grades and action items | |
| 337 | - | docs/todo/todo_mobile.md Mobile port subtodo | |
| 338 | - | docs/todo/todo_done.md Completed items archive | |
| 339 | - | ``` | |
| 340 | - | ||
| 341 | - | - Domain purchased: goingson.app (Feb 2026) |
| @@ -1,269 +0,0 @@ | |||
| 1 | - | # GoingsOn — Completed Items | |
| 2 | - | ||
| 3 | - | Moved from todo.md to keep the active list focused. | |
| 4 | - | ||
| 5 | - | --- | |
| 6 | - | ||
| 7 | - | ## Sprint: Launch Blockers (partial) | |
| 8 | - | ||
| 9 | - | - [x] **[CRITICAL]** Register 24 missing commands in desktop `main.rs` `generate_handler![]` (attachments, drafts, labels, import, time logging, sync subscription) (2026-05-10) | |
| 10 | - | ||
| 11 | - | --- | |
| 12 | - | ||
| 13 | - | ## Sprint: Backup & Export (partial) | |
| 14 | - | ||
| 15 | - | - [x] **[SERIOUS]** Add contacts to `FullExport` backup + restore (2026-05-10). Daily notes, milestones, time sessions, attachments still missing (need `list_all` repo methods). | |
| 16 | - | ||
| 17 | - | --- | |
| 18 | - | ||
| 19 | - | ## Sprint: Bulk Actions (partial) | |
| 20 | - | ||
| 21 | - | - [x] **[SERIOUS]** Fix bulk project/priority update — fetch task first, merge field, send full input (2026-05-10) | |
| 22 | - | - [x] **[LOW]** Use `Promise.allSettled()` in bulk-actions.js; report partial success/failure counts (2026-05-10) | |
| 23 | - | - [x] **[LOW]** Fix raw error display in contacts.js:63,82 and events.js:70 (`+ err` → `getErrorMessage(err)`) (2026-05-10) | |
| 24 | - | ||
| 25 | - | --- | |
| 26 | - | ||
| 27 | - | ## Sprint: Events (partial) | |
| 28 | - | ||
| 29 | - | - [x] **[MEDIUM]** Fix event delete state key — `GoingsOn.state.events` → `upcomingEvents`/`pastEvents` (`events.js:471-489`) (2026-05-10) | |
| 30 | - | ||
| 31 | - | --- | |
| 32 | - | ||
| 33 | - | ## Sprint: Email Hardening (partial) | |
| 34 | - | ||
| 35 | - | - [x] Compose draft autosave to DB — debounced 2s on all fields, Cmd+S manual save, isSending guard (2026-05-07) | |
| 36 | - | ||
| 37 | - | --- | |
| 38 | - | ||
| 39 | - | ## Sprint: Security Hardening | |
| 40 | - | ||
| 41 | - | Run 24 findings — defense-in-depth items. | |
| 42 | - | ||
| 43 | - | - [x] **[MEDIUM]** Add CSP to `tauri.conf.json` (2026-05-10) | |
| 44 | - | ||
| 45 | - | --- | |
| 46 | - | ||
| 47 | - | ## Sprint: Settings & Sync UX (partial) | |
| 48 | - | ||
| 49 | - | - [x] Cloud sync indicator visible when configured — already works: `app.js` calls `refreshSyncIndicator()` on load, shows indicator when `status.configured` is true | |
| 50 | - | ||
| 51 | - | --- | |
| 52 | - | ||
| 53 | - | ## UX Audit Run 20 — Error Handling, Recurrence, Quick Wins, Day Planning (2026-05-07) | |
| 54 | - | ||
| 55 | - | - [x] Actionable error messages with per-code hints (VALIDATION_ERROR, AUTH_ERROR, PARSE_ERROR, etc.) | |
| 56 | - | - [x] Retry buttons in error toasts for all view load failures and sync | |
| 57 | - | - [x] Validation errors suggest expected format; form errors scroll to first invalid field | |
| 58 | - | - [x] Due date field validation with natural language hint on failure | |
| 59 | - | - [x] Recurrence preview text ("Repeats every 2 weeks on Mon, Wed, Fri") | |
| 60 | - | - [x] Monthly recurrence: day-of-month default, nth weekday behind "Specific weekday" toggle | |
| 61 | - | - [x] Tab aria-labels with full descriptions (Work: tasks and projects, etc.) | |
| 62 | - | - [x] Keyboard shortcut hints on buttons: `+ New Task [n]`, `Search [⌘K]`, project/event/contact `[n]` | |
| 63 | - | - [x] Rename Timer pill → "Focus Timer" with tooltip | |
| 64 | - | - [x] "More options" toggle changes text to "Less options" when expanded | |
| 65 | - | - [x] Day plan: hint moved above timeline as styled banner, cursor:grab on timeline slots | |
| 66 | - | - [x] Day plan sidebar: "Unscheduled Tasks" → "Tasks to Schedule" | |
| 67 | - | - [x] Plan/Review toggle tooltips on all three views | |
| 68 | - | - [x] Estimated Minutes field hint | |
| 69 | - | - [x] Fade-out animation on task completion (slide-right + opacity, 250ms) | |
| 70 | - | - [x] Auto-sync after OAuth account connection | |
| 71 | - | - [x] Drag-to-reschedule timeline items (mousedown drag, 15min snap, tasks + events) | |
| 72 | - | - [x] Human-readable time preview in painted event modal with duration | |
| 73 | - | - [x] Confirmation toast with time range on all schedule paths | |
| 74 | - | - [x] Add placeholder/hint text to advanced form fields | |
| 75 | - | ||
| 76 | - | --- | |
| 77 | - | ||
| 78 | - | ## Overflow Menus + Settings Page (2026-05-06) | |
| 79 | - | ||
| 80 | - | From usability audit discoverability and learnability findings. | |
| 81 | - | ||
| 82 | - | - [x] Three-dot overflow (kebab) menus on email rows, event rows, and project cards — shared `.kebab-btn` CSS class generalizes the existing `.task-kebab-btn` pattern. Emails/events show on hover; project cards always visible. Context menu actions (Focus Mode, Track Time, email-to-task, etc.) now discoverable without right-click. | |
| 83 | - | - [x] Settings page replaces settings modal — `#settings-view` with sidebar navigation (6 sections: Appearance, Notifications, Planning & Review, Plugins, Cloud Sync, Import & Export). Each section renders inline including Cloud Sync (all 4 states), plugin manager, and backup settings. No more close-modal-open-modal pattern. Back button returns to previous view. `previousView` tracking in navigation.js. Mobile responsive (sidebar becomes horizontal pills). Sync indicator button navigates to settings + auto-selects Cloud Sync section. | |
| 84 | - | ||
| 85 | - | --- | |
| 86 | - | ||
| 87 | - | ## UX Quick Wins Batch (2026-05-06) | |
| 88 | - | ||
| 89 | - | From usability audit (complexity, completeness, learnability, discoverability). All frontend JS/HTML changes. | |
| 90 | - | ||
| 91 | - | - [x] Search button in header — visible entry point for Cmd+K command palette | |
| 92 | - | - [x] Welcome modal email button fixed — opens email accounts modal (was broken, opened Settings which has no email section) | |
| 93 | - | - [x] `go-welcomed` localStorage timing — only set on button click, not on modal open; re-shows if accidentally dismissed | |
| 94 | - | - [x] Email empty state — shows "Add Account" when no accounts exist, "Compose" when accounts exist | |
| 95 | - | - [x] Stray backtick removed from emails.js empty state code | |
| 96 | - | - [x] Time tab pill labels — Day → Day Plan, Week → Weekly Review, Month → Monthly Review (navigation uses data-subview, not text) | |
| 97 | - | - [x] "Add Annotation" → "Add Note" in task action modal and context menu | |
| 98 | - | - [x] `n` shortcut works on Contacts view (was missing from `newItemForCurrentView`) | |
| 99 | - | - [x] "Create Task" promoted to top-level button in email reader (was buried in Actions dropdown) | |
| 100 | - | - [x] Paint-to-create defaults to "Link to Task" when unscheduled tasks exist (was always "Event") | |
| 101 | - | - [x] App subtitle: "Project Management" → "Personal Productivity" | |
| 102 | - | ||
| 103 | - | --- | |
| 104 | - | ||
| 105 | - | ## Command Palette (2026-05-05) | |
| 106 | - | ||
| 107 | - | - [x] Global search / command palette (Cmd+K) — `search.js` IIFE module with overlay, 150ms debounced search via existing FTS5 backend, arrow key + Enter navigation, mouse hover/click, type icons (task/project/email/event/contact), filter tag display, navigates to item on selection. Escape closes. Registered in `keyboard.js` (works from any context including text inputs). Added to shortcuts help overlay. | |
| 108 | - | ||
| 109 | - | --- | |
| 110 | - | ||
| 111 | - | ## Email Compose | |
| 112 | - | ||
| 113 | - | - [x] HTML email body conversion to readable markdown via pter (replaces hand-rolled strip_html) | |
| 114 | - | - [x] Reply / Reply-All — two distinct buttons, In-Reply-To/References headers, quoted body, thread joining | |
| 115 | - | - [x] Forward — Fwd: prefix, forwarded message header block, From account auto-select | |
| 116 | - | - [x] CC / BCC fields — togglable CC/BCC rows in compose, SMTP CC/BCC headers | |
| 117 | - | - [x] Multiple recipients — comma-separated To/CC/BCC, per-address SMTP validation | |
| 118 | - | ||
| 119 | - | --- | |
| 120 | - | ||
| 121 | - | ## Usability Audit Remediations (2026-05-02) | |
| 122 | - | ||
| 123 | - | Audit run: `/use-fuzz GoingsOn`. Overall grade: B+. | |
| 124 | - | ||
| 125 | - | ### Bugs | |
| 126 | - | - [x] Fix `g v` keyboard shortcut — maps to Weekly Review but overlay says "Events" (keyboard.js:42 vs :156) | |
| 127 | - | - [x] Fix mobile email reply prefill — openComposeModal now accepts prefill data for reply/forward | |
| 128 | - | ||
| 129 | - | ### Discoverability (B-) | |
| 130 | - | - [x] Add Events pill to Time tab navigation — full view currently keyboard/URL-only | |
| 131 | - | - [x] Add overflow/kebab icon on hover for item rows — surfaces context menu actions without right-click | |
| 132 | - | - [x] Add visible Quick Add button or input in Tasks header — `q` shortcut is invisible | |
| 133 | - | - [x] Add "Create Event" to email right-click context menu — parity with existing "Create Task" | |
| 134 | - | - [x] Add one-time onboarding hints: "Press ? for shortcuts" on first launch, "Shift-click to select range" on first bulk selection | |
| 135 | - | - [x] Add play/timer icon to task rows for started tasks — time tracking entry point is buried | |
| 136 | - | ||
| 137 | - | ### Learnability (B) | |
| 138 | - | - [x] Add hint text for day planner paint-to-create: "Drag across time slots to block time" | |
| 139 | - | - [x] Add introductory paragraph for first weekly review explaining the workflow | |
| 140 | - | - [x] Add introductory content for first monthly review (title hidden by CSS, no explanation) | |
| 141 | - | - [x] Add brief descriptions to differentiate Start Task / Schedule Time / Track Time / Focus Mode | |
| 142 | - | - [x] Add 2-3 sentence explanation in sync setup panel (what SyncKit is, what syncs, E2E encryption) | |
| 143 | - | - [x] IMAP auto-detect server settings from email domain (Gmail, Fastmail, Outlook, Yahoo, iCloud) | |
| 144 | - | - [x] Rename "Pri" column header to "Priority" (only abbreviated header in task table) | |
| 145 | - | ||
| 146 | - | ### Complexity (A-) | |
| 147 | - | - [x] Collapse task creation form: show 4 fields (Description, Project, Priority, Due Date) + "More options" toggle for Tags, Recurrence, Estimated Time, Contact, Milestone | |
| 148 | - | - [x] Group time-related context menu items into "Time" submenu or consolidate Track/Focus | |
| 149 | - | - [x] Add undo toast for keyboard task completion (`c` key) — matches delete undo pattern | |
| 150 | - | ||
| 151 | - | ### Feature Completeness (B-) | |
| 152 | - | - [x] Manual time entry — log time retroactively, not just live timer | |
| 153 | - | - [x] Bulk "Set Project" and "Set Priority" in task bulk actions bar | |
| 154 | - | - [x] Monthly review: add explicit "Complete Review" action (weekly has it, monthly does not) | |
| 155 | - | ||
| 156 | - | --- | |
| 157 | - | ||
| 158 | - | ## Code Fuzz Fixes (2026-05-03) | |
| 159 | - | ||
| 160 | - | Audit run: `/code-fuzz goingson`. 8 serious, 10 minor. 7/8 serious fixed, 7/10 minor fixed. | |
| 161 | - | ||
| 162 | - | ### Serious (fixed) | |
| 163 | - | - [x] JMAP token refresh writes empty string to DB instead of actual token (email_sync.rs:289) | |
| 164 | - | - [x] Sync `applying_remote` flag is global — fixed via WAL-isolated transaction on dedicated connection (pull.rs) | |
| 165 | - | - [x] `stop_timer` non-atomic across session + task tables — wrapped in transaction (time_session_repo.rs) | |
| 166 | - | - [x] `milestone reorder` non-atomic — wrapped in transaction (milestone_repo.rs) | |
| 167 | - | - [x] `start_timer` race condition — check+insert wrapped in transaction (time_session_repo.rs) | |
| 168 | - | - [x] Blob sync writes not atomic — tmp+rename pattern (blob_sync.rs) | |
| 169 | - | - [x] Synthetic email message-ID uses non-zero-padded hex — {:02x} (email_sync.rs) | |
| 170 | - | ||
| 171 | - | ### Minor (fixed) | |
| 172 | - | - [x] Plugin sandbox uses lexical path comparison — canonicalize() (plugin-runtime/api.rs) | |
| 173 | - | - [x] CSV formula injection: added `;`/`|` prefixes, tags sanitized individually (export/csv.rs) | |
| 174 | - | - [x] Day boundary at 23:59:59.000 misses sub-second events — use `< next_day_start` (weekly_review.rs, monthly_review.rs) | |
| 175 | - | - [x] Weekly review `event_count` only counts past events — now counts both sources (weekly_review.rs) | |
| 176 | - | - [x] Three unescaped `taskId` in inline handlers — added escAttr() (tasks-render.js) | |
| 177 | - | - [x] vCard unfold strips extra tabs from continuation lines — removed trim_start_matches (vcard.rs) | |
| 178 | - | ||
| 179 | - | --- | |
| 180 | - | ||
| 181 | - | ## Phase 5: File Attachments | |
| 182 | - | ||
| 183 | - | - [x] Sync tests: attachment in changelog, table_columns whitelist, UPSERT/DELETE ordering | |
| 184 | - | ||
| 185 | - | --- | |
| 186 | - | ||
| 187 | - | ## Usability Audit Remediations — Batch 2 (2026-05-02) | |
| 188 | - | ||
| 189 | - | ### Discoverability | |
| 190 | - | - [x] Surface hidden features in task detail modal — subtasks, annotations, focus mode, and time tracking are only accessible via right-click context menu; add visible buttons/sections in the task detail view | |
| 191 | - | - [x] Add `g`-prefix visual feedback — pressing `g` gives no indication a key sequence is active; show a brief "Go to..." overlay listing destinations | |
| 192 | - | - [x] Show keyboard shortcut hints on major buttons — e.g. "[q] Quick Add", "[n] New Task", "[?] Shortcuts" as title attributes or subtle inline labels | |
| 193 | - | - [x] Add quick-add syntax popover — show syntax help when user types `@`, `#`, or `+` in the quick-add field | |
| 194 | - | ||
| 195 | - | ### Learnability | |
| 196 | - | - [x] Enhance welcome flow with first-action guidance or "Load sample data" option | |
| 197 | - | - [x] Add frontend error message mapper — humanize backend error codes for toasts | |
| 198 | - | - [x] Add real-time date parse preview — show parsed date below Due Date input as user types (e.g. "next friday" → "Friday, May 8, 2026") | |
| 199 | - | - [x] Add tooltip/help text for domain-specific terms — "Snooze" ("hide until a chosen date"), "Milestone" ("group tasks into project phases"), "Recurrence" ("auto-create copy after completion") | |
| 200 | - | ||
| 201 | - | ### Complexity | |
| 202 | - | - [x] Use natural language date parsing for milestone target dates (currently requires YYYY-MM-DD) | |
| 203 | - | - [x] Simplify email account setup — make OAuth the hero path; hide IMAP server/port/TLS fields behind "Advanced" toggle; auto-detect from domain; move sync interval to post-setup settings | |
| 204 | - | - [x] Extend undo toast window from 5s to 15s — accidental deletions are irreversible if user misses the short toast | |
| 205 | - | ||
| 206 | - | --- | |
| 207 | - | ||
| 208 | - | ## Code Fuzz Fixes — Batch 2 (2026-05-03) | |
| 209 | - | ||
| 210 | - | ### Serious | |
| 211 | - | - [x] `create_initial_snapshot` called outside sync_lock — TOCTOU gap (commands/sync.rs:269-276) | |
| 212 | - | ||
| 213 | - | ### Minor | |
| 214 | - | - [x] iCal DST spring-forward gap falls back to UTC interpretation (ical.rs:129). Fixed: fall back to `.latest()` for spring-forward gaps. | |
| 215 | - | - [x] Blob files loaded entirely into memory for sync upload (blob_sync.rs:53-61). Non-issue: attachments capped at 50 MB (attachment.rs:79), uploaded sequentially (one at a time), so worst case is ~100 MB transient (plaintext + ciphertext). XChaCha20-Poly1305 AEAD requires full plaintext for sealing. | |
| 216 | - | - [x] Temp HTML files from "Open in Browser" never cleaned up (commands/email.rs:345). Fixed: delayed cleanup + startup sweep. | |
| 217 | - | - [x] Migration FK update failures silently swallowed (migrations.rs:74). Fixed: propagate error. | |
| 218 | - | ||
| 219 | - | --- | |
| 220 | - | ||
| 221 | - | ## Email Compose — Quick Wins (2026-05-04) | |
| 222 | - | ||
| 223 | - | - [x] Keyboard shortcuts — reply (r), forward (f), mark unread (u) from email list | |
| 224 | - | - [x] Quoted text collapse — "On ... wrote:" + > lines collapsed behind toggle | |
| 225 | - | - [x] Attachment download/open — parsed attachment_meta in response, open/save blob commands, attachment panel in reader | |
| 226 | - | ||
| 227 | - | ## Email Compose — Medium Features (2026-05-04) | |
| 228 | - | ||
| 229 | - | - [x] Contact autocomplete — typeahead for To/CC/BCC fields from contacts database, both compose window and modal | |
| 230 | - | - [x] Signatures — per-account email signature stored in DB (migration 041), auto-appended to compose, swaps on account change, syncs across devices | |
| 231 | - | - [x] Email search UI — search bar in email list using FTS5 backend, debounced, type:email filter | |
| 232 | - | ||
| 233 | - | ## Email Compose — Larger Features (2026-05-04) | |
| 234 | - | ||
| 235 | - | - [x] Drafts (real) — is_draft flag (migration 042), save/list/send draft commands, compose window re-open, drafts modal | |
| 236 | - | - [x] Attachment sending — MIME multipart via lettre, file picker in compose window + modal, multiple files | |
| 237 | - | - [x] Labels / folders — local labels (migration 043), folder/label filter dropdowns, move to folder (IMAP + local), label editing | |
| 238 | - | - [x] Notifications — per-account opt-in (migration 044), off by default, fires from auto-sync scheduler when new emails saved | |
| 239 | - | ||
| 240 | - | --- | |
| 241 | - | ||
| 242 | - | ## UX Audit sweep — Phase 0 through Phase 7 (2026-05-19/20) | |
| 243 | - | ||
| 244 | - | Multi-phase audit of the frontend. All artifacts under `docs/ux-audit/`; charter at `docs/design-system.md`; lint guards at `scripts/lint-frontend.sh`. | |
| 245 | - | ||
| 246 | - | **Phase 0 + remediation:** | |
| 247 | - | - [x] Phase 0 — design-system conformance audit. `docs/ux-audit/phase-0.md` + `docs/design-system.md`. | |
| 248 | - | - [x] Consolidation pre-plan. `docs/ux-audit/remediation-plan.md` — 10 steps. | |
| 249 | - | - [x] Step 1 — Toast styling moved into CSS. | |
| 250 | - | - [x] Step 2 — Updater/keycaps/var-fallback hex removed. | |
| 251 | - | - [x] Step 3 — `renderFormField` primitive + form-modal/settings/settings-sync/email-accounts migrated. | |
| 252 | - | - [x] Step 5 — Empty-state consolidation. | |
| 253 | - | - [x] Step 6 — Layout utilities + style-attr sweep; lint reports clean. | |
| 254 | - | - [x] Step 7 — `compose.html` embedded styles reconciled. | |
| 255 | - | - [x] Step 8 — Theme coverage sweep. `docs/ux-audit/theme-coverage.md`. | |
| 256 | - | - [x] Step 9 — `window.confirm()` calls removed. | |
| 257 | - | - [x] Step 10 — Lint guards at `scripts/lint-frontend.sh` (7 rules). | |
| 258 | - | - Step 4 (`renderRow`) deliberately deferred — kept active in todo.md. | |
| 259 | - | ||
| 260 | - | **Surface audits (124 total findings across 6 surfaces):** | |
| 261 | - | - [x] Phase 1 — Shell & navigation. 17 findings (3 critical). | |
| 262 | - | - [x] Phase 2 — Tasks surface. 18 findings (3 critical). | |
| 263 | - | - [x] Phase 3 — Compose & email. 21 findings (3 critical). | |
| 264 | - | - [x] Phase 4 — Events & calendar. 23 findings (4 critical). | |
| 265 | - | - [x] Phase 5 — Projects, contacts, settings. 23 findings (4 critical). | |
| 266 | - | - [x] Phase 6 — Mobile parity sweep. 22 findings (3 critical). Verdict: mobile is ~70 % parallel implementation, ~30 % CSS restyle. | |
| 267 | - | - [x] Phase 7 — Cross-cutting + theme conformance + roll-up. 4 meta-patterns promoted to charter rules (state-by-color-alone, URL-mirrored filter state, bulk-undo, native-dialogs-forbidden). Landed in this commit: `GoingsOn.ui.showPromptDialog`, `bulkTag` off `window.prompt`, `no-native-dialogs` lint rule, cross-cutting rules section in `design-system.md`. | |
| 268 | - | ||
| 269 | - | Recommendations are post-launch; nothing in the audit blocks ship. Tier 1–6 ship order in `phase-7.md` Part C. |
| @@ -1,125 +0,0 @@ | |||
| 1 | - | # GoingsOn - Mobile Port | |
| 2 | - | ||
| 3 | - | Done: Phases 1-7 (CSS, touch, navigation, views, build config, tab bar, distribution setup); touch-native UX rework (paint, reschedule, week carousel); polish (tap targets, scroll lock, modal cutoffs, icon, provider hints, segmented events view); iOS TestFlight internal install on phone (2026-05-19). Active: None. External TestFlight + Android deferred post-launch. | |
| 4 | - | ||
| 5 | - | --- | |
| 6 | - | ||
| 7 | - | ## Remaining | |
| 8 | - | ||
| 9 | - | ### Build & Test | |
| 10 | - | - [ ] (post-launch) Test on Android emulator (`cargo tauri android dev`) | |
| 11 | - | - [ ] All CRUD operations verified on mobile WebView (iOS — on installed TestFlight build) | |
| 12 | - | ||
| 13 | - | ### Polish | |
| 14 | - | - [ ] Physical device testing (iOS); (post-launch) Android | |
| 15 | - | - [x] Safe area insets on various device models (notched, non-notched) — 2026-05-16: timer-widget bottom now uses `calc(52px + env(safe-area-inset-bottom))`; body + fixed UI respect `safe-area-inset-left/right` for landscape. Verify on device. | |
| 16 | - | - [x] Virtual scroller performance on mobile — 2026-05-16: short-circuit re-renders when visible range unchanged; removed redundant O(N) walks per scroll tick. Verify on device with long lists. | |
| 17 | - | - [x] VoiceOver / TalkBack accessibility — 2026-05-16: `mobile-more-btn` gets `aria-expanded`/`aria-haspopup`/`aria-controls`; popover is `role="menu"` with `aria-hidden` toggle; action sheet now traps focus + restores on close + closes on Escape. Spot-check with VoiceOver. | |
| 18 | - | - [x] Keyboard doesn't obscure inputs in modals — 2026-05-16: `wireKeyboardScrollIntoView()` in `mobile.js` uses `visualViewport` to scroll focused inputs into view on keyboard show. Verify on device. | |
| 19 | - | - [x] Background/foreground transitions work correctly — 2026-05-16: `visibilitychange` handler in `app.js` invalidates caches and refreshes view/sync indicator/day-plan time when app was hidden >30s. Verify resume behavior on device. | |
| 20 | - | ||
| 21 | - | ### UX Rework (Touch-Native Interactions) | |
| 22 | - | Mobile UX audit 2026-05-17 — desktop interactions that don't translate. | |
| 23 | - | - [x] **Drag-to-paint event creation** — 2026-05-18: `day-planning-render.js` skips `onmousedown`/`onmouseenter` on touch. Tap opens picker with 30-min default; long-press opens picker with 1-hour default. Both snap to 30-min boundaries on touch to absorb finger imprecision. Wired in `wireTouchInteractions` in `day-planning.js`. | |
| 24 | - | - [x] **Drag-to-reschedule events** — 2026-05-18: `onmousedown` on `.timeline-item` is gated to non-touch. Tap opens the item; long-press opens an action sheet (Open / Reschedule / Unschedule for tasks; Edit time for events/blocks). Reschedule reuses `openScheduleTaskModal`. | |
| 25 | - | - [x] **Dense month/week calendar grids** — 2026-05-18: Month grid tap targets enlarged (64px min-height, larger date numbers); tap-to-expand behavior kept. Week view: single-day swipe carousel at `max-width: 600px` (`renderMobileDay` in `events-calendar.js`); arrows step by 1 day on mobile, 7 on desktop. | |
| 26 | - | ||
| 27 | - | ### UX Rework (additional, 2026-05-18) | |
| 28 | - | - [x] **Day plan timeline tap targets** — `--timeline-slot-h` CSS variable drives `.timeline-slot` height. Default 12px; 22px under `max-width: 600px` (88px/hour on mobile). JS reads it via `GoingsOn.dayPlanRender.getSlotHeight()` so positioning math stays in sync. | |
| 29 | - | - [x] **Horizontal scroll lock** — `html, body { overflow-x: hidden; touch-action: pan-y }` under mobile media query. `.pill-nav` opts back in to `pan-x` for its own scroller. | |
| 30 | - | - [x] **Modal/overlay cutoff behind tab bar** — `.modal-overlay`, `.bulk-actions-bar`, `.filter-bar.mobile-visible` all lifted to sit above the 52px mobile tab bar. Modal `max-height` capped to actual usable viewport. | |
| 31 | - | - [x] **App icon on iOS** — regenerated with `--ios-color "#E8F4F8"` then `#1B365D` so the icon background extends edge-to-edge under iOS's corner mask instead of looking "set-in". | |
| 32 | - | - [x] **Welcome modal mobile copy** — `app.js` `showWelcome` branches on `GoingsOn.touch.isTouchDevice`: step 1 says "tap the + tab", step 2 says "tap or long-press the timeline to schedule"; keyboard-shortcut hint hidden on touch. | |
| 33 | - | - [x] **Email provider hints** — `email-accounts.js` now shows a per-provider call-out beneath the email field once the domain is detected. Covers iCloud/me/mac (app-specific password + link), Fastmail, Gmail, Yahoo, AOL, Outlook (conditional), Proton (Bridge required). | |
| 34 | - | - [x] **Time tab subtab labels** — renamed to **Day / Week / Month / Timer / Events** in `index.html` (was Day Plan / Weekly Review / Monthly Review / Focus Timer / Events). Shared markup → applies to desktop and mobile. | |
| 35 | - | - [x] **Events view flattened** — removed the inner List/Month/Week toggle and the calendar grid containers. Events view is now a segmented flat list: Recurring (templates only, expandable, at top) → Upcoming → Past (collapsible). `events.js` `load()` splits the response into three buckets via `recurrence !== 'None' && !isRecurringInstance` filter; new `renderEventRow` `isRecurring` branch shows the recurrence pattern in place of the date column. | |
| 36 | - | - [x] **iOS missing commands** — `lib.rs` (mobile entry point) was missing several commands present in desktop `main.rs`. Added: `get_monthly_review`, `upsert_monthly_goal`, `update_monthly_goal_status`, `delete_monthly_goal`, `save_monthly_reflection`, `sync_get_tiers`, `list_themes`, `get_theme`, `get_custom_themes_dir`, `import_theme`, `export_theme`. Both handler lists must be kept in sync going forward. | |
| 37 | - | ||
| 38 | - | ### Platform Features (Future) | |
| 39 | - | - [ ] Push notifications via Tauri plugin | |
| 40 | - | - [ ] Haptic feedback for actions | |
| 41 | - | - [ ] Share extension (receive shared text as task) | |
| 42 | - | - [ ] Widgets (iOS 14+, Android) | |
| 43 | - | - [ ] Biometric unlock (Face ID, fingerprint) | |
| 44 | - | ||
| 45 | - | --- | |
| 46 | - | ||
| 47 | - | ## Release | |
| 48 | - | ||
| 49 | - | ### iOS (TestFlight) | |
| 50 | - | ```bash | |
| 51 | - | ./dist/release-ios.sh # full flow: build + export + upload | |
| 52 | - | ./dist/release-ios.sh --build-only # build archive only | |
| 53 | - | ./dist/release-ios.sh --upload-only # export + upload existing archive | |
| 54 | - | ``` | |
| 55 | - | - [x] Accept PLA at developer.apple.com/account | |
| 56 | - | - [x] Register App ID for `com.goingson.app` (Certificates, Identifiers & Profiles) | |
| 57 | - | - [x] App Store Connect: create app record (bundle `com.goingson.app`, SKU `goingson-1`, Apple ID `6759975645`) | |
| 58 | - | - [x] Upload v0.3.1 to TestFlight via `dist/release-ios.sh` (2026-05-12) | |
| 59 | - | - [x] v0.3.1 build processed (status: VALID) | |
| 60 | - | - [x] Answer encryption export-compliance prompt — set via API to `usesNonExemptEncryption: false` (exempt; standard HTTPS/system crypto) | |
| 61 | - | ||
| 62 | - | #### Internal testing (immediate, for own device) | |
| 63 | - | - [x] App Store Connect → Users and Access → invite phone's Apple ID (role: Developer) | |
| 64 | - | - [x] Accept invitation from phone's Apple ID | |
| 65 | - | - [x] TestFlight → Internal Testing → "+" group → add phone Apple ID → add build | |
| 66 | - | - [x] Install TestFlight on phone, accept invite, install build, smoke-test | |
| 67 | - | ||
| 68 | - | #### External testing (public link, slower first time) — POST-LAUNCH | |
| 69 | - | - [x] Privacy policy drafted at `docs/privacy-policy.md` | |
| 70 | - | - [ ] (post-launch) Add Privacy Policy page to GoingsOn project on MNW (dashboard → Settings → Pages); URL will be `https://makenot.work/p/goingson#section-privacy-policy`. Pages feature shipped in MNW v0.5.17. | |
| 71 | - | - [ ] (post-launch) TestFlight → External Testing → "+" group → add v0.3.1 build | |
| 72 | - | - [ ] (post-launch) Fill in Beta App Information: description, feedback email, test notes ("no login required for core features") | |
| 73 | - | - [ ] (post-launch) Submit for Beta App Review (first time: ~24-48h; subsequent builds: minutes via automated screening) | |
| 74 | - | - [ ] (post-launch) Once approved, enable Public Link | |
| 75 | - | - [ ] (post-launch) Push at least one build per quarter — TestFlight builds expire after 90 days | |
| 76 | - | ||
| 77 | - | ### Android — POST-LAUNCH | |
| 78 | - | ```bash | |
| 79 | - | ./dist/release-android.sh # release AAB | |
| 80 | - | ./dist/release-android.sh --apk # release APK | |
| 81 | - | ./dist/release-android.sh --debug # debug APK | |
| 82 | - | ``` | |
| 83 | - | - [ ] (post-launch) Google Play Developer account ($25 one-time) | |
| 84 | - | - [ ] (post-launch) Release AAB build + test | |
| 85 | - | - [ ] (post-launch) Play Console listing (title, description, screenshots) | |
| 86 | - | - [ ] (post-launch) Test on emulator + physical device | |
| 87 | - | - [ ] (post-launch) Play Store submission | |
| 88 | - | ||
| 89 | - | --- | |
| 90 | - | ||
| 91 | - | ## Key Files | |
| 92 | - | ||
| 93 | - | | File | Role | | |
| 94 | - | |------|------| | |
| 95 | - | | `src-tauri/frontend/css/styles.css` | All responsive CSS (~400 lines of mobile rules) | | |
| 96 | - | | `src-tauri/frontend/js/touch.js` | Gesture utilities (~300 lines) | | |
| 97 | - | | `src-tauri/frontend/js/navigation.js` | Bottom tab bar + view switching | | |
| 98 | - | | `src-tauri/frontend/js/mobile.js` | Swipe, pull-to-refresh, long-press wiring | | |
| 99 | - | | `src-tauri/Cargo.toml` | Desktop-only dep gating, Android bundled deps | | |
| 100 | - | | `src-tauri/src/main.rs` | Desktop-only plugin/service gating | | |
| 101 | - | | `dist/release-ios.sh` | iOS build + TestFlight upload script | | |
| 102 | - | | `dist/release-android.sh` | Android build script | | |
| 103 | - | ||
| 104 | - | --- | |
| 105 | - | ||
| 106 | - | ## Running on Mobile | |
| 107 | - | ||
| 108 | - | ```bash | |
| 109 | - | # iOS | |
| 110 | - | cargo tauri ios dev # simulator | |
| 111 | - | cargo tauri ios dev --device "iPhone 17 Pro" # specific simulator | |
| 112 | - | ||
| 113 | - | # Android (set env vars first, or source ~/.zshrc) | |
| 114 | - | NDK_BIN=$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin | |
| 115 | - | PATH="$NDK_BIN:$PATH" CC_aarch64_linux_android="$NDK_BIN/aarch64-linux-android24-clang" \ | |
| 116 | - | AR_aarch64_linux_android="$NDK_BIN/llvm-ar" RANLIB_aarch64_linux_android="$NDK_BIN/llvm-ranlib" \ | |
| 117 | - | cargo tauri android dev # emulator | |
| 118 | - | ``` | |
| 119 | - | ||
| 120 | - | ## Known Issues | |
| 121 | - | ||
| 122 | - | - **iOS Simulator deploy race** — boot manually first (`open -a Simulator && sleep 5`) | |
| 123 | - | - **SQLite on Android** — bundled via `libsqlite3-sys = { features = ["bundled"] }` | |
| 124 | - | - **OpenSSL on Android** — vendored, needs NDK toolchain env vars | |
| 125 | - | - **Java version** — Gradle requires Java 17, not 25 |
| @@ -1,228 +0,0 @@ | |||
| 1 | - | # Compose unification — migration plan | |
| 2 | - | ||
| 3 | - | > Phase 7 Tier 6 #3 (the deferred-but-eventual one). Plans how to collapse | |
| 4 | - | > the two compose surfaces — desktop `compose.html` (its own Tauri window) | |
| 5 | - | > and the mobile compose modal in `js/emails.js` — into one component. | |
| 6 | - | > | |
| 7 | - | > Status as of 2026-05-20: **stage 1 of 6 landed**. Tier 6 #1 (task drawer) | |
| 8 | - | > and #2 (settings drawer) shipped same day. This one's the heaviest of | |
| 9 | - | > the three and touches the email send path, so it's staged across | |
| 10 | - | > releases rather than landed in one drop. | |
| 11 | - | ||
| 12 | - | ## Current state | |
| 13 | - | ||
| 14 | - | | Stage | Status | Notes | | |
| 15 | - | |---|---|---| | |
| 16 | - | | 1 — Data contract | ✓ landed 2026-05-20 | `js/compose-form.js`: caps, `collectInput`, `validate`, `validateForSend`. Both surfaces call in. `to` vs `toAddress` shim deleted. | | |
| 17 | - | | 2 — CC/BCC on modal | ✓ landed 2026-05-20 | `openComposeModal` gains CC/BCC inputs + Show/Hide toggle via `extraContent`. Autocomplete + address-highlight wired to both. Auto-expands on `pf.cc`/`pf.bcc` prefill. Visual order (below Body) deferred to stage 3's shared template. | | |
| 18 | - | | 3 — Shared HTML template | pending | Extract `composeForm.buildFieldsHtml`. Medium risk (DOM-ID dependencies in compose.html's inline JS). | | |
| 19 | - | | 4 — Shared behavior bindings | pending | `composeForm.bindBehaviors`: autocomplete / signature swap / address-highlight / attachment picker. | | |
| 20 | - | | 5 — Desktop-only features to modal | pending | Draft autosave, reply indicator, save-contact prompt promoted into the shared module; modal opts in. | | |
| 21 | - | | 6 — Decide `compose.html`'s fate | open | 6a: keep as thin wrapper. 6b: eliminate, use modal/overlay on desktop too. Decide on creator feedback. | | |
| 22 | - | ||
| 23 | - | **Resuming:** if you're picking this up after a context wipe, read this | |
| 24 | - | file end-to-end, then read `src-tauri/frontend/js/compose-form.js` (the | |
| 25 | - | stage-1 contract) and the CC/BCC block in | |
| 26 | - | `src-tauri/frontend/js/emails.js::openComposeModal` (stage-2 surface). | |
| 27 | - | The next concrete code change is **stage 3**: extract a shared | |
| 28 | - | `composeForm.buildFieldsHtml(opts)` that both `compose.html` and | |
| 29 | - | `openComposeModal` render. Watch the inline-JS DOM-ID dependencies in | |
| 30 | - | `compose.html` (`from-account`, `to-address`, `cc-address`, | |
| 31 | - | `bcc-address`) — preserve them or migrate the inline handlers to | |
| 32 | - | class-based selectors. | |
| 33 | - | ||
| 34 | - | ## Why staged | |
| 35 | - | ||
| 36 | - | The two surfaces have bidirectional drift that makes a one-shot rewrite | |
| 37 | - | unsafe during soft launch: | |
| 38 | - | ||
| 39 | - | | Feature | Desktop window | Mobile modal | | |
| 40 | - | |---|---|---| | |
| 41 | - | | To / Subject / Body | ✓ | ✓ | | |
| 42 | - | | CC / BCC | ✓ | — | | |
| 43 | - | | Account picker | ✓ | ✓ | | |
| 44 | - | | Signature swap on account change | — | ✓ | | |
| 45 | - | | Autocomplete | ✓ (via `address-highlight.js` only) | ✓ | | |
| 46 | - | | Address highlight | ✓ | ✓ | | |
| 47 | - | | Attachment picker | ✓ | ✓ | | |
| 48 | - | | Attachment cap warning | status bar | hard-block toast | | |
| 49 | - | | Reply indicator | ✓ | — (passes refs through) | | |
| 50 | - | | Draft autosave | ✓ | — | | |
| 51 | - | | "Save contact?" prompt | ✓ | — | | |
| 52 | - | | Send-with-delay (undo) | ✓ (via emit to main) | ✓ | | |
| 53 | - | | Payload shape | `{ to, toAddress, ccAddress, bccAddress, ... }` | `{ toAddress, ... }` | | |
| 54 | - | ||
| 55 | - | The `to` vs `toAddress` payload shim in `compose.html` line 451-454 is the | |
| 56 | - | exact kind of bug a unified data contract prevents. A one-shot rewrite | |
| 57 | - | risks shipping a regression in a launch-sensitive flow; staged refactors | |
| 58 | - | let each step ship + bake before the next. | |
| 59 | - | ||
| 60 | - | ## Target end state | |
| 61 | - | ||
| 62 | - | One `GoingsOn.composeForm` module owns: | |
| 63 | - | - The form's HTML template (one shape, both surfaces). | |
| 64 | - | - The send payload contract (one `collectInput` function). | |
| 65 | - | - Validation (one `validate` function). | |
| 66 | - | - Shared behaviors (autocomplete, signature swap, address highlight, attachment picker, attachment cap). | |
| 67 | - | ||
| 68 | - | Each surface (window or modal) is a thin shell that mounts the module: | |
| 69 | - | - `compose.html` becomes a layout-only wrapper that loads the module. | |
| 70 | - | - The mobile modal becomes a layout-only wrapper that loads the module. | |
| 71 | - | ||
| 72 | - | Whether `compose.html` survives as a separate window or gets folded into | |
| 73 | - | an in-app overlay is the final decision (stage 6); deferred until after | |
| 74 | - | the data contract and feature parity are in place. | |
| 75 | - | ||
| 76 | - | ## Stages | |
| 77 | - | ||
| 78 | - | Each stage: | |
| 79 | - | - Ships independently and bakes for at least one release before the next. | |
| 80 | - | - Has its own rollback (revert that PR, the prior stage keeps working). | |
| 81 | - | - Updates `phase-7.md` Tier 6 line item progress. | |
| 82 | - | ||
| 83 | - | ### Stage 1 — Data contract (low risk, ~½ day) | |
| 84 | - | ||
| 85 | - | **What:** New `js/compose-form.js` module exposing: | |
| 86 | - | - `collectInput(form, opts)` → returns the canonical send payload from any form-shape that uses the documented field names. | |
| 87 | - | - `validate(input)` → returns `{ ok: true } | { ok: false, field, message }`. | |
| 88 | - | - `exceedsAttachmentCap(files)` → returns `{ over, warn, totalBytes }`. | |
| 89 | - | ||
| 90 | - | Both surfaces call into these. Per-surface field markup is unchanged. | |
| 91 | - | ||
| 92 | - | **Bug it fixes:** the `to` vs `toAddress` payload shim. Compose.html line 451-454 stops shipping both keys; the module emits the canonical shape. | |
| 93 | - | ||
| 94 | - | **Acceptance criteria:** | |
| 95 | - | - Desktop send and mobile send produce identical wire payloads for the same form input (verify via a trace log or snapshot test in the backend). | |
| 96 | - | - All existing send tests still pass. | |
| 97 | - | - Attachment cap behavior is now uniform: same threshold, same blocking decision. | |
| 98 | - | ||
| 99 | - | **Rollback:** revert the PR; each surface still has its inline payload code (kept until stage 4). | |
| 100 | - | ||
| 101 | - | ### Stage 2 — CC/BCC on the modal (low risk, ~½ day) | |
| 102 | - | ||
| 103 | - | **What:** Add CC and BCC inputs to the mobile compose modal. Use the new | |
| 104 | - | `composeForm.validate` from stage 1 (already handles them). Hide behind a | |
| 105 | - | "Show CC/BCC" toggle, same as `compose.html`. | |
| 106 | - | ||
| 107 | - | **Why now:** brings feature parity to the modal so stage 4 has nothing | |
| 108 | - | new to add when the modal gets the shared template. | |
| 109 | - | ||
| 110 | - | **Acceptance criteria:** | |
| 111 | - | - CC and BCC fields work end-to-end (sent message has the right recipients). | |
| 112 | - | - Address highlight + autocomplete attach to CC and BCC (not just To). | |
| 113 | - | - Modal is no taller than before when CC/BCC are collapsed. | |
| 114 | - | ||
| 115 | - | **Rollback:** revert the PR; modal returns to To-only. | |
| 116 | - | ||
| 117 | - | ### Stage 3 — Shared HTML template (medium risk, ~1 day) | |
| 118 | - | ||
| 119 | - | **What:** Extract the form fields HTML (From / To / CC / BCC / Subject / | |
| 120 | - | Body / Attachments) into `composeForm.buildFieldsHtml(opts)`. Both | |
| 121 | - | surfaces render it; both surfaces' chrome (toolbar in window, modal | |
| 122 | - | header in modal) stays surface-specific. | |
| 123 | - | ||
| 124 | - | **Why this is the medium-risk step:** the desktop window relies on | |
| 125 | - | specific DOM IDs (`from-account`, `to-address`, etc.) for its inline | |
| 126 | - | behaviors (sendEmail, saveDraft, autosaveTimer). The shared template | |
| 127 | - | needs to keep those IDs or migrate the inline JS to class-based | |
| 128 | - | selectors. | |
| 129 | - | ||
| 130 | - | **Acceptance criteria:** | |
| 131 | - | - Desktop window's send / save-draft / autosave / save-contact prompts all work unchanged. | |
| 132 | - | - Mobile modal's signature swap on account change still works. | |
| 133 | - | - One visual regression sweep: side-by-side desktop window vs. mobile modal screenshots before and after. | |
| 134 | - | ||
| 135 | - | **Rollback:** revert the PR; each surface returns to its own inline form HTML. Stages 1 and 2 still apply. | |
| 136 | - | ||
| 137 | - | ### Stage 4 — Shared behavior bindings (medium risk, ~1 day) | |
| 138 | - | ||
| 139 | - | **What:** Extract the shared behaviors into `composeForm.bindBehaviors(rootEl, opts)`: | |
| 140 | - | - Account select → signature swap. | |
| 141 | - | - To/CC/BCC inputs → autocomplete attach. | |
| 142 | - | - To/CC/BCC inputs → address-highlight attach. | |
| 143 | - | - Attach-file button → file picker + attachment row rendering. | |
| 144 | - | ||
| 145 | - | Each surface calls `bindBehaviors` after mounting `buildFieldsHtml`. The | |
| 146 | - | inline binders in each surface are deleted. | |
| 147 | - | ||
| 148 | - | **Why now:** with template (stage 3) and contract (stage 1) shared, | |
| 149 | - | behaviors are the last drift surface. Once shared, any future | |
| 150 | - | compose-form change automatically applies to both surfaces. | |
| 151 | - | ||
| 152 | - | **Acceptance criteria:** | |
| 153 | - | - All shared behaviors work on both surfaces. | |
| 154 | - | - No console errors on either surface. | |
| 155 | - | - Backend send-test suite still green. | |
| 156 | - | ||
| 157 | - | **Rollback:** revert the PR; each surface returns to its own inline | |
| 158 | - | behavior bindings. | |
| 159 | - | ||
| 160 | - | ### Stage 5 — Bring desktop-only features to the modal (low risk, ~1 day) | |
| 161 | - | ||
| 162 | - | **What:** Promote draft autosave, reply indicator, and "Save contact?" | |
| 163 | - | prompt from desktop-only into the shared module. Modal opts in via | |
| 164 | - | `composeForm.bindBehaviors({ ..., enableAutosave: true, enableSaveContactPrompt: true })`. | |
| 165 | - | ||
| 166 | - | **Why now:** with everything else shared, parity gaps are visible and | |
| 167 | - | fixable in one place. Mobile gets full feature parity before stage 6 | |
| 168 | - | decides whether the desktop window even survives. | |
| 169 | - | ||
| 170 | - | **Acceptance criteria:** | |
| 171 | - | - Modal-composed drafts survive page reload (autosave works). | |
| 172 | - | - Replying from mobile shows the same reply indicator the desktop window does. | |
| 173 | - | - Save-contact bar appears on mobile after sending to an unknown recipient. | |
| 174 | - | ||
| 175 | - | **Rollback:** revert the PR; modal returns to no-autosave / no-save-contact / no-reply-indicator. Other stages unaffected. | |
| 176 | - | ||
| 177 | - | ### Stage 6 — Decide on `compose.html`'s fate (open question, deferred) | |
| 178 | - | ||
| 179 | - | **Two paths:** | |
| 180 | - | ||
| 181 | - | **6a — Keep the window.** `compose.html` stays as a layout-only wrapper | |
| 182 | - | that loads `composeForm`. Multi-window desktop users get their preferred | |
| 183 | - | flow. About 200 lines of compose.html shell remain (vs. the current 939). | |
| 184 | - | ||
| 185 | - | **6b — Eliminate the window.** Replace with an in-app overlay (or full | |
| 186 | - | modal) on desktop too. Single code path. `compose.html` deleted, cross- | |
| 187 | - | window `compose:queue-send` event listener removed from `app.js`. | |
| 188 | - | ||
| 189 | - | **Tradeoff:** 6a preserves muscle memory for users who pop compose out | |
| 190 | - | into its own window (common on macOS / multi-monitor setups). 6b is | |
| 191 | - | ~750 fewer lines of code and removes a whole class of cross-window | |
| 192 | - | state-sync bugs. | |
| 193 | - | ||
| 194 | - | **Recommendation:** decide based on user feedback collected during | |
| 195 | - | stages 1-5. If no one reports "I miss the popup window", do 6b. If | |
| 196 | - | multi-window-on-desktop is a load-bearing flow for the early creator | |
| 197 | - | cohort, do 6a. | |
| 198 | - | ||
| 199 | - | **Acceptance criteria (either path):** | |
| 200 | - | - All five preceding stages have shipped and baked for ≥1 release each. | |
| 201 | - | - A user study or feedback channel pass on "do you ever pop compose into its own window?". | |
| 202 | - | ||
| 203 | - | ## Sequencing | |
| 204 | - | ||
| 205 | - | Stages 1 → 2 → 3 → 4 → 5 → 6. Each independently shippable. | |
| 206 | - | ||
| 207 | - | Suggested cadence: one stage per week during soft launch, then evaluate | |
| 208 | - | stage 6 after creator feedback stabilizes (probably 2026-Q3+). | |
| 209 | - | ||
| 210 | - | ## Open questions | |
| 211 | - | ||
| 212 | - | - **Drafts table schema:** does the current drafts schema already | |
| 213 | - | support CC/BCC? If not, stage 2 needs a migration. | |
| 214 | - | - **Modal sizing:** with CC/BCC + autosave indicator + reply indicator | |
| 215 | - | inside a modal, does the form still fit a single screen on small | |
| 216 | - | desktops? May need to scroll the body field at smaller heights. | |
| 217 | - | - **Window menu integration:** if 6b wins, the desktop "File → New | |
| 218 | - | Email" menu shortcut needs to open the modal instead of spawning a | |
| 219 | - | window. Trivial change but track it. | |
| 220 | - | ||
| 221 | - | ## What's *not* in scope | |
| 222 | - | ||
| 223 | - | - Rich-text editing — body remains plain text. Adding rich-text is a | |
| 224 | - | separate Phase 4-style feature, not a unification step. | |
| 225 | - | - Encrypted-by-default outbound — out of scope; track separately. | |
| 226 | - | - Multiple inline images — same. | |
| 227 | - | - Cross-tab draft sync via SyncKit — separate; would land as part of the | |
| 228 | - | drafts table sync work, not here. |
| @@ -1,148 +0,0 @@ | |||
| 1 | - | # Phase 0 — Design-System Conformance Audit | |
| 2 | - | ||
| 3 | - | **Scope:** Inventory of the design primitives the GoingsOn Tauri webview actually uses today, divergence map of where modules reinvent them, and gaps that block consistency. Surface audits (Phase 1+) do not start until the consolidation pre-plan derived from this report lands. | |
| 4 | - | ||
| 5 | - | **Stack:** Tauri 2 webview, vanilla HTML / CSS / JS (no HTMX, no Askama, no framework). 46 IIFE modules under the `GoingsOn.*` namespace. CSS in `src-tauri/frontend/css/styles.css`. 9 runtime-swappable themes under `src-tauri/frontend/themes/helix/`. | |
| 6 | - | ||
| 7 | - | --- | |
| 8 | - | ||
| 9 | - | ## (a) Actual primitive set | |
| 10 | - | ||
| 11 | - | ### CSS custom properties — `styles.css` `:root` (lines 91–196, ~60 tokens) | |
| 12 | - | ||
| 13 | - | | Group | Tokens | | |
| 14 | - | |---|---| | |
| 15 | - | | Color — surface | `--bg-primary`, `--bg-secondary`, `--bg-tertiary`, `--bg-card`, `--bg-hover` | | |
| 16 | - | | Color — text | `--text-primary`, `--text-secondary`, `--text-muted`, `--text-on-accent` | | |
| 17 | - | | Color — accent | `--accent-yellow`, `--accent-green`, `--accent-blue`, `--accent-purple`, `--accent-red`, `--accent-cyan`, plus aliases `--accent-color`, `--accent-primary` | | |
| 18 | - | | Border | `--border-width` (2px), `--border-width-sm`, `--border-color`, `--border-light` | | |
| 19 | - | | Shadow | `--shadow-offset-xs` … `--shadow-offset-xl`; compound `--shadow-brutal-xs/md/lg/xl` (offset-only, no blur — the Neobrute signature) | | |
| 20 | - | | Radius | `--radius-xs/sm/md/lg/xl/full` | | |
| 21 | - | | Spacing | `--space-1` … `--space-6` (0.25rem → 1.5rem) | | |
| 22 | - | | Type — size | `--font-size-xxs` … `--font-size-4xl` | | |
| 23 | - | | Type — line | `--line-height-tight/normal/relaxed` | | |
| 24 | - | | Type — family | `--font-sans`, `--font-serif`, `--font-mono`, `--font-display` (Reglo) | | |
| 25 | - | | Layout | `--width-container` (1400px), `--width-modal` (560px), `--width-sidebar` (280px) | | |
| 26 | - | | Motion | `--transition-fast` (0.1s), `--transition-normal` (0.15s), `--transition-slow` (0.3s) | | |
| 27 | - | | Overlay | `--overlay-color` | | |
| 28 | - | | Cross-layer | `--timeline-slot-h` (12px) — read by JS for day-plan render | | |
| 29 | - | ||
| 30 | - | ### Component classes — counted ~594 selectors in `styles.css` | |
| 31 | - | ||
| 32 | - | Canonical primitives (these are the ones every JS render module should target): | |
| 33 | - | ||
| 34 | - | - **Button** — `.btn`, `.btn-primary`, `.btn-secondary`, `.btn-danger`, `.btn-sm`, `.btn-icon`, `.btn-loading`, `.btn-text`, `.btn-link` | |
| 35 | - | - **Card** — `.card`, `.card-header`, `.card-title`, `.card-description`, `.card-meta`, `.card-badge`, `.cards-grid` | |
| 36 | - | - **Form field** — `.form-group`, `.form-label`, `.form-input`, `.form-select`, `.form-textarea`, `.form-actions`, `.form-row` | |
| 37 | - | - **Badge / tag** — `.badge`, `.tag`, `[data-color="green|yellow|red|cyan|purple|muted"]`; status variants `.tag.status-active|onhold|archived|inactive|completed` | |
| 38 | - | - **Modal** — `.modal-overlay`, `.modal-container` (+ `.modal-large`), `.modal-header`, `.modal-title`, `.modal-content`, `.modal-close`; visibility via `.hidden` and `.closing` | |
| 39 | - | - **Toast** — `.toast`, `.toast-info`, `.toast-success`, `.toast-error`, `.toast-undo`; undo sub-parts `.undo-message`, `.undo-btn`, `.undo-countdown` | |
| 40 | - | - **Tab / pill** — `.tab-navigation`, `.tab.active`; `.pill-nav`, `.pill.active` | |
| 41 | - | - **Filter** — `.filter-bar`, `.filter-select`, `.filter-checkbox` | |
| 42 | - | - **Context menu** — `.context-menu(.visible)`, `.context-menu-item(--danger)`, `.context-menu-separator`, `.context-menu-header` | |
| 43 | - | - **Empty state** — `.empty-state` + `.empty-state-text` + `.empty-state-icon` | |
| 44 | - | - **Skeleton** — `.skeleton-shimmer`, `.skeleton-row`, `.skeleton-lines`, `.skeleton-line.long|.medium|.short`, `.spinner`, `.loading` | |
| 45 | - | - **Progress** — `.progress-bar-container`, `.progress-bar` | |
| 46 | - | - **Table — generic** — `.data-table` | |
| 47 | - | - **Table — task** — `.task-table`, `.task-header-row`, `.task-row`, `.task-cell`, plus row state classes `.task-overdue`, `.task-completed`, `.task-started`, `.task-snoozed`, `.priority-high|medium|low`, `.task-timer-active`, `.task-time-badge.over-estimate` | |
| 48 | - | - **Table — event** — `.event-table(-virtual)`, `.event-header-row`, `.event-row-virtual` | |
| 49 | - | - **Kanban** — `.kanban-board`, `.kanban-column`, `.kanban-card(-empty)` | |
| 50 | - | - **Day-plan timeline** — `.timeline-slot`, `.time-block`, `.block-focus`, `.block-personal` | |
| 51 | - | - **Weekly review** — `.weekly-grid`, `.weekly-cell`, `.weekly-day-header` | |
| 52 | - | - **Subtasks** — `.subtask-item(-linked)`, `.subtask-checkbox`, `.subtask-text-done` | |
| 53 | - | - **Bulk** — `.bulk-actions-bar`, `.bulk-checkbox`, `.bulk-count`, `.bulk-select-all`; selection state `.selected`, `.keyboard-selected` | |
| 54 | - | - **Shell** — `.app-header`, `.app-body`, `.main-content`, `.page-header`, `.page-title` | |
| 55 | - | ||
| 56 | - | ### JS-rendered markup helpers (the *render* primitives) | |
| 57 | - | ||
| 58 | - | - `GoingsOn.ui.openModal(title, html, opts)` — single global `#modal-overlay`, content swapped via `.innerHTML`; supports `.modal-large`; handles Escape, click-outside, focus trap. | |
| 59 | - | - `GoingsOn.ui.showToast(message, type, opts)` — dynamically creates and appends a `.toast.toast-${type}` to `document.body`. | |
| 60 | - | - `GoingsOn.ui.showConfirmDialog(title, message, opts)` and `GoingsOn.ui.confirmDelete(name, action)` — render into the global modal. | |
| 61 | - | - `GoingsOn.ui.showUndoToast(...)` — `.toast-undo` with countdown. | |
| 62 | - | - `GoingsOn.ui.renderEmptyState(message, buttonLabel?, onClickFn?)` — returns `.empty-state` element. | |
| 63 | - | - `GoingsOn.ui.showContextMenu(x, y, items)` — builds `.context-menu` from `{ icon, label, shortcut, subtitle, danger }` items. | |
| 64 | - | - **Missing helpers:** no `renderRow(...)` (each module hand-rolls), no `renderBadge(...)`, no `renderFormField(...)` (form-modal.js builds inline), no skeleton helper. | |
| 65 | - | ||
| 66 | - | ### Themes | |
| 67 | - | ||
| 68 | - | 9 TOML files under `src-tauri/frontend/themes/helix/` (catppuccin × 4, ayu_light, dracula, flatwhite, nord, tokyonight). Each has a Helix-style `[palette]` block and UI-key references. `js/themes.js` maps 13 dotted TOML paths → CSS custom properties at runtime; selection persists to `localStorage` (`goingson-theme`); system preference picks default (`catppuccin-mocha` dark / `neobrute` light). Tokens overridden are **color tokens only** — every other axis (spacing, type, radius, shadow offsets) is theme-invariant. | |
| 69 | - | ||
| 70 | - | --- | |
| 71 | - | ||
| 72 | - | ## (b) Divergence map | |
| 73 | - | ||
| 74 | - | ### Inline `style="…"` — 27 in HTML, 335+ in JS | |
| 75 | - | ||
| 76 | - | Top offenders by file (JS): | |
| 77 | - | - `email-accounts.js` — 51 | |
| 78 | - | - `settings-sync.js` — 31 | |
| 79 | - | - `settings.js` — 25 | |
| 80 | - | - `app.js` — 18 (a `kbdStyle` constant repeated everywhere a keycap is rendered) | |
| 81 | - | - `tasks-render.js` — 15 (subtask modal layout, progress bars) | |
| 82 | - | ||
| 83 | - | Three flavors observed: (1) **visibility control** (`display:none|flex`) — acceptable, but should migrate to `.hidden` / `.is-flex` utility classes for grep-ability; (2) **layout micro-tweaks** (`display:flex; gap:.5rem`) — should become utility classes or a `.stack` / `.row` primitive; (3) **state-keyed colors and backgrounds** — the worst category; couples runtime values to literal hex. | |
| 84 | - | ||
| 85 | - | ### Raw hex outside `styles.css` / `themes/` | |
| 86 | - | ||
| 87 | - | - `components-modal.js` — toast fallbacks `#22c55e`, `#ef4444`, `#c00` injected via `style.cssText`. *These bypass theme switching*: a user on Catppuccin Mocha still sees Tailwind-green success toasts. | |
| 88 | - | - `updater.js` — `#6c5ce7`, `#444`, `#aaa`, `#2d2d2d` for the Tauri updater overlay. | |
| 89 | - | - `keyboard.js` — keycap chrome. | |
| 90 | - | - `emails.js` — `var(--accent-green, #22c55e)` pattern (variable + fallback). Better than raw hex but the fallback still drifts from the token palette. | |
| 91 | - | ||
| 92 | - | ### Duplicated row-rendering recipes | |
| 93 | - | ||
| 94 | - | Each list view hand-writes its row markup: | |
| 95 | - | - `tasks-render.js:renderTaskRow(t, index)` — 7-cell grid with status/state classes. | |
| 96 | - | - `projects-render.js` — `.card` grid cards. | |
| 97 | - | - `contacts-render.js` — `.card.contact-card` with avatar + initials. | |
| 98 | - | - `events.js` — virtual `.event-row-virtual` cells. | |
| 99 | - | - `emails.js` — `.email-list` + `.email-item`. | |
| 100 | - | ||
| 101 | - | No shared `renderRow(kind, model)` exists. The grids share visual language (status pill, primary text, secondary text, right-aligned actions, kebab menu) but each implementation reinvents the slot layout. | |
| 102 | - | ||
| 103 | - | ### Duplicated empty-state markup | |
| 104 | - | ||
| 105 | - | Three canonical forms in use: | |
| 106 | - | - `.empty-state` + `.empty-state-text` (canonical — `components.js` exposes a helper). | |
| 107 | - | - `.empty-dashboard-list` (projects detail). | |
| 108 | - | - `.kanban-empty` (kanban board). | |
| 109 | - | - `.virtual-scroller-empty` (virtual list fallback). | |
| 110 | - | ||
| 111 | - | Several call sites build the empty state inline with `style="padding: 1rem; text-align: center"` rather than calling `renderEmptyState()`. `events.js` has `<div class="empty-state">No events scheduled.</div>` with extra inline padding — a near-canonical that drifted. | |
| 112 | - | ||
| 113 | - | ### Duplicated toast / confirm recipes | |
| 114 | - | ||
| 115 | - | All toasts pass through `showToast()`, **but `showToast` itself injects inline positioning + color via `style.cssText`** — so even the canonical helper is itself a divergence source. Every confirm dialog goes through `showConfirmDialog()` but the markup contains inline button styles in at least one branch (`components-modal.js`). | |
| 116 | - | ||
| 117 | - | ### Mobile vs desktop divergence | |
| 118 | - | ||
| 119 | - | `mobile.js` is a layout-shift module (it doesn't reach into deep markup), which is the *correct* shape. The risk is the `compose.html` `<style>` block (lines 8–100) — it locally overrides field chrome with non-tokenized values. That pattern, if copied, becomes parallel-markup-by-stylesheet. | |
| 120 | - | ||
| 121 | - | ### Hardcoded `--neobrute` default vs theme drift | |
| 122 | - | ||
| 123 | - | The CSS tokens in `styles.css :root` carry the Neobrute hex values. Themes override **only colors**. So any token *not* in the theme's mapping (e.g. accents added later without theme entries, the `--bg-hover` alias) silently falls back to Neobrute light values on every theme. This is invisible until a dark-theme user hovers a control and gets a pale-blue surface. | |
| 124 | - | ||
| 125 | - | --- | |
| 126 | - | ||
| 127 | - | ## (c) Gaps — primitives that should exist and don't | |
| 128 | - | ||
| 129 | - | 1. **`renderRow(kind, model, opts)`** — single helper for the visual shape "icon/avatar · primary · secondary · meta · actions". Adapters per kind (task, project, contact, event, email) supply the cells; the chrome is shared. Eliminates 5 parallel implementations. | |
| 130 | - | 2. **`renderFormField({ kind, label, value, error, help })`** — replaces the manual `.form-group > .form-label + .form-input` blocks scattered across `form-modal.js`, `settings.js`, `email-accounts.js`. Adds a real **error variant** (today errors are surfaced via toast, not next to the field). | |
| 131 | - | 3. **`renderSkeleton(rows)`** — a list-loading primitive. The `.skeleton-*` classes exist but no helper wraps them, so no view actually uses them. | |
| 132 | - | 4. **Confirm-dialog helper that produces no inline styles.** Today's `showConfirmDialog` is mostly there — strip the inline button styles and document it as canonical. | |
| 133 | - | 5. **Toast styling moved into CSS.** Positioning, shadow, and color belong on `.toast.toast-${type}` classes, not in JS-injected `cssText`. This is what makes toasts ignore the active theme. | |
| 134 | - | 6. **`.hidden` / utility class migration.** Replace `style="display:none"` in HTML with `.hidden` (already defined for the modal overlay — extend its scope). Add `.stack`, `.row`, `.gap-2`, `.gap-3` to absorb the most common inline flex tweaks. | |
| 135 | - | 7. **Empty-state variants documented.** Either consolidate to a single `.empty-state` (with size modifiers `.empty-state--compact`, `.empty-state--dashboard`) and delete `.empty-dashboard-list` / `.kanban-empty` / `.virtual-scroller-empty`, or keep them and document when each applies. The decision belongs in the remediation plan. | |
| 136 | - | 8. **Theme coverage check.** Every CSS custom property used in `styles.css` should appear in `js/themes.js`'s mapping or be explicitly marked theme-invariant. Today it's by inspection. | |
| 137 | - | ||
| 138 | - | --- | |
| 139 | - | ||
| 140 | - | ## (d) Charter | |
| 141 | - | ||
| 142 | - | A separate charter file lives at `docs/design-system.md` (this commit). It names each primitive's single canonical class or render helper. `docs/styleguide.md` remains as the visual reference; the new charter cross-references it as the authoritative inventory. | |
| 143 | - | ||
| 144 | - | --- | |
| 145 | - | ||
| 146 | - | ## Summary | |
| 147 | - | ||
| 148 | - | GoingsOn has a real design system, not a Pinterest board: ~60 tokens, ~594 selectors, a clean Neobrute identity, runtime theme switching, and a small set of UI helpers (`openModal`, `showToast`, `renderEmptyState`, `showContextMenu`). What it lacks is a **row-rendering primitive** and a **form-field primitive** — those two absences explain most of the 335 inline styles and most of the parallel-markup drift across `tasks-render.js`, `projects-render.js`, `contacts-render.js`, `events.js`, and `emails.js`. The toast helper is itself the most prominent token-bypass in the codebase: it injects raw hex via `style.cssText` and therefore ignores theme switching. Phase 1 should not begin until (1) `renderRow` and `renderFormField` exist, (2) toast styling is moved to CSS, (3) the `--neobrute` fallback drift is verified across all 9 themes, and (4) `styles.css :root` is the *only* place hex literals appear outside `themes/`. |
| @@ -1,160 +0,0 @@ | |||
| 1 | - | # Phase 1 — Shell & Navigation UX Audit | |
| 2 | - | ||
| 3 | - | **Scope:** the persistent chrome a user sees on every screen — top header, desktop tab navigation, sub-pill nav, mobile bottom nav, global modal / context-menu / action-sheet, sync indicator, search / settings / help controls, project-switching affordance. Feature content within a tab is out of scope (those land in Phases 2–5). | |
| 4 | - | ||
| 5 | - | **Stack:** Tauri 2 webview, vanilla HTML/CSS/JS. Files reviewed: `src-tauri/frontend/index.html` (shell markup), `js/navigation.js`, `js/router.js`, `js/mobile.js`, `js/touch.js`, `js/settings-sync.js#refreshSyncIndicator`, `js/projects.js` (switcher path), and the matching CSS in `css/styles.css`. | |
| 6 | - | ||
| 7 | - | **Method:** classical universal pass (Norman / Tognazzini / Raskin / Apple HIG) plus the flat-design cross-cutting check. | |
| 8 | - | ||
| 9 | - | --- | |
| 10 | - | ||
| 11 | - | ## Architecture snapshot | |
| 12 | - | ||
| 13 | - | - Three top-level tabs: **Work**, **Time**, **Messages**. Each is a `.tab-group` (`#work-view`, `#time-view`, `#messages-view`) hidden/shown via `.hidden`. | |
| 14 | - | - Each tab has a row of **pills** under it (`.pill-nav` / `.pill`). Work: Tasks · Projects. Time: Day · Week · Month · Timer · Events. Messages: Email · Contacts. | |
| 15 | - | - Routing is **URL-based** via History API (`pushState` / `popstate`); no hash. Default route `/tasks`. Project context lives at `/project/{id}`. | |
| 16 | - | - **Mobile (≤768px):** desktop `.app-header` is `display: none`. A fixed bottom bar `#mobile-tab-bar` replaces it with Work / Time / Messages buttons plus a `+` create button and a `More` popover for Settings/Search/Help. | |
| 17 | - | - **Globals (one each):** `#modal-overlay`, `#context-menu`, `#action-sheet`. Touch context menus are dispatched to the action sheet by feature code, not the shell. | |
| 18 | - | - **Active-state markers:** `.tab.active` (filled blue), `.pill.active` (filled dark), `.mobile-tab.active` (color shift to `--accent-blue`), `.page-title` text, and `document.title` ("GoingsOn — Tasks"). | |
| 19 | - | ||
| 20 | - | --- | |
| 21 | - | ||
| 22 | - | ## Critical (fix before declaring Phase 1 done) | |
| 23 | - | ||
| 24 | - | ### 1. Mobile users cannot reach sub-views (pills) from the bottom nav | |
| 25 | - | - **Category:** Mappings, Visibility of state. (**Universal**) | |
| 26 | - | - **Location:** `index.html:537–552` mobile tabs; `js/navigation.js#newItemForCurrentView`; `styles.css` mobile media query at ~7300. | |
| 27 | - | - **Observation:** Tapping a `.mobile-tab` (Work/Time/Messages) always lands on each tab's *default* pill (Tasks / Day / Email). The pills themselves render on mobile and remain tappable, but there is no equivalent of the desktop pill bar surfaced from the bottom nav, and pills are *not* persistent chrome on mobile — they sit at the top of the content area and scroll away. A mobile user opening Time and wanting to reach Events has to scroll back up to find the pill row. A user wanting Projects from Work must already know to look for a Projects pill at the content origin. | |
| 28 | - | - **Why it matters:** the mobile shell exposes only ⅓ of the navigation surface. Three of the most-used destinations on a mobile screen (Projects, Events, Week review) are effectively unreachable without scrolling. | |
| 29 | - | - **Recommendation:** | |
| 30 | - | - Either keep the pill row visually pinned under the mobile title (sticky `top: 0` on the active tab's `.pill-nav`) so the sub-navigation is always one tap away; or | |
| 31 | - | - On mobile, long-press a `.mobile-tab` to open an action-sheet picker of that tab's pills (mirrors how iOS Mail does "Long press inbox icon"); or | |
| 32 | - | - Promote a small chevron next to each mobile tab that opens the pill picker as an action sheet. | |
| 33 | - | - Sticky-pill is the cheapest and most discoverable; recommend that as the default. | |
| 34 | - | ||
| 35 | - | ### 2. Sync indicator state is invisible without color memorization | |
| 36 | - | - **Category:** Feedback, Visibility of state, Error messages. (**Universal**) | |
| 37 | - | - **Location:** header `#sync-indicator` (`index.html:31–33`); states in `styles.css:4314–4319`; logic in `js/settings-sync.js#refreshSyncIndicator`. | |
| 38 | - | - **Observation:** Four states are encoded in a single colored dot — **connected** (green), **syncing** (blue, pulsing), **error** (red), **default / not set up** (muted, but in practice the *button* is hidden when not configured so users mainly see green / blue / red). The dot has `title="Cloud Sync"` and `aria-label="Cloud sync status"` but no visible label, no hover-expansion, no text. There is no surface that says "Synced X minutes ago" or "Sync failed — retry" without opening the modal. | |
| 39 | - | - **Why it matters:** for a personal-data app, sync status is a primary trust signal. A user who notices the dot turn red has to click through to a modal to find out what went wrong, and a user who *doesn't notice* the dot has no idea their data isn't backing up. Red and green at 10px with no text fails WCAG accessible-meaning rules (color-only encoding). | |
| 40 | - | - **Recommendation:** | |
| 41 | - | - On hover (desktop), expand the indicator into a chip showing "Synced 2 min ago" / "Syncing…" / "Sync failed". The plan's `docs/design-system.md` calls this out as a documented sprint item ("Sync indicator: expand to show 'Syncing…' / 'Sync error' on hover" — `todo.md` Sprint: Settings & Sync UX). Promote it from sprint to Phase 1. | |
| 42 | - | - On error, surface a one-line banner in the shell ("Sync failed — Retry") rather than only changing the dot color. This is the standard pattern in Mail, Slack, Notion. | |
| 43 | - | - Add a tiny text label next to the dot at desktop widths ≥ 1200px ("Synced", "Syncing", "Sync error"), so users don't have to learn a color code. | |
| 44 | - | ||
| 45 | - | ### 3. Project context cannot be switched without leaving the current work | |
| 46 | - | - **Category:** Modes, Modelessness (Raskin). (**Universal**) | |
| 47 | - | - **Location:** Project access via the "Projects" pill in Work (`index.html:46`), then card grid click → project dashboard. `js/projects.js#open` pushes `/project/{id}`. | |
| 48 | - | - **Observation:** the active project is *not* shell state. To go from "Tasks filtered by Project A" to "Tasks filtered by Project B" the user navigates Tasks → up to Projects pill → grid → click card B → into the project dashboard → click "Tasks" within the project — at least four taps. There is no persistent project selector in the header, sidebar, or pill row. There is no breadcrumb showing "Project A > Tasks". `GoingsOn.state.currentProjectId` exists in memory but isn't reflected in shell chrome. | |
| 49 | - | - **Why it matters:** for an app whose pitch includes project-grouped tasks/emails, projects are a primary axis. Hiding them inside a sub-view makes them feel like a secondary feature and pushes work into "all-projects" mode by default. This is the kind of mode-shift Raskin warns against: the user has to remember which project they were in. | |
| 50 | - | - **Recommendation:** add a project chip in the page header (next to `.page-title`) for views that respect a project context — Tasks, Day, Events, Email-by-project. The chip would show "All projects" or "Project A ✕" and clicking it would open a small picker (popover on desktop, action sheet on mobile). Out-of-scope-but-related: persisting `currentProjectId` to localStorage so it survives reload (today it does not unless the URL is `/project/{id}`). | |
| 51 | - | ||
| 52 | - | --- | |
| 53 | - | ||
| 54 | - | ## Major (high impact, lower urgency) | |
| 55 | - | ||
| 56 | - | ### 4. Mobile tab active-state is too quiet | |
| 57 | - | - **Category:** Visibility of state, Affordances. (**Flat / Universal**) | |
| 58 | - | - **Location:** `styles.css` mobile section, `.mobile-tab.active { color: var(--accent-blue); }`. | |
| 59 | - | - **Observation:** the only visual difference between an active mobile tab and an inactive one is text color (muted → accent-blue). No background, no underline, no top-bar indicator, no weight change. At a glance on a low-contrast theme (e.g. Catppuccin Latte) the active state is barely perceptible. | |
| 60 | - | - **Recommendation:** add a top-edge accent stripe (`border-top: 3px solid var(--accent-blue)` plus `padding-top: 0` adjustment), or fill the icon area with `--bg-secondary`. The desktop tab uses a full filled background — give the mobile tab proportional weight, not a one-property change. | |
| 61 | - | ||
| 62 | - | ### 5. The `+` button on mobile has no indication of *what* it creates | |
| 63 | - | - **Category:** Anticipation (Tog), Affordances. (**Universal**) | |
| 64 | - | - **Location:** `index.html:548`, `js/navigation.js#newItemForCurrentView`. | |
| 65 | - | - **Observation:** the button is a single `+`. Behavior depends on the active tab (creates a task in Work, an event in Time, an email in Messages). A first-time user has no way to know what tapping it will do without testing in each tab. | |
| 66 | - | - **Recommendation:** make the button context-aware visually — change its `aria-label` *and* its icon/label to match the active tab. Or replace it with two-line content: a tiny verb (`New`) plus a context word (`Task`, `Event`, `Email`). A small adornment is enough; the goal is to remove the surprise. | |
| 67 | - | ||
| 68 | - | ### 6. Three header action buttons all look the same | |
| 69 | - | - **Category:** Hierarchy, Consistency. (**Universal / Flat**) | |
| 70 | - | - **Location:** `index.html:34–36`, class `.settings-btn` reused for Search / Settings / `?`. | |
| 71 | - | - **Observation:** Search, Settings, and the `?` help button all share `.settings-btn` styling. They sit in a row of three pill-shaped, identically-weighted buttons. Search is the most-used (it has a keyboard shortcut shown inline), but it doesn't read as primary. The `?` button is a single character with no icon and no border distinguishing it from a typo. | |
| 72 | - | - **Recommendation:** make Search the visually dominant control — give it the filled `.btn-primary` look or its own icon-prefixed pill. Settings and `?` are utility, demote them to `.btn-icon` style or move them into a header-overflow menu. Single-character icon buttons (the `?`) should pair with an icon glyph or be lifted into a tooltip-bearing icon button. | |
| 73 | - | ||
| 74 | - | ### 7. Project-switching has no mobile equivalent at all | |
| 75 | - | - **Category:** Mappings, Modes. (**Universal**) | |
| 76 | - | - **Location:** mobile `+` button creates context-specific items, but project switching is not exposed in mobile chrome. | |
| 77 | - | - **Observation:** because Projects is a pill (not a tab) and pills aren't surfaced from the mobile bottom bar (Finding #1), a mobile user can reach Projects only by tapping Work and then scrolling to find the Projects pill. The `More` popover doesn't include Projects either. | |
| 78 | - | - **Recommendation:** ties into Findings #1 and #3 — the fix for either subsumes this. If sticky pills are added (#1), mobile Project access becomes one tap. If a project chip is added to the page header (#3), it works on mobile too. | |
| 79 | - | ||
| 80 | - | ### 8. Dual title system can drift | |
| 81 | - | - **Category:** Consistency, Visibility of state. (**Universal**) | |
| 82 | - | - **Location:** `.app-title` ("GoingsOn") + `.app-subtitle` ("Personal Productivity") on desktop; `#mobile-view-title` on mobile (`index.html:14–17`). Each subview also has its own `.page-title` ("Tasks", "Projects"). | |
| 83 | - | - **Observation:** the mobile title is set imperatively by `updateMobileViewTitle()` (`navigation.js:262–266`), separately from the in-content `.page-title` set by each view. They're not bound to the same source. If a feature changes its `.page-title` mid-flow (e.g., "Tasks" → "Snoozed Tasks") but forgets to call `updateMobileViewTitle`, mobile shows stale text. | |
| 84 | - | - **Recommendation:** make `.page-title` the source of truth. On mobile, hide the in-content `.page-title` and pull its text into `#mobile-view-title` via a MutationObserver or by routing every set through a single helper. Phase 6 (Mobile parity sweep) is the natural home for this; flag now so it doesn't ship a drift bug in Phase 2. | |
| 85 | - | ||
| 86 | - | ### 9. Reload from a feature route may silently drop the project context | |
| 87 | - | - **Category:** Forgiveness, Persistence. (**Universal**) | |
| 88 | - | - **Location:** `js/router.js#init`, `js/projects.js#open`; `GoingsOn.state.currentProjectId` is in-memory only. | |
| 89 | - | - **Observation:** the URL is the source of truth for *view*, but the *project context* is in-memory. Reload at `/tasks` always shows all-project tasks; the URL doesn't carry the project filter. If a user was on "Tasks filtered to Project A" and reloads, they get all-project tasks with no warning. (Reload at `/project/A` re-enters the project dashboard correctly; the gap is the pill-deep views.) | |
| 90 | - | - **Recommendation:** either (a) extend the URL scheme so filtered views carry `?project=A`, or (b) persist `currentProjectId` to localStorage and re-apply on init. (a) is preferable because deep-linking is more useful than implicit state. This finding overlaps with #3 — adding a header project chip makes the persisted state visible, which makes the bug not-a-bug. | |
| 91 | - | ||
| 92 | - | --- | |
| 93 | - | ||
| 94 | - | ## Minor (worth fixing during normal cleanup) | |
| 95 | - | ||
| 96 | - | ### 10. "Personal Productivity" subtitle is dead pixels | |
| 97 | - | - **Category:** Hierarchy. (**Polish/Universal**) | |
| 98 | - | - **Location:** `index.html:16`, `.app-subtitle`. | |
| 99 | - | - **Observation:** a permanent tagline in the header taking up space that could be used for a project chip or search bar. Users have already opened the app — they know what it is. | |
| 100 | - | - **Recommendation:** remove, or demote to a tooltip on the logo. Use the freed space for a project chip (#3) or to widen Search. | |
| 101 | - | ||
| 102 | - | ### 11. `<h1>` semantic vs visual weight | |
| 103 | - | - **Category:** Hierarchy, Consistency. (**Polish**) | |
| 104 | - | - **Location:** `.app-title h1` (GoingsOn brand) vs `.page-title h2` (Tasks/Projects/etc). | |
| 105 | - | - **Observation:** the brand `h1` is small (logo-styled), but the in-content `h2` "Tasks" is visually dominant. Semantic and visual hierarchy don't match. Not a bug; screen readers do the right thing. But style-guides often want the *current view* to be the document's primary heading. Decide and document. | |
| 106 | - | - **Recommendation:** either change brand to a non-heading element (`div.app-title`) and promote `.page-title` to `<h1>`; or accept the inversion and document it in `design-system.md`. Either is fine — pick one. | |
| 107 | - | ||
| 108 | - | ### 12. `?` button can be missed | |
| 109 | - | - **Category:** Discoverability. (**Polish**) | |
| 110 | - | - **Location:** `index.html:36`. | |
| 111 | - | - **Observation:** the `?` is a single character with no icon and no visible label. New users may not realize it's the help affordance, especially since the `?` key already does the same thing. | |
| 112 | - | - **Recommendation:** pair with an icon, or change the label to `Help` and let it remain a small text button. (Three-button-pill problem #6 also applies.) | |
| 113 | - | ||
| 114 | - | ### 13. Modal overlay assumes a dark scrim, but on dark themes it renders light | |
| 115 | - | - **Category:** Consistency across themes, Flat-design. (**Cross-cutting** — already documented in `theme-coverage.md` finding #1.) | |
| 116 | - | - **Location:** `--overlay-color: color-mix(in srgb, var(--text-primary) 60%, transparent)`. | |
| 117 | - | - **Observation:** on Catppuccin Mocha and similar dark themes, the modal scrim becomes light because it derives from `--text-primary` which is near-white. The user sees a *light* veil over a *dark* surface, which reads as a hover state or a popover, not a modal. | |
| 118 | - | - **Recommendation:** introduce a dedicated `--scrim` token (themeable, default `rgba(0,0,0,0.6)`), or hardcode the overlay to `rgba(0,0,0,0.6)` since modal scrims are universally dark regardless of theme. Already queued; mention in Phase 7 sweep. | |
| 119 | - | ||
| 120 | - | ### 14. Pill `aria-selected` is not set | |
| 121 | - | - **Category:** Accessibility. (**Polish**) | |
| 122 | - | - **Location:** `index.html:44–47`, `index.html:236–242`, etc. | |
| 123 | - | - **Observation:** desktop tabs have `aria-selected` toggled by `navigation.js`, but pills (`<button class="pill">`) do not. Screen readers announce them as plain buttons. Since the pills form a `tablist` semantically (you select one of N sub-views), they should have `role="tab"` + `aria-selected` like the desktop tabs do, and the container `.pill-nav` should have `role="tablist"`. | |
| 124 | - | - **Recommendation:** small markup change in `index.html` plus a one-line update in `navigation.js` to set `aria-selected` on activation. Five-minute fix. | |
| 125 | - | ||
| 126 | - | --- | |
| 127 | - | ||
| 128 | - | ## Polish (only if you have time) | |
| 129 | - | ||
| 130 | - | ### 15. Routing is split across `router.js` and `navigation.js` with a `suppressPush` flag | |
| 131 | - | - **Category:** Internal consistency. (**Universal — code-only, not user-facing**) | |
| 132 | - | - **Observation:** `router.js` owns URL pushing/popstate; `navigation.js` owns view + pill activation. A `suppressPush` flag prevents loops. Functional but two layers and a flag is fragile — a future routing change has two places to land. | |
| 133 | - | - **Recommendation:** consolidate into one module. Not Phase 1 work; mark as a follow-up refactor. | |
| 134 | - | ||
| 135 | - | ### 16. Day-plan sidebar shares the "sidebar" name but isn't shell | |
| 136 | - | - **Category:** Consistency. (**Polish**) | |
| 137 | - | - **Observation:** `.day-plan-sidebar` is feature-local (`index.html:267–281`) but uses the word "sidebar" — which suggests app-wide chrome. The actual shell has no persistent sidebar. | |
| 138 | - | - **Recommendation:** rename to `.day-plan-aside` or `.day-plan-panel` to avoid implying chrome-ness. Touches one CSS class and a few JS references. | |
| 139 | - | ||
| 140 | - | ### 17. Mobile bottom-nav height couples with modal overlay bottom-padding | |
| 141 | - | - **Category:** Fragility. (**Polish**) | |
| 142 | - | - **Observation:** `styles.css:7355` hardcodes `bottom: calc(52px + env(safe-area-inset-bottom))` on the modal overlay to clear the 52px bottom-nav. If the nav height changes, two places must update. | |
| 143 | - | - **Recommendation:** extract `--mobile-tab-bar-h: 52px` as a token; consume from both rules. | |
| 144 | - | ||
| 145 | - | --- | |
| 146 | - | ||
| 147 | - | ## Cross-cutting / flat-design check | |
| 148 | - | ||
| 149 | - | The shell is on the right side of "flat with affordances": tabs have a filled-active state, pills have a filled-active state, buttons have neobrute offset shadows. Two flat-design risks did surface: | |
| 150 | - | ||
| 151 | - | - **Mobile tab active state** (Finding #4) — color-only encoding; too quiet. | |
| 152 | - | - **Sync dot** (Finding #2) — color-only state encoding with no text fallback. | |
| 153 | - | ||
| 154 | - | Both are color-as-sole-signal mistakes and both fail the same WCAG rule. Either alone is fixable; together they suggest a pattern — when shell affordances are minimized for cleanliness, state communication leans on color and breaks for theme + accessibility cases. Worth raising at the design-system layer: any *state* (active, syncing, error, selected) needs a non-color secondary signal (text, shape, position, weight). | |
| 155 | - | ||
| 156 | - | --- | |
| 157 | - | ||
| 158 | - | ## Summary | |
| 159 | - | ||
| 160 | - | The shell is structurally sound — three clear tabs, persistent global modals, hash-free URL routing, mobile bottom bar at a sensible breakpoint. The flaws cluster around three patterns: (1) **navigation surface compression on mobile** — pills aren't reachable from the bottom nav, project switching has no mobile presence; (2) **state encoding by color alone** — sync dot, active-tab color shifts; (3) **project context is in-memory chrome state, not URL** — switching projects requires four taps and survives reload only on `/project/{id}` routes. The single biggest user-facing fix is sticky pills on mobile (Finding #1). The single biggest trust fix is a real sync status surface (Finding #2). The single biggest workflow fix is a project chip in the page header (Findings #3, #7, #9). After those three, the shell is ready for Phase 2's deeper dive into the Tasks surface. |
| @@ -1,198 +0,0 @@ | |||
| 1 | - | # Phase 2 — Tasks Surface UX Audit | |
| 2 | - | ||
| 3 | - | **Scope:** the densest surface in GoingsOn — the task list (list + kanban), the task row and its actions, the task action sheet, the task detail view, the edit form, the subtasks modal, bulk actions, the filter bar, the snooze flow, time-tracking entry points, quick-add, and the recurrence editor. Shell chrome that wraps this surface was covered in Phase 1. | |
| 4 | - | ||
| 5 | - | **Stack:** Tauri 2 webview, vanilla HTML/CSS/JS. Files reviewed: `js/tasks.js`, `js/tasks-render.js`, `js/tasks-filter.js`, `js/tasks-kanban.js`, `js/task-board.js`, `js/task-forms.js`, `js/task-overview.js`, `js/bulk-actions.js`, `js/selection-manager.js`, `js/snooze.js`, `js/time-tracking.js`, `js/focus-timer.js`, `js/keyboard.js#openQuickAddModal`, `js/virtual-scroller.js`, `js/pagination-manager.js`, plus the matching CSS. | |
| 6 | - | ||
| 7 | - | **Method:** classical universal pass (Norman / Tognazzini / Raskin / HIG) plus the flat-design cross-cutting check. | |
| 8 | - | ||
| 9 | - | --- | |
| 10 | - | ||
| 11 | - | ## Surface snapshot | |
| 12 | - | ||
| 13 | - | - **Row anatomy:** 7-column grid — Description (with inline badges and started-icon), Project, Priority (letter only), Due, Recurs, Progress bar, Actions (checkbox + kebab). | |
| 14 | - | - **State classes** on `.task-row`: `.task-started` (green left stripe), `.task-completed` (50 % opacity + strike), `.task-overdue` (red description + due), `.task-snoozed` (badge), plus `.priority-high|medium|low` on the priority cell. | |
| 15 | - | - **Entry points to single-task actions:** kebab menu (`⋮`), right-click context menu (same handler), long-press on touch (same action sheet via `context-menus.js`), keyboard shortcuts (`c` complete, `s` snooze, `e` edit, `t` email-to-task), action sheet entries. | |
| 16 | - | - **New-task entry points:** `n` key, "+ New Task" button, `q` quick-add, mobile `+` button (Phase 1 #5), empty-state button. | |
| 17 | - | - **Bulk-actions bar:** appears when selection is non-empty; offers Complete · Set Project · Set Priority · Snooze · Delete · Select All. | |
| 18 | - | - **Detail surface:** a *separate full view* (`#task-overview`) navigated to via `taskOverview.open(id)`, not a side panel or drawer. The edit form is a modal opened separately. | |
| 19 | - | - **Pagination:** virtual scroller, 500-item server cap (`tasks.js:65`); no "load more" UI; no total count. | |
| 20 | - | - **Kanban:** toggle via List/Board pills; columns hard-coded as Pending / Started / Completed; drag-drop for status change. | |
| 21 | - | ||
| 22 | - | --- | |
| 23 | - | ||
| 24 | - | ## Critical (fix before declaring Phase 2 done) | |
| 25 | - | ||
| 26 | - | ### 1. Selection set persists across filter changes; bulk actions can hit invisible rows | |
| 27 | - | - **Category:** Forgiveness, Modes, Feedback. (**Universal**) | |
| 28 | - | - **Location:** `js/selection-manager.js` (no filter-change listener); `js/tasks-filter.js#applyFilters`. | |
| 29 | - | - **Observation:** when a user selects 12 rows, then changes the filter (e.g., switches status from Pending to Completed, or adds a project filter), the bulk-actions bar still reads "12 selected" but the visible rows no longer include some of those IDs. Clicking "Delete" then deletes the *invisible* rows. The selected set is not cleared on filter change; it is not even narrowed to the new visible set. | |
| 30 | - | - **Why it matters:** silent action on hidden rows is the canonical destructive-mistake category. There is no undo for bulk delete (Finding #2), so a user can lose 12 tasks they cannot see and cannot recover. | |
| 31 | - | - **Recommendation:** | |
| 32 | - | - On every filter / sort / view-mode change: clear the selection (preferred), or | |
| 33 | - | - Narrow the selection to the new visible set and update the bar to "N of M selected, N hidden by current filter" so the user can decide. | |
| 34 | - | - Either way, never run a bulk action against IDs that aren't currently visible without an explicit confirmation that names the hidden count. | |
| 35 | - | ||
| 36 | - | ### 2. Bulk Delete, Snooze, Set-Project, Set-Priority are not undoable; single-task Complete has undo | |
| 37 | - | - **Category:** Forgiveness, Consistency. (**Universal**) | |
| 38 | - | - **Location:** `js/tasks.js:603` shows `showUndoToast` after single-task complete; `js/bulk-actions.js:122–139` (delete) and `155–166` (project) lack undo wiring. | |
| 39 | - | - **Observation:** `GoingsOn.ui.showUndoToast` exists and is used after single-task completion. Bulk operations all skip it. Bulk Delete shows a confirm dialog; Bulk Snooze and Bulk Set-Project skip even that. Set-Project on 50 tasks irreversibly rewrites them with no toast preview and no undo. | |
| 40 | - | - **Why it matters:** the undo toast is the safety net that *enables* the speed of one-click destructive actions. Without it, users either become slow (re-confirming everything) or get burned. Inconsistent presence is worse than uniform absence — users learn "I can undo complete" and assume the same is true elsewhere. | |
| 41 | - | - **Recommendation:** | |
| 42 | - | - Wrap every bulk action in `showUndoToast` with a 10–15 s window. The Rust backend already supports the inverse operation (uncomplete, unsnooze, set-project-to-null, set-priority-to-previous) because the single-task path uses it. | |
| 43 | - | - Cluster the implementation: a single helper `bulkActionWithUndo(action, inverse, ids, prevState)` that captures pre-state and offers undo. Bulk Delete needs soft-delete semantics; verify backend. | |
| 44 | - | - This also resolves the deeper risk in Finding #1 — even a mis-targeted bulk action becomes recoverable. | |
| 45 | - | ||
| 46 | - | ### 3. Kanban view silently discards the status filter | |
| 47 | - | - **Category:** Visibility of state, Mappings. (**Universal**) | |
| 48 | - | - **Location:** `js/tasks-kanban.js#renderBoard`; status filter hidden on board mode but its value is not displayed elsewhere. | |
| 49 | - | - **Observation:** when the user switches from List to Board, the status filter dropdown is hidden and the board fetches *all statuses* (Pending + Started + Completed) regardless of what the filter was set to. There is no UI indication that the filter was overridden. Switching back to List re-applies the filter. A user who filtered to "Started" and then switched to board sees Completed cards mixed in and may not realize why. | |
| 50 | - | - **Why it matters:** silent state changes break the user's mental model. The first time this happens to a user with a screen full of completed cards in their "Started" view, they have to debug what changed. | |
| 51 | - | - **Recommendation:** | |
| 52 | - | - On switch to Board, show a small banner above the columns: "Showing all statuses on the board view — switch back to List to filter by status." | |
| 53 | - | - Or honor the status filter on the board too, hiding columns whose status isn't selected. | |
| 54 | - | - Prefer the latter: it preserves the user's filter intent across view modes. | |
| 55 | - | ||
| 56 | - | --- | |
| 57 | - | ||
| 58 | - | ## Major (high impact, lower urgency) | |
| 59 | - | ||
| 60 | - | ### 4. Task detail view is a separate route, not a side panel | |
| 61 | - | - **Category:** Modes, Modelessness (Raskin), Locus of attention. (**Universal**) | |
| 62 | - | - **Location:** `js/task-overview.js` switches the main view; `js/tasks.js` keeps the list as the main view. | |
| 63 | - | - **Observation:** clicking a row's description column navigates *away* from the task list to `#task-overview`. The list view tears down; coming back requires re-rendering, scroll position is lost (virtual scroller resets to top unless explicitly preserved). Most productivity apps with dense lists pair them with a right-side detail pane (Things, Linear, Mail) so users can scan rows + read details in one place. | |
| 64 | - | - **Why it matters:** users frequently triage — read row, glance at notes / time history / subtasks, decide action, move on. A full route swap forces a context-switch per task, which discourages triage and pushes everything into the edit form. | |
| 65 | - | - **Recommendation:** | |
| 66 | - | - Desktop: render task details in a right-side drawer (or below the list on narrow desktop) instead of a separate view. Click row → drawer opens; click another row → drawer updates in place; Esc closes; left/right keys move through rows. | |
| 67 | - | - Mobile: keep the full-view swap; it's the right shape there. | |
| 68 | - | - Out of scope for this audit: the `#task-overview` view is also where recurring-task streaks and the heatmap live; they are valuable and need a home in either the drawer or the edit modal. | |
| 69 | - | ||
| 70 | - | ### 5. Three time-tracking flows look like three features but share one backend | |
| 71 | - | - **Category:** Anticipation, Mappings, Consistency. (**Universal**) | |
| 72 | - | - **Location:** `js/time-tracking.js` (Track Time), `js/focus-timer.js` (Focus Mode), `js/day-planning.js` (Schedule Time Block). Action sheet exposes all three as peers. | |
| 73 | - | - **Observation:** the action sheet lists "Schedule Time Block · Track Time · Focus Mode (25/5)" as three sibling items. The first plans a block on the day; the second runs a live timer; the third runs a Pomodoro-style countdown. All three start the same backend `start_timer` call. Users cannot tell from the menu that Track Time and Focus Mode are variants of the same underlying timer, or that they can't run simultaneously (starting Focus while a Track Time timer runs *replaces* it without warning). | |
| 74 | - | - **Why it matters:** the todo file itself flagged this (`Sprint: Timer & Focus → Distinguish time tracking vs focus timer in UI (two features, no differentiation)`). Cognitive overhead per task: which of these three does the user want, and what happens to the other one if it's running? | |
| 75 | - | - **Recommendation:** | |
| 76 | - | - Action-sheet grouping: a "Time" subsection with one entry "Start timer" that opens a small dialog: "Live timer | 25 min focus | Custom Pomodoro". Schedule Time Block stays as a separate "Plan" entry because it doesn't run the timer. | |
| 77 | - | - When a timer is running, the menu entry says "Stop timer (12:34)" instead of offering to start another. | |
| 78 | - | - Surface the *single* running-timer state in the shell — the existing floating timer widget is good; make it more prominent (todo file already calls this out). | |
| 79 | - | ||
| 80 | - | ### 6. Priority is encoded as a one-letter abbreviation that depends on color to disambiguate | |
| 81 | - | - **Category:** Visibility, Accessibility (color as sole signal). (**Universal**) | |
| 82 | - | - **Location:** `js/tasks-render.js` renders the priority cell as `task.priority.charAt(0)` — `H`, `M`, `L`; styled by `.priority-high|medium|low`. | |
| 83 | - | - **Observation:** new users see "H" / "M" / "L" with no legend. The CSS supplies a color, but on certain themes (Catppuccin Latte, Flatwhite) the color contrast between High and Medium is subtle, and the letter is identical for High and Hard / Hold / Held etc. — there's no built-in semantic. | |
| 84 | - | - **Why it matters:** priority is one of the most-scanned attributes in a task list. Forcing the user to learn an abbreviation is a small but constant tax. Color-only encoding fails WCAG accessible-meaning. | |
| 85 | - | - **Recommendation:** | |
| 86 | - | - Render priority with a glyph + word affordance: a colored chevron-up / dash / chevron-down with the word "High" / "Medium" / "Low" at desktop widths; the chevron alone on narrow widths. | |
| 87 | - | - Or invert: use the full word as the cell content (`High`, `Med`, `Low`) and drop the color-only encoding to a tint of the cell background. | |
| 88 | - | - At minimum: add `title="Priority: High"` to the cell so hover reveals the meaning. | |
| 89 | - | ||
| 90 | - | ### 7. "Waiting Only" filter has no companion UI to set the waiting state | |
| 91 | - | - **Category:** Consistency, Anticipation. (**Universal**) | |
| 92 | - | - **Location:** `js/tasks-filter.js` — `filter-waiting` checkbox; `js/tasks.js` edit form has no "waiting" field; the waiting status is not in the form's select options. | |
| 93 | - | - **Observation:** the filter bar exposes a "Waiting Only" toggle, implying a binary "waiting" state on tasks. But there is no UI to *mark* a task as waiting — the edit form's status select is Pending / Started / Completed only. The waiting state must be set via the API or another path not visible in the frontend. | |
| 94 | - | - **Why it matters:** orphan filters are a stronger signal than they look — they tell users "this state exists, you just can't reach it from here." Either complete the loop (add a way to mark waiting) or remove the filter. | |
| 95 | - | - **Recommendation:** | |
| 96 | - | - If the waiting state is meaningful (e.g., "blocked by external party"), add a fourth status (`Waiting`) to the edit form's select and to the kanban columns. | |
| 97 | - | - If it's vestigial or rolled into Pending, remove the filter. | |
| 98 | - | - Either decision unblocks: the current half-state is the worst case. | |
| 99 | - | ||
| 100 | - | ### 8. Selection clears with no animation; bulk-actions bar pops in/out without transition | |
| 101 | - | - **Category:** Feedback, Locus of attention. (**Universal / Polish overlap**) | |
| 102 | - | - **Location:** `css/styles.css` `.bulk-actions-bar.hidden { display: none }`. | |
| 103 | - | - **Observation:** the bar is toggled via `.hidden` (display none/flex). Hard show/hide. When a user selects a row, the entire bottom of the screen rearranges with no transition. When the last selected row is deselected (or the filter clears the selection), the bar vanishes without warning. | |
| 104 | - | - **Why it matters:** sudden layout shifts in dense lists make users lose their place. Standard pattern is a slide-up / slide-down with a 150–200 ms ease. | |
| 105 | - | - **Recommendation:** add a transition on opacity + transform (`translateY`) tied to a `.is-visible` modifier. Cheap fix, big perceived-quality win. | |
| 106 | - | ||
| 107 | - | ### 9. Pagination is invisible; no total count, no end-of-list affordance | |
| 108 | - | - **Category:** Visibility of state, Feedback. (**Universal**) | |
| 109 | - | - **Location:** virtual scroller; `tasks.js` server cap of 500 (`response = await GoingsOn.api.tasks.listFiltered(...)` with no offset/limit indicator). | |
| 110 | - | - **Observation:** the list shows visible rows only; the user never sees "247 tasks" anywhere. If the user has more than 500 tasks, the server silently caps and the user gets a list that *ends* with no indication that more exist. (The todo file flags this: `Paginate beyond 500-item caps — tasks and emails silently cap`.) | |
| 111 | - | - **Why it matters:** users with large task lists make decisions based on volume ("I have 30 things this week"); a hidden total degrades planning. The silent cap is worse — a user with 600 tasks loses access to 100 of them with no error. | |
| 112 | - | - **Recommendation:** | |
| 113 | - | - Show a "247 tasks" count in the filter bar at all times. | |
| 114 | - | - When the cap is reached, show a banner at the end of the list: "Showing 500 of 600. Filter to narrow the list." | |
| 115 | - | - Long term: real pagination or fully-virtual server-side scrolling. The todo file has this queued. | |
| 116 | - | ||
| 117 | - | ### 10. Filter state lives only in the DOM; reload resets all filters | |
| 118 | - | - **Category:** Persistence, Anticipation. (**Universal**) | |
| 119 | - | - **Location:** `js/tasks-filter.js#getFilters` reads `document.getElementById('filter-status').value` directly; no localStorage write; no URL sync. | |
| 120 | - | - **Observation:** a user who filters to "Project A, Priority High, Waiting" and reloads the app gets the default Pending-all-projects view back. The URL doesn't carry the filter; localStorage isn't used. | |
| 121 | - | - **Why it matters:** a user who has built up a daily-triage filter has to rebuild it every cold start. This is also the root cause of the "reload loses project context" issue raised in Phase 1 #9. | |
| 122 | - | - **Recommendation:** | |
| 123 | - | - Mirror the active filter state into `location.search` (`?status=pending&project=ABC&priority=high&waiting=1`) so deep-linking and reload preserve it. | |
| 124 | - | - As a quicker stopgap: write to localStorage under `goingson.taskFilters` and rehydrate on init. | |
| 125 | - | - Either way, the design-system charter should add a "filter state persistence" rule for all Phase-N audits. | |
| 126 | - | ||
| 127 | - | --- | |
| 128 | - | ||
| 129 | - | ## Minor (worth fixing during normal cleanup) | |
| 130 | - | ||
| 131 | - | ### 11. Kebab button and right-click context menu are the same target | |
| 132 | - | - **Category:** Consistency, Locus of attention. (**Polish/Universal**) | |
| 133 | - | - **Location:** `js/tasks-render.js` — both `oncontextmenu` on the row and `onclick` on the kebab call `GoingsOn.contextMenus.showTask(event, id)`. | |
| 134 | - | - **Observation:** the kebab and the right-click both produce the same menu. Discoverable for the user who tries either, but the kebab is redundant on desktop where right-click is universal, and the kebab is the only path on touch (since right-click doesn't exist). Action sheet vs context menu split is handled at dispatch. | |
| 135 | - | - **Recommendation:** keep both. But document the precedence in the design-system charter: "kebab on touch; right-click on desktop; both are accelerators for the same context-menu API." Worth flagging because future contributors may try to add a third entry point. | |
| 136 | - | ||
| 137 | - | ### 12. Quick-add syntax hints fade as you type, removing the reference exactly when needed | |
| 138 | - | - **Category:** Anticipation (Tog), Discoverability. (**Universal/Polish**) | |
| 139 | - | - **Location:** `js/keyboard.js:290–309` — token-by-token highlighting + fade of inactive tokens. | |
| 140 | - | - **Observation:** the syntax hint row (`@` project / `#` tag / `+` priority / `due:` …) starts evenly weighted, but as the user types a `@`, the other tokens drop to 0.4 opacity. This is the wrong direction — a user who just learned to type `@` is *more* likely to want to know what else is available, not less. The remaining hints get dimmer the more the user types. | |
| 141 | - | - **Recommendation:** invert the highlight — bold the active token, but keep the others at full opacity (or 0.85). The current pattern was probably borrowed from "current step is highlighted" wizards, where the past steps are done; here every token is independent and any can appear at any cursor position. | |
| 142 | - | ||
| 143 | - | ### 13. Snooze badge is text-only and indistinguishable from other inline badges | |
| 144 | - | - **Category:** Visibility, Hierarchy. (**Polish**) | |
| 145 | - | - **Location:** `js/tasks-render.js` — snooze badge inline in description cell. | |
| 146 | - | - **Observation:** the snooze badge renders as `<span class="snooze-badge">Snoozed</span>` alongside the subtask count (`2/4`), contact name (`Alice`), and time badge (`15m`). All inline in the same description cell with similar styling. Snooze is a row *state* (hidden until X) but reads like just another piece of metadata. | |
| 147 | - | - **Recommendation:** give snooze its own visual treatment — left-edge stripe like `.task-started` but in `--accent-yellow`, or fade the entire row to ~70 % opacity (it's effectively hidden until X, treat it like a soft archive). Snooze is information-dense enough to deserve its own state class, not a badge among other badges. | |
| 148 | - | ||
| 149 | - | ### 14. Selection range hint shown once and lost | |
| 150 | - | - **Category:** Discoverability. (**Polish**) | |
| 151 | - | - **Location:** `js/selection-manager.js` — one-time hint after first selection. | |
| 152 | - | - **Observation:** "Shift-click to select a range of items" is shown the first time a user selects a row. Stored in localStorage; never shown again. A user who misses it has no other affordance for the feature. | |
| 153 | - | - **Recommendation:** add a tiny "?" pip on the bulk-actions bar that pops a small popover with "Tips: Shift-click for range, Cmd/Ctrl-click for multi, Select All button." Persistent affordance for an accelerator. | |
| 154 | - | ||
| 155 | - | ### 15. Bulk Set-Project picker doesn't scroll/search for long project lists | |
| 156 | - | - **Category:** Anticipation, Fitts. (**Universal/Polish**) | |
| 157 | - | - **Location:** `js/bulk-actions.js#setProjectTasks` — inline buttons, max-height 300 px via `.bulk-modal-scroll`. | |
| 158 | - | - **Observation:** the modal lists projects as a vertical column of buttons inside a 300 px-tall scroll container. With 30+ projects, finding the right one means scrolling without search. The single-task edit form uses a `<select>` for the same field. | |
| 159 | - | - **Recommendation:** match the single-task form — use a `<select>` (or a typeahead) in the bulk modal too. Cheaper to use than a button column when N > ~8. | |
| 160 | - | ||
| 161 | - | --- | |
| 162 | - | ||
| 163 | - | ## Polish (only if you have time) | |
| 164 | - | ||
| 165 | - | ### 16. Empty state for new users doesn't surface the `q` quick-add shortcut | |
| 166 | - | - **Category:** Discoverability. | |
| 167 | - | - **Location:** `js/tasks.js:127–130` — new-user empty state has a "New Task" button only. | |
| 168 | - | - **Observation:** the empty state has the "New Task" button but doesn't mention `q` (quick-add) or the Cmd-K search, even though both are faster than the modal. The welcome modal *does* mention `q` (Phase 1 found this), but a user who skipped welcome lands here. | |
| 169 | - | - **Recommendation:** add a one-liner under the button: "or press <kbd>q</kbd> for quick add" (touch devices hide the kbd hint). | |
| 170 | - | ||
| 171 | - | ### 17. Recurrence preview is text-only; no visual calendar | |
| 172 | - | - **Category:** Anticipation. | |
| 173 | - | - **Location:** `js/task-forms.js` — preview line "Repeats every 2 weeks on Mon, Wed, Fri". | |
| 174 | - | - **Observation:** the user types in the rule, the preview spells it out as English. For complex rules (every 2nd Thursday of the month, last weekday of the month), a small 5-week mini-calendar showing the next 5 generated occurrences would catch off-by-one mistakes the text preview hides. | |
| 175 | - | - **Recommendation:** below the text preview, render a strip of "Next: Mar 12, Mar 26, Apr 9…" — pure data, no calendar grid needed. Three lines of JS. | |
| 176 | - | ||
| 177 | - | ### 18. Two ways to open the subtasks modal; both close it on "Track Time" | |
| 178 | - | - **Category:** Consistency, Locus of attention. | |
| 179 | - | - **Location:** `js/tasks-render.js#renderSubtasksModal` — Track Time button has `onclick="GoingsOn.ui.closeModal(); GoingsOn.timeTracking.startTimer(...)"`. | |
| 180 | - | - **Observation:** opening the subtasks modal from either kanban or action sheet leads to the same UI. Clicking Track Time (or Focus, or Schedule) closes the subtasks modal. If the user wanted to keep referencing subtasks while timing, they can't — modal closes to make room for the timer overlay. | |
| 181 | - | - **Recommendation:** the timer widget is bottom-fixed; the subtasks modal could stay open. Only close the modal if the user explicitly cancels. Small win that becomes a big quality-of-life upgrade once users actually use both features in the same session. | |
| 182 | - | ||
| 183 | - | --- | |
| 184 | - | ||
| 185 | - | ## Cross-cutting / flat-design check | |
| 186 | - | ||
| 187 | - | The tasks surface has more state encoding than the shell, and most of it works: filled-left-border for Started, opacity-strike for Completed, red text for Overdue. Two flat-design risks recur: | |
| 188 | - | ||
| 189 | - | - **Priority cell** is colored letter only — same color-as-sole-signal problem as the Phase 1 sync dot. | |
| 190 | - | - **Snooze badge** is monochrome text inline with other monochrome text — no shape/position to distinguish it. | |
| 191 | - | ||
| 192 | - | Both fit the pattern noted in Phase 1: when state is encoded by color alone, the design system pays an accessibility tax and a theme-fragility tax. The fix is to pair every color with a non-color signal (icon, shape, position, weight). | |
| 193 | - | ||
| 194 | - | --- | |
| 195 | - | ||
| 196 | - | ## Summary | |
| 197 | - | ||
| 198 | - | The tasks surface is functionally complete and structurally well-organized: row → action sheet → form is a clean path, virtual scrolling holds up for medium lists, and the keyboard shortcuts are dense and usable. The failure modes cluster around **data safety** (selection-bleed across filters, no undo on bulk operations, silent cap at 500 tasks), **filter persistence** (everything resets on reload — same root cause as Phase 1 #9), and **state communication** (priority and snooze read as decoration, kanban silently drops the status filter). The single highest-priority fix is bulk-action undo + selection-clear-on-filter (Findings #1 + #2 together) — they remove the only class of *unrecoverable* user mistake in this surface. The single biggest quality-of-life improvement is a side-drawer task detail (#4) — it makes triage modeless. The single most overdue cleanup is the orphan "Waiting Only" filter (#7) — either complete the loop or delete it. |
| @@ -1,216 +0,0 @@ | |||
| 1 | - | # Phase 3 — Compose & Email UX Audit | |
| 2 | - | ||
| 3 | - | **Scope:** the email surface — list, reader / thread, compose window (desktop) and compose modal (mobile), drafts, autocomplete, attachments, labels, folder filter, search-within-emails, snooze, the reader action bar, send / error / retry behavior. Email-account management was covered in Phase 0 — out of scope here. | |
| 4 | - | ||
| 5 | - | **Stack:** Tauri 2 webview, vanilla HTML/CSS/JS. Files reviewed: `compose.html`, `js/emails.js`, `js/autocomplete.js`, `js/address-highlight.js`, the `#messages-view` block in `index.html`, plus the matching CSS in `css/styles.css`. | |
| 6 | - | ||
| 7 | - | **Method:** classical universal pass (Norman / Tognazzini / Raskin / HIG) plus the flat-design cross-cutting check. | |
| 8 | - | ||
| 9 | - | --- | |
| 10 | - | ||
| 11 | - | ## Surface snapshot | |
| 12 | - | ||
| 13 | - | - **List row:** `.email-item` with from / subject / 100-char preview / date / thread-count badge / labels / snooze badge. Unread state = 4 px blue left border + bold + light-blue tint. **No archived indicator** in list; only in reader. | |
| 14 | - | - **Reader:** large modal. Header (subject + thread count, meta line, contact card), pooled attachments block, thread messages (oldest first; latest gets a 3 px blue stripe), action bar with up to 10 controls. | |
| 15 | - | - **Compose surface:** *two* implementations. Desktop opens a **separate Tauri window** (`compose.html`) with its own inline `<style>` block, autocomplete, address highlighting, Cmd+S / Cmd+Enter shortcuts. Mobile opens an **in-app modal** built with the form-builder; same fields, fewer affordances. | |
| 16 | - | - **Send flow:** validation → status bar "Sending…" → success "Email sent!" + auto-close after 1 s → status bar red error if fails. No undo-send. No retry button. | |
| 17 | - | - **Drafts:** autosaved every 2 s after typing, accessed via a "Drafts" header button → modal list → click to resume in compose window (desktop) or modal (mobile). | |
| 18 | - | - **Filters:** folder dropdown, label dropdown, in-pane search input (debounced 250 ms, FTS5). | |
| 19 | - | - **Cross-cuts:** snooze delegated to the shared snooze module (same modal as tasks); label entry from the Actions dropdown (`Edit Labels`). | |
| 20 | - | ||
| 21 | - | --- | |
| 22 | - | ||
| 23 | - | ## Critical (fix before declaring Phase 3 done) | |
| 24 | - | ||
| 25 | - | ### 1. Send is final — no undo-send, no soft delay | |
| 26 | - | - **Category:** Forgiveness. (**Universal**) | |
| 27 | - | - **Location:** `compose.html#sendEmail` lines ~430–467; `js/emails.js` mobile modal send path. | |
| 28 | - | - **Observation:** clicking Send goes straight to `GoingsOn.api.emails.send(...)`. The status bar shows "Sending…", then "Email sent!" + the window auto-closes after 1 second. The user has no way to cancel between clicking Send and the SMTP handoff. After the window closes, the message is on its way irrevocably. | |
| 29 | - | - **Why it matters:** undo-send is a 2010s expectation — Gmail, Apple Mail, Outlook, Fastmail all offer a 5–30 s delay window. Without it, every "oh wait I meant to add Bob" is a recall email. For an app whose pitch includes "manage your inbox calmly," not having undo-send is a missing primary feature. | |
| 30 | - | - **Recommendation:** | |
| 31 | - | - Add a `send-with-delay` queue: on Send, write the message to a local pending-send queue and start a 5 s (configurable: 5/10/20 s) countdown. During the countdown, surface a persistent toast with "Sending in 4… Undo". On expiry, hand off to SMTP. | |
| 32 | - | - The compose window closes immediately; the toast lives in the main app shell. | |
| 33 | - | - On Undo, requeue the draft (re-open the compose window pre-filled). | |
| 34 | - | - Backend has the primitives — drafts already exist; this is mostly a frontend queue + a delay. | |
| 35 | - | ||
| 36 | - | ### 2. No client-side attachment-size warning; send fails on the server with a generic error | |
| 37 | - | - **Category:** Anticipation (Tog), Error messages. (**Universal**) | |
| 38 | - | - **Location:** `compose.html#pickAttachment`, `compose.html#sendEmail`. | |
| 39 | - | - **Observation:** the file picker accepts any file. There is no client-side size check, no aggregate-size readout, no per-file warning. The user can attach 80 MB; on Send, the server enforces its limits and returns an error. The status bar shows "Failed to send: [server error string]" — which may or may not include the size hint. The user has uploaded that 80 MB through the local IPC for nothing. | |
| 40 | - | - **Why it matters:** SMTP servers typically cap at 25 MB (some 35 MB or 10 MB depending on provider). A user attaches a 60 MB video, types a long message, hits Send, gets a cryptic error, and now has to remove the attachment without losing the message. The error happens *after* the user committed mentally to sending. | |
| 41 | - | - **Recommendation:** | |
| 42 | - | - Show running attachment total below the attachments bar: "Total: 4.2 MB / 25 MB". Update as files are added/removed. | |
| 43 | - | - At a configurable warning threshold (default 20 MB), pre-warn in the status bar: "Large attachment — your mail server may reject this." | |
| 44 | - | - At a hard threshold (default 25 MB), disable Send and surface a specific error: "Attachments exceed 25 MB — most mail servers will reject this. Try a file share link instead." | |
| 45 | - | - The cap per account should come from the account settings (account already has IMAP/SMTP configured; reasonable defaults work). | |
| 46 | - | ||
| 47 | - | ### 3. Two parallel compose implementations with feature drift | |
| 48 | - | - **Category:** Consistency, Modes. (**Universal**) | |
| 49 | - | - **Location:** `compose.html` (desktop, separate Tauri window) vs. `js/emails.js` lines ~150–280 (mobile, in-app modal built by `GoingsOn.ui.openFormModal`). | |
| 50 | - | - **Observation:** the two compose surfaces share the same fields but diverge: | |
| 51 | - | - Desktop has **address highlighting** (`address-highlight.js`) — red for malformed, blue for known contact. Mobile does not. | |
| 52 | - | - Desktop has **Cmd+S** (save draft) and **Cmd+Enter** (send). Mobile has no keyboard shortcuts. | |
| 53 | - | - Desktop has an **attachments bar with remove buttons** styled `.compose-attachment-item`. Mobile renders attachments inline in the form as `.email-attachment-tag` chips. | |
| 54 | - | - Desktop has the **`showSaveContactPrompt`** post-send bar. Mobile shows a toast. | |
| 55 | - | - Desktop autosaves drafts via a 2 s debounce; mobile autosaves via the form-modal's own dirty tracking (verify; the form-builder doesn't have autosave by default). | |
| 56 | - | - Desktop autocomplete is its own local copy; main-app autocomplete is in `js/autocomplete.js`. | |
| 57 | - | - **Why it matters:** every fix to compose has to be done twice. Address highlighting and keyboard shortcuts being desktop-only is the kind of disparity that survives because mobile users don't complain — they assume the app just doesn't have it. | |
| 58 | - | - **Recommendation:** | |
| 59 | - | - Unify on one component. Two options: | |
| 60 | - | - **A.** Have mobile load `compose.html` in a webview/modal too (Tauri 2 supports multiple webviews per window). One codebase, one feature set, one autocomplete. | |
| 61 | - | - **B.** Extract compose into a shared module (`js/compose-form.js`) used by both `compose.html` and the mobile modal — markup-builder helpers + behavior modules that can be mounted in either context. | |
| 62 | - | - **B is the cheaper path**: extract address highlighting, autocomplete wire-up, autosave, keyboard shortcuts, and attachment management into module-level functions; both shells import. | |
| 63 | - | - Either way, the design-system charter should add a rule: "Compose is one component. Surface-specific behavior (window chrome, etc.) wraps it, but the form lives in one place." | |
| 64 | - | ||
| 65 | - | --- | |
| 66 | - | ||
| 67 | - | ## Major (high impact, lower urgency) | |
| 68 | - | ||
| 69 | - | ### 4. Reader action bar carries 10 controls with no hierarchy | |
| 70 | - | - **Category:** Hierarchy, Fitts. (**Universal / Flat**) | |
| 71 | - | - **Location:** `js/emails.js` lines ~431–460, `.email-actions-bar` styling. | |
| 72 | - | - **Observation:** the bottom action bar of the reader contains, in order: Reply, Reply All, Forward, Delete (red), Archive/Unarchive, Snooze/Unsnooze, Create Task, an Actions ▾ dropdown (with Convert to Task, Convert to Event, Edit Labels, Move to Folder), and Open in Browser. Ten visible controls, no primary/secondary visual split — only the Delete button is colored (red). Reply is `.btn-primary` but the visual difference between primary and secondary is small enough that Reply doesn't dominate. | |
| 73 | - | - **Why it matters:** the most common action (Reply, Archive) gets the same weight as the least common (Open in Browser). New users have to read every button; experienced users have to scan past the noise. | |
| 74 | - | - **Recommendation:** | |
| 75 | - | - **Primary cluster (left):** Reply · Reply All · Forward — these are 90 % of opens, group them together visually. | |
| 76 | - | - **State actions (middle):** Archive · Snooze · Mark Unread — these change the message's state. | |
| 77 | - | - **Overflow (right):** an Actions ▾ menu absorbing Delete, Create Task, Convert to Event, Edit Labels, Move to Folder, Open in Browser. Delete is destructive — it doesn't need to be top-level if the menu is one click away. | |
| 78 | - | - Promote Reply visually with a filled accent treatment; demote the rest to ghost buttons. | |
| 79 | - | ||
| 80 | - | ### 5. "Create Task" appears twice with subtly different paths | |
| 81 | - | - **Category:** Consistency, Modes. (**Universal**) | |
| 82 | - | - **Location:** `js/emails.js` lines ~439 and ~451–453. | |
| 83 | - | - **Observation:** the action bar has a top-level "Create Task" button that calls `createTaskFromEmail(id)`; the Actions ▾ dropdown contains a "Convert to Task" item that calls the same function. They produce identical results. Same for "Convert to Event" which has no top-level equivalent — making the dropdown inconsistent (Task is both top-level and in the menu; Event is only in the menu). | |
| 84 | - | - **Why it matters:** twin entries split the user's discovery — half the users use the top-level button, the other half open the dropdown looking for it. Future maintainers add a new place to invoke it and the count grows. | |
| 85 | - | - **Recommendation:** keep "Create Task" top-level (it's a primary cross-feature pivot in GoingsOn). Remove the duplicate from the dropdown. Add "Create Event" top-level next to it so the parallel actions live side-by-side. | |
| 86 | - | ||
| 87 | - | ### 6. Status bar uses color as the only success/error signal | |
| 88 | - | - **Category:** Visibility, Accessibility. (**Universal / Flat — recurring pattern**) | |
| 89 | - | - **Location:** `compose.html` `.status-bar.error` / `.status-bar.success` rules. | |
| 90 | - | - **Observation:** the compose-window status bar at the bottom flips between grey (default), green (success), red (error). The text changes too ("Sending…" / "Email sent!" / "Failed to send: …"), so this isn't *purely* color-coding — but the visual scan signal is the color. On dark themes with low-contrast greens/reds, or for colorblind users, the state difference between "Sending…" (grey) and "Email sent!" (green) is hard to distinguish at a glance. | |
| 91 | - | - **Recommendation:** add a leading status glyph — `· Ready`, `⟳ Sending…`, `✓ Email sent!`, `✕ Failed: …` — paired with the color. Same pattern as Phase 1 #2 (sync indicator) and Phase 2 #6 (priority): never let color be the sole state signal. | |
| 92 | - | ||
| 93 | - | ### 7. Address validation only in the desktop compose window | |
| 94 | - | - **Category:** Anticipation, Forgiveness. (**Universal**) | |
| 95 | - | - **Location:** `js/address-highlight.js` — only wired in `compose.html`; mobile compose modal in `emails.js` doesn't import it. | |
| 96 | - | - **Observation:** mobile compose lets users type `bob@@example` or `friend at gmail.com` with no warning. Server returns "invalid address" on send. The desktop window catches this before send with a red highlight. | |
| 97 | - | - **Why it matters:** mobile users are *more* likely to typo addresses (small keyboard, autocorrect). The lack of pre-send validation there is upside-down. | |
| 98 | - | - **Recommendation:** part of Finding #3 (unify compose). Until then, port `address-highlight.js` to the mobile form-modal — the autocomplete wrapper already supports per-input mounting; this is a 30-line patch. | |
| 99 | - | ||
| 100 | - | ### 8. Filter state (folder, label, search) doesn't persist across reload | |
| 101 | - | - **Category:** Persistence, Anticipation. (**Universal**) | |
| 102 | - | - **Location:** `js/emails.js` — `activeFolder` / `activeLabel` are module-level vars; no localStorage write; no URL sync. | |
| 103 | - | - **Observation:** identical pattern to Phase 1 #9 and Phase 2 #10. A user filtered to "Work label, INBOX folder, searching 'invoice'" loses all of that on reload. | |
| 104 | - | - **Recommendation:** mirror the active state into `location.search` (`?folder=INBOX&label=work&q=invoice`) on every filter change. One implementation can serve tasks and emails — pull into a shared `query-state.js` helper. This finding plus Phase 1 #9 and Phase 2 #10 are the same bug in three places; a single fix should clear them all. | |
| 105 | - | ||
| 106 | - | ### 9. Snooze badge in list uses the same yellow as label badges and isn't distinguishable | |
| 107 | - | - **Category:** Visibility, Hierarchy. (**Flat / Universal**) | |
| 108 | - | - **Location:** `js/emails.js` line ~135, `.snooze-badge` styling. | |
| 109 | - | - **Observation:** the snooze badge appears inline in the email header row alongside label badges (`.email-label-badge`). Both are small chips. The snooze badge is yellow-tinted; some labels are also yellow if the user names a label that way (or if the label-badge color cycles). At a glance, "Snoozed" reads like a custom label. | |
| 110 | - | - **Recommendation:** | |
| 111 | - | - Move the snooze indicator out of the badge area entirely: dim the entire `.email-item.email-snoozed` row to ~70 % opacity and add a small "💤" or clock icon at the right edge, paired with the snoozed-until time. The visual treatment should match `.task-snoozed` once Phase 2 #13 lands — they're the same conceptual state. | |
| 112 | - | - Reserve the badge area for labels. | |
| 113 | - | ||
| 114 | - | ### 10. Send failure leaves the user with no recovery beyond "click Send again" | |
| 115 | - | - **Category:** Error messages, Forgiveness. (**Universal**) | |
| 116 | - | - **Location:** `compose.html#sendEmail` error branch (lines ~469–471). | |
| 117 | - | - **Observation:** on send failure, the status bar shows "Failed to send: [server error]" in red and the compose window remains open. There is no retry button; no inspect-the-error affordance; no "Save Draft and try later" prompt. If the user closes the window after a failure, autosave (2 s debounce) *may* have saved a draft, but timing is fragile — a fast typer can fail-send, panic, close, and lose data. | |
| 118 | - | - **Recommendation:** | |
| 119 | - | - On send failure: surface a one-line error + two buttons inline above the status bar — `Retry` and `Save Draft & Close`. | |
| 120 | - | - On window close while there is unsent content, always save a draft (no debounce dependence). Show a confirmation: "Saved as draft — restore from the Drafts list." | |
| 121 | - | - If the error is a known transient (network / TLS handshake / 421 / 4xx temporary), retry automatically once after 2 s before surfacing. | |
| 122 | - | ||
| 123 | - | ### 11. Compose opens even when no email accounts are configured; the wall is at Send, not entry | |
| 124 | - | - **Category:** Anticipation, Visibility of state. (**Universal**) | |
| 125 | - | - **Location:** `compose.html#init` lines ~330–340, `.from-account` select default "Loading accounts…" → "No email accounts configured. Add one in Settings." | |
| 126 | - | - **Observation:** clicking Compose with zero accounts opens the full compose window. The From dropdown ends up empty; the status bar says "No email accounts configured." Send is disabled — the user has typed a message into a UI that was never going to send it. | |
| 127 | - | - **Why it matters:** the most useful state-visibility move is to *prevent* the dead-end flow rather than recover from it. Showing a blocking error after the user composed a message is wasted work. | |
| 128 | - | - **Recommendation:** | |
| 129 | - | - Disable the Compose buttons (header, list empty state, reply-from-reader) when no accounts exist; instead, surface a single CTA: "Add an email account to start composing". | |
| 130 | - | - If the user gets to the compose window via a reply path (which can't happen without an account already — the email they're replying to had to be received), nothing changes. | |
| 131 | - | ||
| 132 | - | --- | |
| 133 | - | ||
| 134 | - | ## Minor (worth fixing during normal cleanup) | |
| 135 | - | ||
| 136 | - | ### 12. CC/BCC toggle button doesn't update its label after expanding | |
| 137 | - | - **Category:** Feedback, Consistency. (**Polish/Universal**) | |
| 138 | - | - **Location:** `compose.html` `#toggle-cc` — `onclick="toggleCcBcc()"`. | |
| 139 | - | - **Observation:** the button reads "Show CC/BCC" before clicking; after clicking, CC and BCC rows appear but the button text still says "Show CC/BCC" rather than "Hide CC/BCC". (Verify via the toggleCcBcc implementation.) | |
| 140 | - | - **Recommendation:** flip the label on toggle. Standard pattern. | |
| 141 | - | ||
| 142 | - | ### 13. Email link extraction renders `<span class="email-link">` instead of `<a href>` | |
| 143 | - | - **Category:** Affordances. (**Universal/Polish**) | |
| 144 | - | - **Location:** `js/emails.js#formatEmailBody` (or `utils.js`), `.email-link` CSS. | |
| 145 | - | - **Observation:** links in email bodies are detected and styled to look like links (blue, underline), but rendered as `<span>` not `<a>`. They're not clickable in the webview — at least not via the browser's default link behavior. The codebase may attach a click handler elsewhere; verify. If not, users see links but can't open them. | |
| 146 | - | - **Recommendation:** render as `<a href target="_blank" rel="noopener">` and let Tauri's `tauri-plugin-shell` (or the existing `allowedExternal` config) handle external opens. If the current `<span>` is a security measure to prevent HTML-injection, the security goal is met by `escapeHtml` *before* link extraction — the link can be a real anchor. | |
| 147 | - | ||
| 148 | - | ### 14. Archive state isn't shown in the list, only in the reader | |
| 149 | - | - **Category:** Visibility of state. (**Polish**) | |
| 150 | - | - **Location:** `js/emails.js` row render — no archived-state indicator. | |
| 151 | - | - **Observation:** the list filters out archived emails by default. If a user opens "All folders" or searches and an archived email appears, there is no visual cue — the row looks identical to an inbox email. The reader meta-line shows "· *Archived*" but only after open. | |
| 152 | - | - **Recommendation:** add a small archive icon or `.email-item.archived` styling (low-saturation tint, or a tiny boxed "📦" / "Archived" at the right edge of the row). | |
| 153 | - | ||
| 154 | - | ### 15. "Open in Browser" is ambiguous | |
| 155 | - | - **Category:** Mappings, Anticipation. (**Polish**) | |
| 156 | - | - **Location:** Reader action bar, last button. | |
| 157 | - | - **Observation:** the button label is "Open in Browser". A user might reasonably guess: (a) open the email's HTML body in the system browser, (b) open the original Gmail/Fastmail/whatever web URL, (c) view the email source. The implementation likely does (a). The label doesn't pin it down. | |
| 158 | - | - **Recommendation:** rename to "Open HTML in Browser" or "View HTML version". If the feature is rarely used, demote to the Actions ▾ menu (folds into Finding #4). | |
| 159 | - | ||
| 160 | - | ### 16. Reply quoted body uses bare `>` prefix without line-wrapping | |
| 161 | - | - **Category:** Consistency, Polish. (**Polish**) | |
| 162 | - | - **Location:** `js/emails.js#reply` body construction. | |
| 163 | - | - **Observation:** the quoted body is built as `> ` prefix per line of the original. Long lines stay long; sequences of `>` for nested replies accumulate awkwardly (`>>>>>>` in long threads). Standard mail-client behavior is to re-flow at 72/76 columns. | |
| 164 | - | - **Recommendation:** wrap quoted lines at 72 columns, preserving paragraph breaks. Trim trailing whitespace. | |
| 165 | - | ||
| 166 | - | ### 17. Drafts modal lists drafts but doesn't show *which account* drafted them | |
| 167 | - | - **Category:** Visibility. (**Polish**) | |
| 168 | - | - **Location:** `js/emails.js#openDraftsModal` line ~910 — meta shows "To: ... · date". | |
| 169 | - | - **Observation:** a user with multiple accounts sees drafts but doesn't see which account each draft is From. Opening the draft reveals it. | |
| 170 | - | - **Recommendation:** add "From: account-name" to the draft row meta. Two lines instead of one; small change. | |
| 171 | - | ||
| 172 | - | ### 18. Selection in email list doesn't have the same shift-click range/select-all features as tasks | |
| 173 | - | - **Category:** Consistency. (**Polish**) | |
| 174 | - | - **Location:** `js/emails.js` bulk-actions wiring; `js/selection-manager.js` may be reused. | |
| 175 | - | - **Observation:** verify whether the email `SelectionManager` instance supports shift-click ranges and the Select-All button. The infrastructure exists for tasks. If emails skip it, that's a parity gap. | |
| 176 | - | - **Recommendation:** reuse the `SelectionManager` from Phase 2 wholesale; same UI hint, same bulk-actions bar shape. | |
| 177 | - | ||
| 178 | - | --- | |
| 179 | - | ||
| 180 | - | ## Polish (only if you have time) | |
| 181 | - | ||
| 182 | - | ### 19. Plain-text body with no editor affordances; no indication that rich text is unsupported | |
| 183 | - | - **Category:** Anticipation. | |
| 184 | - | - **Location:** `compose.html` body `<textarea>`; signature appending in plain text. | |
| 185 | - | - **Observation:** the body is a `<textarea>`. There is no formatting toolbar, no "B / I / U" buttons, no Markdown hint, no shortcut for inserting links. This is fine as a design choice ("plain text by default"), but there's no hint to users who type `**bold**` expecting it to render — it won't on the recipient's side either. Users may discover this only when their recipient asks "why is your email full of stars?" | |
| 186 | - | - **Recommendation:** a faint inline hint below the toolbar: "Plain text — no Markdown or rich formatting." Or surface a single Markdown-on toggle for users who want it (the recipient will see raw Markdown unless the server renders). | |
| 187 | - | ||
| 188 | - | ### 20. Send button has no keyboard label / shortcut hint on mobile | |
| 189 | - | - **Category:** Discoverability. | |
| 190 | - | - **Location:** Mobile compose modal — no `<kbd>` hint next to Send. | |
| 191 | - | - **Observation:** desktop has Cmd+Enter to send; mobile doesn't expose any shortcut. Mobile users don't have a hardware keyboard usually, so this is fine, but if a user pairs a Bluetooth keyboard with their phone, the same shortcut should work. | |
| 192 | - | - **Recommendation:** wire Cmd+Enter / Ctrl+Enter into the mobile modal too. Already implemented in compose.html; the form-builder doesn't propagate. | |
| 193 | - | ||
| 194 | - | ### 21. No search progress indicator | |
| 195 | - | - **Category:** Feedback. | |
| 196 | - | - **Location:** `js/emails.js#search` — debounced 250 ms. | |
| 197 | - | - **Observation:** search results replace the list after the FTS5 query returns. For large mailboxes, this may be 500 ms+. No spinner; the list stays as-is until results land. | |
| 198 | - | - **Recommendation:** show a tiny spinner in the search input's right edge during the in-flight query. | |
| 199 | - | ||
| 200 | - | --- | |
| 201 | - | ||
| 202 | - | ## Cross-cutting / flat-design check | |
| 203 | - | ||
| 204 | - | The email surface shows the same patterns Phase 1 and Phase 2 identified, now appearing for the third time: | |
| 205 | - | ||
| 206 | - | - **State-by-color-alone** — status bar, snooze badge, send-state colors. | |
| 207 | - | - **Filter state not persisted across reload** — same root cause as tasks filters. | |
| 208 | - | - **Action overload in dense bars** — 10-button action bar parallels the bulk-actions bar problem in Phase 2 #15. | |
| 209 | - | ||
| 210 | - | When a pattern shows up across three independent phases, it's not a per-surface bug — it belongs in the design-system layer. The remediation plan's Step 8 (theme coverage) handled one half (color-only ↔ state); the missing half is filter state. Recommend adding to the design-system charter a rule: "every filter / sort / view-mode setting must be mirrored to the URL on change." | |
| 211 | - | ||
| 212 | - | --- | |
| 213 | - | ||
| 214 | - | ## Summary | |
| 215 | - | ||
| 216 | - | Email is the riskiest surface in GoingsOn — the todo file already flags it ("Email is the riskiest area — data loss potential + OAuth friction") — and the audit confirms three classes of risk: **send irreversibility** (no undo-send, no soft delay, no failure retry, no client-side size warning); **two compose UIs that drift** (desktop has validation and shortcuts, mobile doesn't); **action-bar density** in the reader (10 buttons with weak hierarchy and a duplicate Create Task entry). The single biggest user-trust fix is undo-send + size-warning (Findings #1 + #2) — both reduce the rate of "I just sent that?" mistakes. The single biggest cleanup is consolidating compose into one component (Finding #3) — it cuts the maintenance surface in half and ports address validation to mobile. The single biggest UI polish is restructuring the action bar (Findings #4 + #5) to put Reply forward and Delete + secondary actions in an overflow menu. |
| @@ -1,245 +0,0 @@ | |||
| 1 | - | # Phase 4 — Events & Calendar UX Audit | |
| 2 | - | ||
| 3 | - | **Scope:** every time-relative surface — the events list view, the month and week calendar grids, the day plan timeline (with painting and scheduling), weekly review, monthly review, the snooze flow as it touches events, reminders, time-block running state, and the date/time pickers used everywhere in this surface. | |
| 4 | - | ||
| 5 | - | **Stack:** Tauri 2 webview, vanilla HTML/CSS/JS. Files reviewed: `js/events.js`, `js/events-calendar.js`, `js/day-planning.js` (+ render / paint / schedule subfiles), `js/snooze.js`, `js/weekly-review.js` + render, `js/monthly-review.js` + render, `js/focus-timer.js`, `js/time-tracking.js`, the `#time-view` block in `index.html`, plus the matching CSS. | |
| 6 | - | ||
| 7 | - | **Method:** classical universal pass (Norman / Tognazzini / Raskin / HIG) plus the flat-design and cross-cutting checks. | |
| 8 | - | ||
| 9 | - | --- | |
| 10 | - | ||
| 11 | - | ## Surface snapshot | |
| 12 | - | ||
| 13 | - | - **Time tab pills:** Day · Week · Month · Timer · Events (5 pills; the todo for Phase 1 #1 already flags pill-discovery on mobile). | |
| 14 | - | - **Day Plan:** 24-hour timeline of 96 × 15-min slots (slot height read from `--timeline-slot-h`, 12 px desktop / 22 px mobile). Hour labels on hour boundaries only. Red 2-px "now" indicator updates every 60 s. Paint with mouse drag (desktop) or 2 s long-press (touch). | |
| 15 | - | - **Day Plan sidebar:** time summary, "Tasks to Schedule" list (virtual-scrolled), accomplished-inline section. Collapsible on mobile. | |
| 16 | - | - **Paint flow:** opens a modal that asks *what kind of thing* to create — Event, Time Block, or Link to Task. Block-type options: free_time, personal, vacation, focus. | |
| 17 | - | - **Events list:** three segmented sections — Recurring (collapsible), Upcoming (always open), Past (collapsible, reverse-chrono). Each section is virtually scrolled. | |
| 18 | - | - **Event form fields:** title, description, start_time (natural-date text), end_time (natural-date text), location, recurrence, block_type, contact_id, project_id. | |
| 19 | - | - **Snooze:** modal supports tasks and emails; **events are not snoozable**. | |
| 20 | - | - **Reminders:** **not implemented in the UI**. Per CLAUDE.md, "Event reminders" sits in the Notifications & Reminders sprint. | |
| 21 | - | - **Recurring events:** the list separates *templates* from *instances*; instances appear in Upcoming/Past, but only template editing is exposed. | |
| 22 | - | - **Calendar (month):** 7-col grid; event chips inline in cells with `.block-*` color; day-cell click opens inline day-detail under the grid. | |
| 23 | - | - **Calendar (week):** 7-col, hours 6 am–10 pm on desktop; mobile collapses to a swipeable single-day view. | |
| 24 | - | - **Time pickers:** mixture — `<input type="text">` with natural-date parsing in event form; `<input type="datetime-local">` in schedule-task / snooze / paint modals; `<input type="date">` in day-plan navigation. | |
| 25 | - | ||
| 26 | - | --- | |
| 27 | - | ||
| 28 | - | ## Critical (fix before declaring Phase 4 done) | |
| 29 | - | ||
| 30 | - | ### 1. Event reminders are completely absent | |
| 31 | - | - **Category:** Anticipation (Tog), Feedback, Forgiveness. (**Universal**) | |
| 32 | - | - **Location:** event form (`js/events.js#getEventFormFields`) has no reminder field; no scheduler hook; no notification permission request anywhere. | |
| 33 | - | - **Observation:** an event exists in the calendar but the application never tells the user "your meeting starts in 5 minutes". The only proactive surface is the events-tab status dot which polls every 30 s and reflects "upcoming events within a 15-min lead window" — but that's a peripheral signal, not a notification. On the day the user is busy and doesn't open the app, GoingsOn is silent. CLAUDE.md acknowledges this is queued; the audit confirms it's the single largest functionality gap in this surface. | |
| 34 | - | - **Why it matters:** for any app that calls itself a calendar, reminders are table stakes. Users will check Google / Apple calendars in parallel rather than rely on GoingsOn for time-based prompts. Once they do that, the events surface becomes shelf-ware. | |
| 35 | - | - **Recommendation:** | |
| 36 | - | - Add a `reminders` field to the event form: multi-select of "At time of event", "5 min before", "10 min", "15 min", "30 min", "1 hour", "1 day" (plus custom). | |
| 37 | - | - Wire to OS notifications via Tauri's `tauri-plugin-notification`. Request permission on first reminder set. | |
| 38 | - | - On launch and at every minute tick, evaluate upcoming events; emit notifications for ones whose lead time has elapsed. | |
| 39 | - | - Backend may already have the schema (recurring events store the rule); confirm and add `reminder_offsets_seconds` if missing. | |
| 40 | - | - This isn't a 1-day fix but it's a 1-week fix and it unblocks the calendar surface entirely. | |
| 41 | - | ||
| 42 | - | ### 2. Event form's start/end times are free-text with silent parse failure | |
| 43 | - | - **Category:** Error messages, Anticipation, Forgiveness. (**Universal**) | |
| 44 | - | - **Location:** `js/events.js` lines 107–115 — `start_time` and `end_time` are `type='text'` with `onInput="GoingsOn.utils.dateParsePreview"` and transform via `parseNaturalDate(v)`. | |
| 45 | - | - **Observation:** the user types "tomorow 3pm" (typo) or "Mar 32" (invalid) or "wednesday" (which Wednesday?). The preview helper shows the parsed result *if* parsing succeeds; on failure it does… something (need to verify — likely shows nothing or keeps the raw string). On Save, the form posts either a parsed ISO string or a raw fallback. The backend may accept the raw string as best-effort, store a wrong time, and the user only notices when the event shows up at the wrong moment. | |
| 46 | - | - **Why it matters:** date/time entry is the highest-leverage place for typos in a calendar app. Silent acceptance of unparseable input produces invisible-until-too-late errors. Standard pattern is preview-with-warning: as the user types, show the parsed value below the input ("→ Wed, Mar 12 at 3:00 PM"), with a red error if unparseable. | |
| 47 | - | - **Recommendation:** | |
| 48 | - | - On every keystroke, set the preview to one of three states: empty (no input), parsed-value (`→ Wed, Mar 12 at 3:00 PM`), or error (`Couldn't parse "tomorow 3pm" — try "tomorrow 3pm"`). | |
| 49 | - | - On submit, block save if the preview is in error state. | |
| 50 | - | - Add a small visible "shortcut" hint near the field: "tomorrow 3pm · friday 10am · 2026-12-25 09:00". | |
| 51 | - | - Consider adding a `<input type="datetime-local">` fallback alongside the natural-date text, toggleable — same shape the snooze modal already uses. | |
| 52 | - | ||
| 53 | - | ### 3. Events cannot be snoozed; same modal handles tasks and emails but not events | |
| 54 | - | - **Category:** Consistency. (**Universal**) | |
| 55 | - | - **Location:** `js/snooze.js` lines 72–78 — branches on `itemType === 'task'` or `'email'`; no event branch. | |
| 56 | - | - **Observation:** the snooze modal accepts `itemType` and calls the appropriate API. There is no event API for snooze and no entry point from the event detail / list / context menu. A user who wants "remind me about this meeting tomorrow" or "hide this from my upcoming list until Friday" has no path. | |
| 57 | - | - **Why it matters:** snoozing an event is a common pattern, especially for tentative meetings or events you can't confirm yet. The omission feels like a feature gap and reads inconsistently because tasks and emails *can* be snoozed. | |
| 58 | - | - **Recommendation:** | |
| 59 | - | - Either implement event-snooze (most natural — backend gets a `snoozed_until` column on events, snooze.js gets a third branch, list filters them out of Upcoming until the snooze expires), or | |
| 60 | - | - Decide explicitly that events are not snoozable and document why. (Reasoning could be: an event has a hard time anchor; "snoozing" it doesn't move the time — but the *visibility in the list* could still be snoozed.) | |
| 61 | - | - Recommend implement; this is a 1-day change. | |
| 62 | - | ||
| 63 | - | ### 4. All-day events are detected on render but cannot be authored | |
| 64 | - | - **Category:** Mappings, Anticipation. (**Universal**) | |
| 65 | - | - **Location:** `js/events-calendar.js` lines 210–215 — events with duration ≥ 23 hours are placed in a separate `.cal-week-allday-row`. Event form has no "All day" checkbox. | |
| 66 | - | - **Observation:** the calendar render *handles* all-day events: it pulls them out of the timed grid and renders them in a banner row above. But the only way to create one is to manually set `start_time` to a midnight value and `end_time` to the next midnight — which the natural-date parser may or may not do depending on what the user types. The asymmetry is sharp: the calendar can show all-day events that *can't be authored through the app*. | |
| 67 | - | - **Why it matters:** asymmetric features (renderable but not authorable) are confusing for users — they see "Conference Day" rendered as all-day on someone else's invite (imported from .ics or set via API) but can't replicate the affordance themselves. | |
| 68 | - | - **Recommendation:** | |
| 69 | - | - Add an "All day" checkbox at the top of the event form. When checked, hide the time fields and only show `start_date` and `end_date` (or just `date` for single-day all-day). | |
| 70 | - | - On render, the existing detection logic continues to work as a fallback for imported events. | |
| 71 | - | - This is the explicit todo item under "Sprint: Events → All-day event support". Promote to Phase 4 fix. | |
| 72 | - | ||
| 73 | - | --- | |
| 74 | - | ||
| 75 | - | ## Major (high impact, lower urgency) | |
| 76 | - | ||
| 77 | - | ### 5. Painting a block on mobile requires a 2-second long-press | |
| 78 | - | - **Category:** Fitts, Anticipation. (**Universal**) | |
| 79 | - | - **Location:** `js/day-planning.js` lines 84–88; long-press handler snaps to 30-min boundary. | |
| 80 | - | - **Observation:** to create anything on the timeline on mobile, the user must press and hold for 2 seconds on an empty slot. There's no shortcut (e.g., a floating "+" on the timeline, or a tap-to-create with default 30-min duration). 2 seconds is slow as a primary creation gesture — Apple Calendar uses ~0.5 s on iOS, Google Calendar a single tap. | |
| 81 | - | - **Why it matters:** if creating a block is the primary action on the day plan, it should be fast. A 2 s threshold also conflicts with users naturally scrolling: a brief pause during a scroll could trigger paint. | |
| 82 | - | - **Recommendation:** | |
| 83 | - | - Drop the long-press threshold to 500 ms. | |
| 84 | - | - Add a floating "+" button (FAB-style) bottom-right of the timeline; tap opens the same paint modal with `start = ceil(now, 15min)`, `end = start + 30min` defaults. | |
| 85 | - | - The drag-paint gesture remains for users who want a specific range. | |
| 86 | - | ||
| 87 | - | ### 6. Two overlapping "block" concepts in the domain | |
| 88 | - | - **Category:** Consistency, Mappings. (**Universal**) | |
| 89 | - | - **Location:** day plan creates "Time Blocks" (the paint modal's "Time Block" mode); the event form has a `block_type` field with the same set of options (free_time, personal, vacation, focus). | |
| 90 | - | - **Observation:** the codebase has two distinct concepts that share a name and a color vocabulary: | |
| 91 | - | - **Time Block** — created by painting on the day plan, lives as a separate entity, no title/description (just a colored span). | |
| 92 | - | - **Event with block_type** — a normal event that *also* has a block-color attached. | |
| 93 | - | Both render with `.block-focus`, `.block-personal`, etc. To a user, they look the same on the timeline. The data model and edit affordances differ. | |
| 94 | - | - **Why it matters:** when a user clicks a focus block, they can't tell whether they're editing an "Event" or a "Time Block" until the form opens. The naming is leaky and the dual model invites bugs (e.g., conflict detection treating them differently). | |
| 95 | - | - **Recommendation:** | |
| 96 | - | - Pick one. Either: collapse time blocks into events (every block *is* an event with a block_type, no separate Time Block entity), or keep them separate but rename one (e.g., "Time Block" stays for unstructured colored ranges; "Event" loses its `block_type` field and uses tags or labels instead). | |
| 97 | - | - The first option (events-only) is more consistent and reduces the model surface. Migration: convert existing Time Blocks to events with `block_type` set. | |
| 98 | - | - Surface in the UI: when a user clicks a colored span on the timeline, the edit modal title says "Edit time block" or "Edit event" based on the underlying type — currently this is implicit. | |
| 99 | - | ||
| 100 | - | ### 7. Recurring template vs occurrence editing is unsurfaced | |
| 101 | - | - **Category:** Modes, Feedback. (**Universal**) | |
| 102 | - | - **Location:** `js/events.js` Recurring section (lines 252+) shows templates only; instances appear in Upcoming/Past. Edit button on a template opens the form; edit button on an instance opens the form too — but the *scope of the edit* (this occurrence only? all future? all?) isn't asked. | |
| 103 | - | - **Observation:** Google Calendar, Apple Calendar, Outlook all prompt "Edit this event / This and future / All events" when editing a recurring instance. GoingsOn has no such prompt. Editing an instance presumably edits the *template* (which cascades to all occurrences). The user might think they're moving just tomorrow's instance and end up moving all 52 weekly instances. | |
| 104 | - | - **Why it matters:** silent cascading destroys schedules. A single misclick can break months of recurring data. | |
| 105 | - | - **Recommendation:** | |
| 106 | - | - When the user opens edit on an *instance* (not a template), prompt: "Edit this occurrence only · This and future · All occurrences" before showing the form. | |
| 107 | - | - When the user deletes a recurring instance, prompt the same. | |
| 108 | - | - Templates in the Recurring section behave as today (full series). | |
| 109 | - | - Backend must support per-occurrence overrides; verify. | |
| 110 | - | ||
| 111 | - | ### 8. No timezone awareness anywhere in the UI | |
| 112 | - | - **Category:** Anticipation, Forgiveness. (**Universal**) | |
| 113 | - | - **Location:** all date inputs use local browser time; storage strips Z; render uses `new Date()`. | |
| 114 | - | - **Observation:** events are stored and rendered in implicit local time. A user who creates a "3pm Tuesday" meeting in San Francisco and then travels to New York opens the app and sees "3pm Tuesday" — but their phone clock has shifted, and their counterpart in SF expected 3pm SF time, which is 6pm NY time. The app gives no warning, doesn't shift the display, doesn't surface the original timezone. | |
| 115 | - | - **Why it matters:** this is fine for purely-local users who never travel. For anyone with an online meeting across timezones, GoingsOn will silently mis-place every event. Even calendar imports from .ics (which carry timezone info) lose it on parse. | |
| 116 | - | - **Recommendation:** | |
| 117 | - | - Phase 1 fix: store each event's timezone (default: user's current zone at creation). Render in user's current zone, but show the *original zone* as a meta-line ("3pm Tuesday · originally PT"). | |
| 118 | - | - Phase 2 fix: allow editing the timezone on the event form. Surface a settings option "default timezone" and "always show in this zone". | |
| 119 | - | - This is a lot of work; the medium-term fix is to surface the user's current timezone in the shell (small text near the date in the day plan or weekly review) so users at least *see* what zone they're in. | |
| 120 | - | ||
| 121 | - | ### 9. The "now" indicator updates every 60 s on a 12 px / 15 min grid | |
| 122 | - | - **Category:** Feedback, Visibility of state. (**Universal/Polish**) | |
| 123 | - | - **Location:** `js/day-planning.js` line 48 — `setInterval(updateNowLine, 60000)`. | |
| 124 | - | - **Observation:** the slot is 12 px tall and represents 15 min — so 1 min ≈ 0.8 px. The indicator updates once a minute. Between updates, it's stale by up to a full minute, which on the timeline means up to 0.8 px of drift — visually imperceptible. *But* the staleness compounds with the "events tab dot" 30 s poll and creates a system where state lags inconsistently across surfaces. | |
| 125 | - | - **Why it matters:** less about the visual drift (which is fine), more about battery and consistency. Two `setInterval`s running while the app is open: 60 s for the now-line, 30 s for the upcoming-events dot. | |
| 126 | - | - **Recommendation:** unify into a single 60 s tick driven by `requestAnimationFrame` + visibility-API gating (pause when window is hidden). Drop the polled event-dot; reuse the same tick. Battery and CPU savings are real on a Tauri webview that's open in the background. | |
| 127 | - | ||
| 128 | - | ### 10. Conflict detection is server-side and only shows after save | |
| 129 | - | - **Category:** Anticipation, Forgiveness. (**Universal**) | |
| 130 | - | - **Location:** day-planning timeline applies `.conflict` class on render after the backend computes overlaps. | |
| 131 | - | - **Observation:** while the user is *painting* a new block, the timeline doesn't show whether the painted range overlaps an existing event. The schedule-task modal has a conflict warning slot (`#schedule-conflict-warning`) but it's populated server-side, after the user picks a time. There's no real-time "this conflicts with X" while painting. | |
| 132 | - | - **Why it matters:** the user paints a block, opens the modal, fills the form, hits Save, then sees "Conflicts with Standup". Now they have to back out and re-paint. The conflict should be visible *during* the paint. | |
| 133 | - | - **Recommendation:** | |
| 134 | - | - During paint, if the painted range overlaps an existing block, color the preview red instead of blue and show a small inline label "Conflicts with: Standup". | |
| 135 | - | - In the modal that opens after paint, pre-populate the conflict warning from client-side data (the events for this day are already loaded into the day plan). | |
| 136 | - | - Server-side check on save remains as the authoritative gate. | |
| 137 | - | ||
| 138 | - | ### 11. Vacation day banner has no UI to set the vacation state | |
| 139 | - | - **Category:** Mappings, Consistency. (**Universal**) | |
| 140 | - | - **Location:** `js/day-planning-render.js` lines 42–48 — banner shown if `dayPlanData.isVacationDay`. | |
| 141 | - | - **Observation:** the timeline shows "Day Off" if the day is marked vacation, but there's no UI to mark a day as vacation. The user can paint a block with `block_type=vacation` (which is *something*), or there might be a setting elsewhere. The relationship between vacation blocks and vacation days isn't surfaced. | |
| 142 | - | - **Recommendation:** | |
| 143 | - | - Add a small toggle near the day-plan date picker: "Mark as day off". When toggled, sets `isVacationDay` and dims/grays the timeline. | |
| 144 | - | - When the user creates a vacation-type block that spans the full day, prompt: "Mark the day as a day off?" | |
| 145 | - | - Either way, the affordance for the banner needs to exist somewhere visible. | |
| 146 | - | ||
| 147 | - | ### 12. Event form mixes `contact_id` and `project_id` with no surfaced purpose | |
| 148 | - | - **Category:** Visibility, Anticipation. (**Universal/Polish**) | |
| 149 | - | - **Location:** `js/events.js` lines 174–195 — event form has both `contact_id` (select) and `project_id` (hidden if pre-selected). | |
| 150 | - | - **Observation:** the user can attach an event to a contact and a project. The form lets them set it. But nowhere in the event row, calendar, or detail view does the contact or project surface — at least not obviously. (Contact may render in detail; project may filter the list but isn't a row column.) The user does work that doesn't pay off in visibility. | |
| 151 | - | - **Recommendation:** | |
| 152 | - | - Either render contact/project chips on the event row (consistent with tasks), or | |
| 153 | - | - Add a "Show project / contact in event row" toggle in settings, or | |
| 154 | - | - Remove these fields from the form if they don't drive visible behavior. | |
| 155 | - | ||
| 156 | - | ### 13. Week view's hour range (6 am–10 pm) is hard-coded | |
| 157 | - | - **Category:** Anticipation. (**Universal**) | |
| 158 | - | - **Location:** `js/events-calendar.js` lines 183–185 — `HOURS_START=6`, `HOURS_END=22`. | |
| 159 | - | - **Observation:** night-shift workers, parents up at 5 am, people with 11 pm online classes — all of them have events outside 6 am–10 pm and the week view simply doesn't show them. The events still exist on the day plan timeline (24h), but the week view truncates. | |
| 160 | - | - **Recommendation:** make the range a user setting. Default 6 am–10 pm is fine for most; allow overrides in Settings → Time. Smarter: auto-expand the range if any event in the visible week falls outside it. | |
| 161 | - | ||
| 162 | - | --- | |
| 163 | - | ||
| 164 | - | ## Minor (worth fixing during normal cleanup) | |
| 165 | - | ||
| 166 | - | ### 14. Running focus block has no on-timeline indicator | |
| 167 | - | - **Category:** Visibility. (**Polish**) | |
| 168 | - | - **Location:** `js/focus-timer.js` shows a full-screen overlay during focus; the timeline doesn't pulse / glow / animate the block while it's running. | |
| 169 | - | - **Observation:** when the user starts a focus session on a 10–11 am block, the block stays static-red on the timeline. The overlay says "Focus Mode 14:23 remaining" but the timeline doesn't reflect that *this* block is the live one. If the user dismisses the overlay (which they shouldn't but might), there's no in-timeline cue. | |
| 170 | - | - **Recommendation:** add a `.timeline-item.running` class with a subtle pulse animation (1 s `box-shadow` cycle in `--accent-red`) for the currently-active focus block. Pair with a progress bar inside the block showing elapsed/total. | |
| 171 | - | ||
| 172 | - | ### 15. Hour labels only on hour starts; users scanning the middle of the timeline have no time reference | |
| 173 | - | - **Category:** Visibility, Anticipation. (**Polish**) | |
| 174 | - | - **Location:** `js/day-planning-render.js` line 78 — `timeStr` rendered only if `isHourStart`. | |
| 175 | - | - **Observation:** a user scrolled to the middle of the day sees blocks but the nearest hour label may be 30+ px above. Adding labels at every 15-min slot is too much; at every 30 min (`isHalfHour`) would be a useful compromise. | |
| 176 | - | - **Recommendation:** show a light label on quarter 2 (`:30`) — e.g., faded `--text-muted` "9:30". Hour labels stay prominent. | |
| 177 | - | ||
| 178 | - | ### 16. Block colors are the only semantic for block type | |
| 179 | - | - **Category:** Accessibility, Visibility (color-as-sole-signal). (**Flat — recurring pattern**) | |
| 180 | - | - **Location:** `.block-focus` / `.block-personal` / `.block-vacation` / `.block-free_time` all rely on `--accent-*` for distinction. | |
| 181 | - | - **Observation:** same pattern as Phase 1 #2 (sync dot), Phase 2 #6 (priority), Phase 3 #6 (status). On themes where two accent colors are close in luminance, focus and personal can look identical. | |
| 182 | - | - **Recommendation:** add a small glyph or letter inside each block ("F", "P", "V", "Free") — at least at desktop widths. On narrow timelines where the block height is < 24 px, the glyph degrades to a left-edge stripe pattern. | |
| 183 | - | ||
| 184 | - | ### 17. Schedule-task modal ignores the task's estimated duration | |
| 185 | - | - **Category:** Anticipation. (**Polish**) | |
| 186 | - | - **Location:** `js/day-planning-schedule.js` — duration presets 15m, 30m (default), 45m, 1h, 1.5h, 2h. | |
| 187 | - | - **Observation:** if the task has `estimated_minutes` set, the modal should default to that value instead of 30. A user who carefully estimated "60 minutes" for a task and then schedules it shouldn't have to re-pick 1h every time. | |
| 188 | - | - **Recommendation:** read `task.estimated_minutes` and pre-select the closest preset (or surface a custom field with the value pre-filled). | |
| 189 | - | ||
| 190 | - | ### 18. Past events are kept in the list forever; only collapsible, never archived | |
| 191 | - | - **Category:** Hierarchy. (**Polish**) | |
| 192 | - | - **Location:** `js/events.js` Past section. | |
| 193 | - | - **Observation:** an event from 2 years ago lives in the Past section, collapsed by default but always loaded into the list. On a heavy-use account, this is hundreds of dead rows in memory. | |
| 194 | - | - **Recommendation:** auto-collapse "Past" by default *and* time-window the query to the last 90 days at first load; show "Load older" to fetch more. The virtual scroller helps with render cost but not memory. | |
| 195 | - | ||
| 196 | - | ### 19. The Time tab's Timer pill duplicates Track Time / Focus Mode entry points | |
| 197 | - | - **Category:** Consistency (with Phase 2 #5). | |
| 198 | - | - **Location:** Time tab pill "Timer" (index.html:239). | |
| 199 | - | - **Observation:** Phase 2 already noted that Track Time, Focus Mode, and Schedule Time Block are three flows for one backend. The Timer pill is now a *fourth* surface for time-related UI. A user clicking Timer expects… what? Track-time controls? Focus controls? Both? | |
| 200 | - | - **Recommendation:** rename the pill and the underlying view to match what it actually shows. If it's a Pomodoro / focus session launcher, call it "Focus"; if it's a track-time dashboard with history, call it "Time log". The pill label should reveal the contents. | |
| 201 | - | ||
| 202 | - | ### 20. Mobile week view collapses to swipeable single-day, but the swipe is undiscoverable | |
| 203 | - | - **Category:** Discoverability. (**Polish**) | |
| 204 | - | - **Location:** `js/events-calendar.js` lines 335–404 — mobile day swipe. | |
| 205 | - | - **Observation:** on mobile, "Week" pill renders a single day with swipe-left / swipe-right to change days. There's no on-screen indication ("← swipe →") on first view. A first-time mobile user may not realize they can swipe. | |
| 206 | - | - **Recommendation:** show a subtle two-arrow hint on the date header for the first three opens (localStorage flag), then suppress. | |
| 207 | - | ||
| 208 | - | --- | |
| 209 | - | ||
| 210 | - | ## Polish | |
| 211 | - | ||
| 212 | - | ### 21. Natural-date preview text behavior is unspecified | |
| 213 | - | - **Category:** Anticipation. | |
| 214 | - | - **Location:** event form `onInput="GoingsOn.utils.dateParsePreview"`. | |
| 215 | - | - **Observation:** the helper exists but its rendering is unverified. If it doesn't yet exist or just logs to console, this is the underlying cause of Finding #2. | |
| 216 | - | - **Recommendation:** fix as part of #2. | |
| 217 | - | ||
| 218 | - | ### 22. No keyboard shortcut for new event from the events list | |
| 219 | - | - **Category:** Discoverability. | |
| 220 | - | - **Location:** keyboard.js shortcuts are tab-aware ("New for current view"), so `n` should work — verify. | |
| 221 | - | - **Observation:** Phase 2 Tasks has `n` for new task. Events should be symmetric. Quick check: from the Events pill, does `n` open the event form? | |
| 222 | - | - **Recommendation:** if not, wire it. One line in `newItemForCurrentView`. | |
| 223 | - | ||
| 224 | - | ### 23. Vacation block (type) is not the same as Vacation day (banner) | |
| 225 | - | - **Category:** Consistency. (**Polish**) | |
| 226 | - | - **Observation:** the user can paint a `block_type=vacation` block (which is a colored span on one day) *or* the day can be `isVacationDay=true` (which is a full-day banner). Two ways to express "day off". (Related to Finding #11.) | |
| 227 | - | - **Recommendation:** rationalize. Either auto-promote a full-day vacation block to `isVacationDay`, or remove one of the two encodings. | |
| 228 | - | ||
| 229 | - | --- | |
| 230 | - | ||
| 231 | - | ## Cross-cutting / flat-design check | |
| 232 | - | ||
| 233 | - | The events surface concentrates the patterns we've seen in earlier phases: | |
| 234 | - | ||
| 235 | - | - **Color-as-sole-signal**: block types (#16) replay Phase 1 #2, Phase 2 #6, Phase 3 #6. | |
| 236 | - | - **Filter / view state not persisted**: the day-plan date selection, the week / month navigation, none mirror to the URL. Reload returns to today. Same root cause as Phase 1 #9, Phase 2 #10, Phase 3 #8. | |
| 237 | - | - **Action bar density / dual entry points**: Time tab has 5 pills, one of which (Timer) parallels Track Time and Focus Mode flows; Phase 2 #5 and Phase 3 #5 are the same shape. | |
| 238 | - | ||
| 239 | - | A new pattern emerges here: **passive-feature gaps** — reminders, snooze-events, all-day toggle, recurring-instance edit prompt — features that exist *in the data model or render path* but aren't authored through the UI. Three of the four critical findings are this shape. The events surface has the most domain richness (recurrence, all-day, time zones, conflicts, reminders) and the thinnest UI affordances per feature. | |
| 240 | - | ||
| 241 | - | --- | |
| 242 | - | ||
| 243 | - | ## Summary | |
| 244 | - | ||
| 245 | - | The events surface is the most domain-rich and the most under-served in GoingsOn. The data model supports recurrence, all-day events, conflicts, block types, contacts, and projects; the UI exposes maybe half of these as authoring affordances. The single largest functional gap is reminders (Finding #1) — without them, this is a calendar that doesn't tell you about your meetings. The single largest correctness gap is silent natural-date parsing (Finding #2) — every wrong time saved is a missed meeting. The single largest consistency gap is the two-headed "block" concept (Finding #6) — collapsing it into one model would simplify everything downstream. After Phase 4, the audit has seen the same three meta-patterns (state-by-color, filter-not-persisted, action-overload) across four surfaces; they should be promoted from per-phase findings to design-system charter rules. |
| @@ -1,246 +0,0 @@ | |||
| 1 | - | # Phase 5 — Projects, Contacts, Settings UX Audit | |
| 2 | - | ||
| 3 | - | **Scope:** the configuration + management surfaces — the projects grid and per-project dashboard with milestones; the contacts grid, per-contact dashboard, and sub-collections (email/phone/social/custom-field); the full settings page with its six sections (Appearance, Notifications, Planning & Review, Plugins, Cloud Sync, Data); the import / export flows; the OTA updater. These are the lowest-traffic but most-stateful surfaces in the app. | |
| 4 | - | ||
| 5 | - | **Stack:** Tauri 2 webview, vanilla HTML/CSS/JS. Files reviewed: `js/projects.js`, `js/projects-render.js`, `js/contacts.js`, `js/contacts-render.js`, `js/contact-dashboard.js`, `js/settings.js`, `js/settings-sync.js`, `js/themes.js`, `js/import.js`, `js/import-external.js`, `js/export.js`, `js/shared-updater.js`, `js/updater.js`, plus the matching CSS and the `#projects-view`, `#contacts-view`, `#contact-dashboard`, `#settings-view`, and `#project-dashboard-view` blocks in `index.html`. | |
| 6 | - | ||
| 7 | - | **Method:** classical universal pass (Norman / Tognazzini / Raskin / HIG) plus the cross-cutting checks. | |
| 8 | - | ||
| 9 | - | --- | |
| 10 | - | ||
| 11 | - | ## Surface snapshot | |
| 12 | - | ||
| 13 | - | - **Projects:** card grid, `n` to create, click-to-open-dashboard. Project has name / description / type (SideProject / Job / Company / Essay / Article / Other) / status (Active / On Hold / Completed / Archived). Dashboard is a 4-column grid (Tasks · Events · Emails · Attachments) plus a Milestones section above. Milestones support reorder via ▲/▼ buttons, edit, delete. | |
| 14 | - | - **Contacts:** card grid, search + tag filter. Click opens a *full-page* contact dashboard (separate view, like the task overview). Sub-collections: email / phone / social / customField — each has Add and Remove but **no Edit**. Bulk select + bulk tag (via `prompt()`). Tags shown as pills. | |
| 15 | - | - **Settings:** full-page view with sidebar + content layout. Six sections: **Appearance**, **Notifications**, **Planning & Review**, **Plugins**, **Cloud Sync**, **Data (Import & Export)**. Most settings auto-save to `localStorage` on change. Cloud Sync has stateful OAuth flow. | |
| 16 | - | - **Import:** two parallel entry points — "Import Contacts / Calendar" (external file: vCard or .ics) and "Import via Plugin" (plugin-driven, multi-step wizard). | |
| 17 | - | - **Export:** three buttons (JSON / Tasks CSV / Events ICS) and a separate backup system (manual + automatic). | |
| 18 | - | - **Updater:** thin local wrapper around a shared module from `MNW/shared/tauri-updater-ui` (per repo layout). | |
| 19 | - | ||
| 20 | - | --- | |
| 21 | - | ||
| 22 | - | ## Critical (fix before declaring Phase 5 done) | |
| 23 | - | ||
| 24 | - | ### 1. There is no "About" / version surface in the app at all | |
| 25 | - | - **Category:** Visibility of state, Error messages. (**Universal**) | |
| 26 | - | - **Location:** Settings has six sections (Appearance, Notifications, Planning & Review, Plugins, Cloud Sync, Data) — none of them show a version number. `js/app.js#showAbout` exists but is only invoked from… nowhere visible in the inventory (the welcome modal opens it, but only on first run). | |
| 27 | - | - **Observation:** a user who wants to know "what version of GoingsOn am I running?" cannot find out from the settings page. The only place the version appears is the OTA updater dialog when an update is offered — i.e., when the app is *about to no longer be that version*. CLAUDE.md and the build process emit versions (the current desktop is 0.3.1, iOS is 0.3.3), but the user has no in-app affordance to verify. | |
| 28 | - | - **Why it matters:** support requests start with "what version are you on?" Bug reports degrade quickly when the user can't answer. Onboarding to TestFlight or Discord communities works the same way. | |
| 29 | - | - **Recommendation:** | |
| 30 | - | - Add an **About** section (or a footer in the settings sidebar): version, build date, git commit short hash, license, link to changelog, link to support. | |
| 31 | - | - Make the existing `showAbout()` modal accessible from a header menu item or from the bottom of the settings sidebar. | |
| 32 | - | - Pair with a "Check for updates" button that triggers the OTA flow on demand — currently updates check on a schedule the user can't see. | |
| 33 | - | ||
| 34 | - | ### 2. Contact bulk-tag uses native `window.prompt()` | |
| 35 | - | - **Category:** Consistency, Forgiveness. (**Universal**) | |
| 36 | - | - **Location:** `js/contacts.js#bulkTag` — `const tag = prompt('Tag to add:');` | |
| 37 | - | - **Observation:** Step 9 of the consolidation already removed every `window.confirm()` call from the frontend. `window.prompt()` survived. The bulk-tag flow opens a native browser prompt: no theme, no validation, no autocomplete against existing tags, no preview ("you're about to add this tag to 12 contacts"). On Tauri 2 some platforms even disable `prompt()` entirely (it returns null on iOS). | |
| 38 | - | - **Why it matters:** the design-system charter (`docs/design-system.md`) says all confirms/inputs route through `GoingsOn.ui` — `prompt()` is exactly the kind of native call the charter forbids. On a platform where it's disabled, the feature silently breaks. | |
| 39 | - | - **Recommendation:** | |
| 40 | - | - Add `GoingsOn.ui.showPromptDialog(title, message, opts)` that renders into the global modal with a `<input class="form-input">` plus OK/Cancel. Mirror the existing `showConfirmDialog` API. | |
| 41 | - | - Replace the `prompt()` call. While there, add autocomplete from existing tags (the backend already returns tag lists) and a confirmation preview: "Add tag 'follow-up' to 12 contacts?" | |
| 42 | - | - Add a lint rule for `prompt(` in `scripts/lint-frontend.sh`. | |
| 43 | - | ||
| 44 | - | ### 3. Sub-collections on contacts can be Added and Removed but not Edited | |
| 45 | - | - **Category:** Forgiveness, Anticipation. (**Universal**) | |
| 46 | - | - **Location:** `js/contacts.js` `openAddSubCollection` exists for each of email/phone/social/customField; no `openEditSubCollection` exists. | |
| 47 | - | - **Observation:** a user with a contact whose email is `bob@oldcompany.com` and wants to change it to `bob@newcompany.com` must: | |
| 48 | - | 1. Remove the old email (× button → confirm) | |
| 49 | - | 2. Re-Add the new email (modal → form → submit) | |
| 50 | - | 3. Re-mark Primary if it was primary | |
| 51 | - | This loses any metadata (label, primary flag) and risks accidentally removing the wrong row. | |
| 52 | - | - **Why it matters:** address book contacts are append-only in practice but constantly need correction. The two-step remove-then-add flow forces users to re-enter context they already have. | |
| 53 | - | - **Recommendation:** | |
| 54 | - | - Add a tap target on each sub-item that opens the same form modal pre-filled. The Add form already exists; reuse it for Edit by passing the existing record. | |
| 55 | - | - Visual: clicking the row itself opens Edit; the × stays for delete. | |
| 56 | - | - Backend: most sub-collections are updateable; verify and wire. | |
| 57 | - | ||
| 58 | - | ### 4. Bulk operations on contacts have no undo (recurring pattern) | |
| 59 | - | - **Category:** Forgiveness, Consistency. (**Universal — recurring pattern**) | |
| 60 | - | - **Location:** `js/contacts.js#bulkTag` calls the API and just shows a success toast; no `showUndoToast`. | |
| 61 | - | - **Observation:** same shape as Phase 2 #2 (bulk task delete/snooze/set-project lack undo while single-task complete has it) — the bulk contact operations have no undo wrapper, while the single-project delete *does* use `showUndoToast` (`projects.js:200`). This is the same asymmetry repeating: undo exists in the codebase, gets wired only sometimes. | |
| 62 | - | - **Why it matters:** a user accidentally tags 50 contacts with the wrong label and has no recovery. The cumulative effect across phases: every bulk operation in the app needs to be checked for undo coverage. | |
| 63 | - | - **Recommendation:** | |
| 64 | - | - Phase 2 already recommended a `bulkActionWithUndo(action, inverse, ids, prevState)` helper. Build it once; use it here, in bulk tasks, and in any future bulk contact actions (bulk delete, bulk merge). | |
| 65 | - | - For bulk tag specifically: capture pre-state (which contacts already had the tag — those are skipped on undo; the newly-tagged ones are reverted). 10-line helper. | |
| 66 | - | ||
| 67 | - | --- | |
| 68 | - | ||
| 69 | - | ## Major (high impact, lower urgency) | |
| 70 | - | ||
| 71 | - | ### 5. Settings is a full-page view, not a modal — pulls users out of context | |
| 72 | - | - **Category:** Modes, Modelessness (Raskin). (**Universal**) | |
| 73 | - | - **Location:** `js/settings.js#openSettings` calls `GoingsOn.navigation.switchView('settings')`, which tears down the current view. | |
| 74 | - | - **Observation:** the rest of GoingsOn uses modals for configuration tasks (task edit, event edit, contact edit, snooze picker). Settings is the exception — it's a full-page replacement. A user in the middle of triaging tasks who realizes they want to change the theme has to leave Tasks entirely, change the theme, then re-navigate back, losing scroll position and filter state (compounding with the Phase 2 filter-not-persisted issue). | |
| 75 | - | - **Why it matters:** the disruption isn't huge for users who only visit settings occasionally, but it means the "small adjustment" use case (toggle a plugin, switch theme to test, change work hours) becomes a multi-step trip. It also visually loses the parent context, which means changes to theme don't preview against the user's actual content. | |
| 76 | - | - **Recommendation:** | |
| 77 | - | - Convert Settings to a side-drawer (right-edge sliding panel) or a modal with the sidebar/content layout intact. | |
| 78 | - | - The sidebar layout still works inside a modal — the sections fit comfortably in a 720 × 600 dialog. | |
| 79 | - | - This is also the natural fix for Finding #14 (theme preview): if the settings modal overlays content rather than replacing it, the user sees theme changes apply behind it. | |
| 80 | - | ||
| 81 | - | ### 6. Most settings auto-save on change with no "undo last change" affordance | |
| 82 | - | - **Category:** Forgiveness, Consistency. (**Universal**) | |
| 83 | - | - **Location:** Appearance theme onChange writes `localStorage` and re-applies CSS variables immediately; Planning & Review work-hours `<select>` writes localStorage on every change. | |
| 84 | - | - **Observation:** auto-save is the right default for settings (no Save button to forget), but it pairs poorly with destructive changes. A user clicks the theme dropdown, scrolls through, "previews" a theme by mouse-over — except every selection commits. Switching to a theme they hate and then back to "the previous one" requires remembering the previous theme's name. | |
| 85 | - | - **Why it matters:** auto-save with no undo is the worst form of "fast". Fast actions need fast recovery. | |
| 86 | - | - **Recommendation:** | |
| 87 | - | - On every auto-saved setting change, surface a small "Undo" toast with the previous value: "Theme: Catppuccin Frappe · Undo". | |
| 88 | - | - Track per-session "last setting changed"; the toast lasts 6 s and clicking Undo reverts to the prior value. | |
| 89 | - | - For multi-field forms (work hours, backup settings), batch changes into a single undo toast: "Settings updated · Undo". | |
| 90 | - | ||
| 91 | - | ### 7. Milestone reordering is up/down buttons with no drag | |
| 92 | - | - **Category:** Fitts, Anticipation. (**Universal/Polish**) | |
| 93 | - | - **Location:** `js/projects-render.js` lines 163–182 — `moveMilestone(id, ±1)` buttons. | |
| 94 | - | - **Observation:** with three milestones, ▲/▼ works fine. With ten, moving #10 to position #1 requires nine clicks. There's no drag-handle, no "move to top/bottom" affordance. | |
| 95 | - | - **Recommendation:** | |
| 96 | - | - Add drag-drop (`SortableJS` or a 30-line custom). The existing `.milestone-card` becomes the drag target. | |
| 97 | - | - Add `Move to top` / `Move to bottom` to the context menu. | |
| 98 | - | - The reorder buttons can stay as a fallback. | |
| 99 | - | ||
| 100 | - | ### 8. Project edit form lets you change status to Completed/Archived but the create form doesn't | |
| 101 | - | - **Category:** Consistency, Mappings. (**Polish/Universal**) | |
| 102 | - | - **Location:** `js/projects.js` lines 65–73 — `PROJECT_STATUSES.slice(0, 2)` for create, full list for edit. | |
| 103 | - | - **Observation:** a user creating a project can only choose Active or On Hold. To create a project that's already Completed (e.g., backfilling) requires creating it as Active and immediately editing it. | |
| 104 | - | - **Why it matters:** the asymmetry is mild — most projects start Active — but it surprises users who are migrating from another system and want to record completed work for retrospective. | |
| 105 | - | - **Recommendation:** show all four statuses in the create form. The "edit changes more than create" pattern is unusual and worth removing unless there's a backend constraint. | |
| 106 | - | ||
| 107 | - | ### 9. No "Account / Profile" section; Cloud Sync is the only place a connected account is mentioned | |
| 108 | - | - **Category:** Visibility of state, Trust. (**Universal**) | |
| 109 | - | - **Location:** settings sections list — no Account section; `settings-sync.js` only shows "Connected to {serverUrl}" without surfacing which Makenot.work account is signed in. | |
| 110 | - | - **Observation:** after a user connects their Makenot.work account via OAuth, the sync section shows server URL and sync stats but doesn't tell them *which account email* they're using. To check, they'd have to disconnect (which logs them out) and re-connect (which re-prompts the OAuth flow with email shown). There's also no in-app affordance to: | |
| 111 | - | - See their account email | |
| 112 | - | - Change the password they use to authenticate | |
| 113 | - | - View their subscription details beyond "active / not active" | |
| 114 | - | - Get a backup of their encryption recovery key | |
| 115 | - | - **Why it matters:** for a sync product, the connected identity is primary trust state. A user who shared their device with someone, or who has multiple accounts, can't tell from the UI which one is active. | |
| 116 | - | - **Recommendation:** | |
| 117 | - | - Add an **Account** section: connected email, last login, "Open account on makenot.work" link, "Sign out" button. | |
| 118 | - | - Surface the subscription tier (Basic / Small Files / Big Files / Everything) with current price and renewal date. | |
| 119 | - | - Recovery key: this is critical for E2EE — surface a "Generate recovery key" affordance (the backend supports it per the todo). | |
| 120 | - | ||
| 121 | - | ### 10. Export has no progress indicator and no item-count preview | |
| 122 | - | - **Category:** Feedback, Anticipation. (**Universal**) | |
| 123 | - | - **Location:** `js/export.js#exportJSON` (and tasks CSV, events ICS) — `await GoingsOn.api.export.json(filePath)` is awaited synchronously; success toast appears after. | |
| 124 | - | - **Observation:** for a small database (100 items) the export finishes in <1 s and the toast confirms. For a large mailbox / task list (10 K+ items) the export may take 30 s. During that time the user sees nothing — the file picker has closed, the toast hasn't arrived. They may click again, try to use the app, or quit thinking it's stuck. | |
| 125 | - | - **Why it matters:** anything over 500 ms needs a progress indicator. Anything over 5 s needs a cancellation affordance. | |
| 126 | - | - **Recommendation:** | |
| 127 | - | - Replace the dialog → silent → toast pattern with: dialog → modal "Exporting…" with item count and a progress bar → toast on complete. | |
| 128 | - | - If backend can't stream progress, show indeterminate spinner. | |
| 129 | - | - Add Cancel button that aborts mid-export. | |
| 130 | - | ||
| 131 | - | ### 11. Default backup retention is "Keep 1 backup (Recommended)" | |
| 132 | - | - **Category:** Forgiveness, Anticipation. (**Universal**) | |
| 133 | - | - **Location:** `js/settings.js#renderData` — `retentionOptions = [{ value: 1, label: 'Keep 1 backup (Recommended)' }, ...]`. | |
| 134 | - | - **Observation:** the recommended retention policy is one backup. Each new automatic backup deletes the previous one. If the latest backup happened during a corruption window (e.g., a bad write, a SyncKit conflict that wasn't resolved well), the user has no fallback. | |
| 135 | - | - **Why it matters:** backups exist *because* the latest state might be bad. "Keep 1" defeats the purpose. A reasonable default is 3 or 7. | |
| 136 | - | - **Recommendation:** | |
| 137 | - | - Change the default to "Keep 7 backups (Recommended)". | |
| 138 | - | - Add a short hint: "Older backups protect against corruption that happened recently." | |
| 139 | - | - Optionally: tier the retention — daily for last 7 days + weekly for last 4 weeks. Standard rolling-backup pattern. | |
| 140 | - | ||
| 141 | - | ### 12. Theme picker has no live preview; every change commits and re-renders | |
| 142 | - | - **Category:** Anticipation, Forgiveness. (**Universal**) | |
| 143 | - | - **Location:** `js/themes.js` + `js/settings.js#renderAppearance` — `<select>` with `onchange="GoingsOn.themes.onChange(this.value)"` applies the theme immediately. | |
| 144 | - | - **Observation:** the theme list is a dropdown. Users select a theme; it applies. To preview a different one, they select it; it applies. There's no preview-on-hover, no thumbnail grid, no side-by-side. With 9+ themes, users either commit-then-revert repeatedly (each commit writes to localStorage and re-applies all CSS variables) or settle for the first acceptable one. | |
| 145 | - | - **Why it matters:** themes are a primary differentiator for an app that ships 9 of them. Letting users browse them efficiently matters. | |
| 146 | - | - **Recommendation:** | |
| 147 | - | - Replace the dropdown with a grid of thumbnail cards — each card shows a small mockup of the app chrome (header, tab, button) in that theme's colors. Hover applies for preview; click commits. | |
| 148 | - | - Preview on hover is the lowest-effort improvement; the thumbnail grid is the polish version. | |
| 149 | - | ||
| 150 | - | ### 13. Plan / Review "nudge" settings exist but the nudges aren't documented | |
| 151 | - | - **Category:** Anticipation, Mappings. (**Universal/Polish**) | |
| 152 | - | - **Location:** `js/settings.js#renderPlanning` — Plan nudges and Review nudges toggles, default enabled. | |
| 153 | - | - **Observation:** the settings have toggles for "Plan nudges" and "Review nudges". What's a nudge? When does it appear? Where? The form-hint doesn't explain. A user toggling them off may not know what they're turning off. | |
| 154 | - | - **Recommendation:** | |
| 155 | - | - Add a one-line hint under each: "Plan nudges: show a dot on the Day pill when you haven't planned today yet." / "Review nudges: show a dot on the Week pill when this week's review isn't complete." | |
| 156 | - | - Better: a tiny "See where nudges appear →" link that highlights the dot. | |
| 157 | - | ||
| 158 | - | ### 14. Import has two parallel entry points; the relationship isn't surfaced | |
| 159 | - | - **Category:** Consistency, Mappings. (**Universal/Polish**) | |
| 160 | - | - **Location:** Settings → Import & Export has both "Import Contacts / Calendar" (external file) and "Import via Plugin" (plugin-based). | |
| 161 | - | - **Observation:** a user with a vCard file might naturally try "Import via Plugin" first (because vCard plugin support is on the roadmap — `todo.md` Sprint: Plugin System). Today they'd be told "no plugin matches your file" or similar. The two entry points have overlapping conceptual scope. | |
| 162 | - | - **Recommendation:** | |
| 163 | - | - Unify into a single "Import" button that opens a modal: "What are you importing? Contacts (vCard) · Calendar (.ics) · Other (use a plugin)". The third path leads to the plugin wizard. | |
| 164 | - | - This puts the user's data type at the front of the decision rather than the implementation. | |
| 165 | - | ||
| 166 | - | --- | |
| 167 | - | ||
| 168 | - | ## Minor (worth fixing during normal cleanup) | |
| 169 | - | ||
| 170 | - | ### 15. Contact avatars are uncolored initials; all contacts look the same | |
| 171 | - | - **Category:** Visibility, Polish. (**Polish**) | |
| 172 | - | - **Location:** `js/contacts-render.js` — `.contact-avatar` styled with default neutral colors; no hash-to-color. | |
| 173 | - | - **Observation:** typical address books color the initials avatar by hashing the name. GoingsOn renders every avatar the same color, so a list of 30 contacts has 30 identical-looking circles. Scanning the list by avatar is impossible. | |
| 174 | - | - **Recommendation:** hash `contact.id` (or `displayName`) to one of 8 accent-tinted background colors. The accent palette already has 6 (`--accent-yellow / green / blue / purple / red / cyan`); cycle through them. | |
| 175 | - | ||
| 176 | - | ### 16. Tag filter on contacts is a single-select dropdown | |
| 177 | - | - **Category:** Anticipation. (**Polish**) | |
| 178 | - | - **Location:** Contacts filter bar. | |
| 179 | - | - **Observation:** the filter is one tag at a time. A user with multiple tags can't filter to "friend AND coworker" or "friend OR vendor". The bulk-tag operation also adds only one tag at a time. | |
| 180 | - | - **Recommendation:** convert the filter to a multi-select chip control. Bulk-tag could accept multiple tags in the prompt (once #2 lands and the input is a real form field). | |
| 181 | - | ||
| 182 | - | ### 17. Contacts list has no sort control | |
| 183 | - | - **Category:** Anticipation. (**Polish**) | |
| 184 | - | - **Location:** Contacts page header. | |
| 185 | - | - **Observation:** the list is sorted alphabetically by display name (backend default). No UI to sort by recently added, last contacted, company, or last interaction. | |
| 186 | - | - **Recommendation:** add a sort dropdown next to the search/filter. Same pattern as the mobile sort dropdown in tasks. | |
| 187 | - | ||
| 188 | - | ### 18. Projects list has no sort, no filter, no search | |
| 189 | - | - **Category:** Anticipation. (**Polish**) | |
| 190 | - | - **Location:** Projects page header. | |
| 191 | - | - **Observation:** with 20+ projects, no way to filter by status, type, or recency. Cards stack chronologically. | |
| 192 | - | - **Recommendation:** add a status filter and a sort control. For now, hide completed/archived behind a toggle (default: show only Active and On Hold). | |
| 193 | - | ||
| 194 | - | ### 19. Plugin item description has weird format | |
| 195 | - | - **Category:** Polish. (**Polish**) | |
| 196 | - | - **Location:** `js/settings.js` plugin render — `<span class="plugin-extensions">Files: .${extensions.join(', .')} | Types: ${entityTypes.join(', ')}</span>` | |
| 197 | - | - **Observation:** the format reads as "Files: .vcf, .vcard | Types: contact". The `.` prefix on the first item gets `.${join('.')}` and produces `.vcf, .vcard` — which reads almost right, but the `|` pipe between Files and Types is visually clunky. | |
| 198 | - | - **Recommendation:** use two lines with subtle separation, or a `·` dot separator. Drop the `|`. | |
| 199 | - | ||
| 200 | - | ### 20. Settings sidebar active state is a faint color change | |
| 201 | - | - **Category:** Visibility. (**Polish — recurring pattern**) | |
| 202 | - | - **Location:** `.settings-nav-item.active` styling. | |
| 203 | - | - **Observation:** same pattern as Phase 1 #4 — the active settings section is differentiated by a small color change. Easy to miss when scanning. State-by-color-alone, again. | |
| 204 | - | - **Recommendation:** add a left-edge accent stripe (`border-left: 3px solid var(--accent-blue)`) to the active item, plus the existing color shift. Matches the pattern recommended in Phase 1. | |
| 205 | - | ||
| 206 | - | ### 21. Settings auto-save toast is missing | |
| 207 | - | - **Category:** Feedback. (**Polish**) | |
| 208 | - | - **Location:** plugin toggle, theme change, work-hours change — all write silently to localStorage / API. | |
| 209 | - | - **Observation:** the user toggles "Enable automatic backups" → the checkbox state changes but nothing confirms the save. (Inferred — verify.) A small flash, a subtle "Saved" toast, anything to confirm the change persisted. | |
| 210 | - | - **Recommendation:** for every auto-save, show a small inline "Saved ✓" near the changed field that fades after 1.5 s. Don't use the toast system for every settings change — too noisy. | |
| 211 | - | ||
| 212 | - | --- | |
| 213 | - | ||
| 214 | - | ## Polish | |
| 215 | - | ||
| 216 | - | ### 22. Project dashboard's 4-column grid breaks at narrow widths | |
| 217 | - | - **Category:** Polish. | |
| 218 | - | - **Location:** `.project-dashboard-grid` CSS. | |
| 219 | - | - **Observation:** Tasks · Events · Emails · Attachments in a 4-column grid. On a 1024-wide window (small desktop / tablet), each column is ~220 px, which truncates row content quickly. | |
| 220 | - | - **Recommendation:** collapse to 2×2 below 1100 px and to 1-column below 768 px. | |
| 221 | - | ||
| 222 | - | ### 23. `showAbout` modal exists in `app.js` but only the welcome flow invokes it | |
| 223 | - | - **Category:** Polish. | |
| 224 | - | - **Location:** `js/app.js#showAbout`. | |
| 225 | - | - **Observation:** see Finding #1 — the modal exists but has no entry point in the running app. Either wire it up (from settings sidebar bottom, or a header overflow menu) or delete it. | |
| 226 | - | ||
| 227 | - | --- | |
| 228 | - | ||
| 229 | - | ## Cross-cutting / flat-design check | |
| 230 | - | ||
| 231 | - | The patterns from earlier phases continue here: | |
| 232 | - | ||
| 233 | - | - **Color-as-sole-signal** — settings sidebar `.active` state (Finding #20) repeats Phase 1 #4 (mobile tab active), Phase 2 #6 (priority), Phase 4 #16 (block type). | |
| 234 | - | - **Filter-state-not-persisted** — projects and contacts list states (tag filter, search query) live in the DOM; the URL doesn't carry them. Same root cause as Phase 1 #9, Phase 2 #10, Phase 3 #8, Phase 4 (week-nav). | |
| 235 | - | - **No-undo on bulk operations** — contacts bulk-tag (Finding #4) repeats Phase 2 #2 (tasks). | |
| 236 | - | - **Native browser dialogs forbidden by charter** — `window.prompt()` survives in contacts bulk-tag (Finding #2). `window.confirm()` was killed in Step 9; `prompt()` is the same problem with the same fix. | |
| 237 | - | ||
| 238 | - | **One new pattern** specific to Phase 5: **stateful flows hide their state**. The OAuth/encryption flow surfaces a different UI at each step but doesn't show a step indicator ("Step 2 of 3: Encryption"). The plugin import wizard has steps 1–4 but the UI doesn't show a progress dot. The user travels through a multi-step flow with no map. Worth raising at the design-system layer as "multi-step flows must show progress." | |
| 239 | - | ||
| 240 | - | --- | |
| 241 | - | ||
| 242 | - | ## Summary | |
| 243 | - | ||
| 244 | - | Projects and contacts are functionally complete but under-served on discoverability (no sort, no multi-filter, no avatar color hashing, no sub-collection edit). Settings is comprehensive but architecturally split from the rest of the app (full-page view, auto-save without undo, no About/Account/version surface). The most user-visible critical fix is **adding an About section** with version info (#1) — it costs nothing and unblocks every support flow. The most architecturally significant fix is **converting Settings to a modal/drawer** (#5) — it brings settings into the same modeless pattern as the rest of the app and unlocks live theme preview (#12). The most overdue cleanup is **killing the `prompt()` call in contacts bulk-tag** (#2) — it's the last remaining native dialog after Step 9, and adding `showPromptDialog` to `GoingsOn.ui` is a 30-line task that pays off across future features. | |
| 245 | - | ||
| 246 | - | After Phase 5, the audit has identified 18 cross-cutting patterns spanning 5 surfaces. The four most-recurring (state-by-color-alone, filter-not-persisted, no-undo-on-bulk, native-dialog-survivors) are no longer per-phase findings — they're design-system rules that should be added to `docs/design-system.md` and enforced via `scripts/lint-frontend.sh`. |
| @@ -1,229 +0,0 @@ | |||
| 1 | - | # Phase 6 — Mobile Parity Sweep | |
| 2 | - | ||
| 3 | - | **Scope:** every surface in the mobile viewport — bottom nav, mobile-tab-bar, mobile-more popover, action sheet, mobile create button, mobile sort/filter bar, mobile-view-title; the touch-conditional JS branches across the app; the safe-area-inset implementation; the touch-gesture vocabulary; the keyboard-avoidance machinery; and the responsive CSS at all breakpoints. | |
| 4 | - | ||
| 5 | - | **Stack:** Tauri 2 webview running on iOS (TestFlight live) and prospective Android. Files reviewed: `js/mobile.js`, `js/touch.js`, `js/components.js#showContextMenuSmart`, `js/context-menus.js`, mobile branches in `js/tasks.js` / `tasks-render.js` / `emails.js` / `events-calendar.js` / `day-planning.js` / `keyboard.js` / `app.js#showWelcome` / `components-modal.js`, the `#mobile-tab-bar` / `#mobile-more-popover` / `#action-sheet` blocks in `index.html`, and 12 mobile-breakpoint CSS blocks in `styles.css` (lines ~2882, 2914, 5909, 5935, 6514, 7278, 7758, 8304, 8467, 8666, 8994, 9051). | |
| 6 | - | ||
| 7 | - | **Method:** the inventory was structured around one question: **is mobile a CSS restyle of the desktop UI, or a parallel implementation with its own JS and markup?** Audit applies universal principles plus the cross-cutting flat-design check, with extra attention to consistency-across-surfaces and feature-parity. | |
| 8 | - | ||
| 9 | - | --- | |
| 10 | - | ||
| 11 | - | ## Architecture verdict | |
| 12 | - | ||
| 13 | - | **Mobile is ~70% parallel implementation, ~30% CSS restyle.** | |
| 14 | - | ||
| 15 | - | - **Parallel JS paths (8 modules):** `mobile.js` (entire module), `touch.js` (entire module), `keyboard.js` (shortcut registration skipped on touch), `emails.js` (compose dispatch), `day-planning.js` (long-press vs drag), `components-modal.js` (drag-to-dismiss), `components.js` (action-sheet dispatch), `app.js#showWelcome` (step variants), `context-menus.js` (touch contextmenu suppression). | |
| 16 | - | - **Mobile-only markup (6 elements):** `#mobile-tab-bar`, `#mobile-create-btn`, `#mobile-more-btn` + `#mobile-more-popover`, `#action-sheet`, `#task-mobile-sort`, `#mobile-view-title`. | |
| 17 | - | - **CSS restyle (the 30%):** task rows (grid → flex column), settings (sidebar → horizontal tabs), modals (centered → bottom-sheet), day-plan sidebar collapse, hour-row hiding in week view. | |
| 18 | - | - **Detection is dual:** CSS uses `@media (max-width: 768px)` (with secondary breakpoints at 1024 / 900 / 640 / 600); JS uses `GoingsOn.touch.isTouchDevice` (`'ontouchstart' in window || navigator.maxTouchPoints > 0`). Both must be true for full mobile mode. | |
| 19 | - | ||
| 20 | - | This architectural choice is the single most important finding of the audit — it's the lens for everything that follows. | |
| 21 | - | ||
| 22 | - | --- | |
| 23 | - | ||
| 24 | - | ## Surface snapshot | |
| 25 | - | ||
| 26 | - | - **Bottom nav (`#mobile-tab-bar`):** 5 buttons — Work · Time · Messages · `+` · More. 52 px tall + `env(safe-area-inset-bottom)`, `z-index: 1100`, fixed at bottom. | |
| 27 | - | - **More popover:** single-button menu (Settings only). Opens above the tab bar at `bottom: calc(52px + safe-area-inset-bottom)`. | |
| 28 | - | - **Action sheet (`#action-sheet`):** bottom-sheet modal that *replaces* the desktop context menu on touch. `z-index: 10001` (above modals). | |
| 29 | - | - **Mobile-view-title (`#mobile-view-title`):** in-header subview label, separate from `.page-title`. | |
| 30 | - | - **Mobile sort/filter (`#task-mobile-sort`):** desktop sort/filter lives in the filter bar; mobile gets its own dropdown + "Filters" toggle. | |
| 31 | - | - **Compose dispatch:** desktop opens `compose.html` in a new Tauri window; mobile opens `openComposeModal` (in-app form modal) — Phase 3 #3 already audited this. | |
| 32 | - | - **Day-plan timeline:** `--timeline-slot-h: 12px` desktop → `22px` at ≤600 px. Long-press 500 ms to paint, snaps to 30-min boundary. | |
| 33 | - | - **Calendar week view:** desktop 7-col grid; ≤600 px collapses to single-day swipeable carousel. | |
| 34 | - | - **Touch gestures wired in `mobile.js`:** swipe row (complete/snooze/archive/delete), long-press (selection + paint), pull-to-refresh, swipe nav (between days/views), drag-to-dismiss (modals + action sheet), rubber-band overscroll. | |
| 35 | - | - **iOS meta tags:** `viewport-fit=cover` is present; `apple-mobile-web-app-status-bar-style` and `theme-color` are NOT. | |
| 36 | - | ||
| 37 | - | --- | |
| 38 | - | ||
| 39 | - | ## Critical (fix before declaring Phase 6 done) | |
| 40 | - | ||
| 41 | - | ### 1. Kanban board is non-functional on mobile — drag-drop has no touch fallback | |
| 42 | - | - **Category:** Mappings, Anticipation. (**Universal**) | |
| 43 | - | - **Location:** `js/tasks-kanban.js` lines ~72–108 — `draggable="true"`, `ondragstart` / `ondrop` only. No `touchstart` / `touchmove` handlers; no `addLongPress`-and-pick alternative. | |
| 44 | - | - **Observation:** the Kanban view renders fine on touch — three columns (Pending / Started / Completed), cards inside each. But the only way to move a card between columns is HTML5 drag-and-drop, which is not triggered by touch on iOS (Android Chrome partially supports drag events with long-press; iOS Safari and WKWebView do not). A mobile user can *see* the board but cannot *change* it. There's no context-menu action to move a card to a different status either; the row context menu (action sheet) doesn't include "Move to Started / Completed". | |
| 45 | - | - **Why it matters:** the entire board view is shelf-ware for mobile users. They can browse but can't act, which is worse than not having the view at all — they see the affordance and discover it's dead. | |
| 46 | - | - **Recommendation:** | |
| 47 | - | - Quickest: on mobile, hide the "Board" pill in the view-mode toggle entirely. Show only List. | |
| 48 | - | - Better: add a context-menu action sheet entry on each card — "Move to Pending / Started / Completed" — and a tap-target long-press to invoke it. | |
| 49 | - | - Best: real touch drag-drop with a lift-on-long-press, ghost card following finger, and column highlight on hover. Libraries like SortableJS handle this; ~50 lines of integration. | |
| 50 | - | - Pick option 1 (hide) until option 3 is delivered. Showing a broken affordance is worse than not showing it. | |
| 51 | - | ||
| 52 | - | ### 2. Mobile is a parallel implementation with feature drift | |
| 53 | - | - **Category:** Consistency, Architecture. (**Universal — meta-finding**) | |
| 54 | - | - **Location:** the 8 JS modules with touch-conditional branches; 6 mobile-only HTML elements; the 12 breakpoint blocks. | |
| 55 | - | - **Observation:** the inventory confirms what Phase 3 #3 (compose), Phase 1 #8 (dual title system), Phase 5 (settings sidebar reflow), and the kanban finding above all point to — mobile is built as a *parallel surface* rather than a responsive restyle. The pattern has costs: | |
| 56 | - | - **Feature drift:** Phase 3 found the desktop compose has address highlighting and Cmd+S that mobile lacks. Phase 1 found mobile-view-title can drift from the in-content page-title. Kanban above is a third instance. | |
| 57 | - | - **Maintenance multiplier:** every UI change touches 1.7× as many files because the same logical thing has a desktop branch and a mobile branch. | |
| 58 | - | - **Inconsistent gesture vocabulary:** swipe-to-archive in emails uses 80 px threshold; swipe-to-snooze in tasks uses 80 px; long-press is 500 ms in some places. The system works but isn't documented or unified. | |
| 59 | - | - **Why it matters:** this is the architectural debt that produces all the smaller mobile-parity findings. Each per-phase mobile finding is a symptom; the root is the architecture. | |
| 60 | - | - **Recommendation:** | |
| 61 | - | - **Short-term (this audit cycle):** document the mobile-vs-desktop split in `docs/design-system.md`. List every feature that branches on `isTouchDevice` and what each branch does. This makes drift visible to future contributors. | |
| 62 | - | - **Medium-term:** for each touch branch, ask: "could this be CSS-only?" Compose modal vs. window is justified (Tauri webview limitation, surfaced). Mobile-tab-bar vs. desktop-tabs could be one component with two CSS layouts. Action sheet vs. context menu could be one helper that picks display mode by media query. | |
| 63 | - | - **Long-term:** target ≤30 % parallel. The win is fewer drift bugs; the cost is one rewrite pass. | |
| 64 | - | ||
| 65 | - | ### 3. Pills are unreachable from the mobile bottom nav (Phase 1 #1 — reconfirmed) | |
| 66 | - | - **Category:** Mappings, Visibility of state. (**Universal — reconfirmed**) | |
| 67 | - | - **Location:** mobile-tab-bar has Work / Time / Messages buttons; the per-tab pills (Tasks/Projects, Day/Week/Month/Timer/Events, Email/Contacts) sit at the top of the content area on mobile and scroll away. | |
| 68 | - | - **Observation:** Phase 1 raised this as critical; Phase 6 reconfirms because mobile-only markup is the lever to fix it. The bottom nav has parallel markup; that markup could surface pills directly (long-press the Time tab → action sheet picker of Day/Week/Month). | |
| 69 | - | - **Recommendation:** see Phase 1 #1. The fix integrates cleanly with the mobile-only architecture — add a long-press handler on each `.mobile-tab` that opens an action sheet of that tab's pills. | |
| 70 | - | ||
| 71 | - | --- | |
| 72 | - | ||
| 73 | - | ## Major (high impact, lower urgency) | |
| 74 | - | ||
| 75 | - | ### 4. Touch gestures are undiscoverable | |
| 76 | - | - **Category:** Discoverability, Anticipation. (**Universal**) | |
| 77 | - | - **Location:** swipe handlers in `mobile.js` lines ~224–260 (swipe row to complete/snooze/archive/delete); pull-to-refresh at line ~280; drag-to-dismiss modals/sheets. | |
| 78 | - | - **Observation:** GoingsOn ships at least 8 distinct touch gestures (row swipe in 3 directions per surface, pull-to-refresh, long-press select, long-press paint, long-press timeline-item, drag-to-dismiss, swipe-nav between days/views, rubber-band overscroll). None of them are documented in the UI. A new mobile user has to either stumble onto them or be told. | |
| 79 | - | - **Why it matters:** an app with 8 invisible gestures is an app with 8 features no one uses. Compare to iOS Mail, which surfaces swipe colors with a label peek as you drag — you see "Trash" appear as you swipe past the threshold. | |
| 80 | - | - **Recommendation:** | |
| 81 | - | - **Peek labels** on swipe: as the user drags a row past ~30 % of the threshold, show the action that will fire ("Complete", "Snooze", "Archive") with a colored background. iOS / Android Mail does this universally. | |
| 82 | - | - **First-run hint** on the bottom-most row: a small "← Swipe →" overlay that fades on first scroll. | |
| 83 | - | - **Gestures index** in the `?` shortcuts overlay — currently desktop-only; on touch, replace keyboard shortcuts with a "Gestures" reference (swipe row, long-press, etc.). | |
| 84 | - | - The todo file already flags "Touch gesture hints on first mobile use" — promote it from sprint to Phase 6 fix. | |
| 85 | - | ||
| 86 | - | ### 5. iOS-specific meta tags are missing | |
| 87 | - | - **Category:** Polish, Trust. (**Universal**) | |
| 88 | - | - **Location:** `index.html` lines 1–10 — has `<meta name="viewport" content="…viewport-fit=cover">` but no `theme-color`, no `apple-mobile-web-app-status-bar-style`, no `apple-mobile-web-app-capable`, no `apple-touch-icon`. | |
| 89 | - | - **Observation:** the Tauri webview on iOS picks up its tint and chrome from defaults. The status bar above the app is white-on-white or default. On Android Chrome (if the app ever lands as a PWA), `theme-color` would tint the browser chrome to match the app. | |
| 90 | - | - **Why it matters:** the absence is a small "this isn't a real iOS app" tell. The status bar above the app doesn't match the theme. On dark Catppuccin Mocha, the iOS status bar reads as a different surface entirely. Polish-level but it adds up. | |
| 91 | - | - **Recommendation:** | |
| 92 | - | - Add `<meta name="theme-color" content="#E0E4FA">` (the current `--bg-primary` light value) and update via JS when theme switches. | |
| 93 | - | - Add `<meta name="apple-mobile-web-app-status-bar-style" content="default">` (or `black-translucent` if you want the app to extend under the status bar with safe-area-inset). | |
| 94 | - | - Add an `apple-touch-icon` linking to a high-res app icon. | |
| 95 | - | ||
| 96 | - | ### 6. Long-press at 500 ms triggers different actions on different targets | |
| 97 | - | - **Category:** Consistency, Mappings. (**Universal**) | |
| 98 | - | - **Location:** `js/touch.js` default 500 ms; `day-planning.js` lines ~81–90 (timeline slots → paint), lines 96 (timeline items → action sheet), `mobile.js` lines ~317–327 (row long-press → select). | |
| 99 | - | - **Observation:** the same hold duration triggers four different things depending on where the finger is: select a row, open a context menu on a row, paint a block on the timeline, open an action sheet on a timeline item. Users build a single mental model: "long-press = do something contextual" — which is fine *if* the contextual feedback is immediate (a card lifts, a glow appears). Today's implementation just fires the action after 500 ms with no pre-feedback during the hold. | |
| 100 | - | - **Why it matters:** ambiguous gestures fail mid-action. A user trying to scroll the timeline accidentally holds for 500 ms and the paint modal opens. They cancel; they try again; they still don't know how to tell the app "I just want to scroll". | |
| 101 | - | - **Recommendation:** | |
| 102 | - | - Add an in-progress affordance: at 250 ms (half the threshold), show a small visual cue — finger pressure highlight, card scale up 2 %, or a ring around the touch point. This tells users "the gesture is being recognized; lift now to cancel". | |
| 103 | - | - Standardize the threshold and document it in the design-system charter ("long-press = 500 ms with 250-ms feedback ring"). | |
| 104 | - | - On the timeline, distinguish slot-paint from item-action with the touch-target distinction (paint on empty slots, action sheet on filled items) — which the code already does; verify the feedback differs. | |
| 105 | - | ||
| 106 | - | ### 7. Modal on tablet stretches edge-to-edge with no max-width | |
| 107 | - | - **Category:** Anticipation, Polish. (**Polish/Universal**) | |
| 108 | - | - **Location:** `styles.css` line ~7359 — `.modal-container { width: 100% !important; max-width: 100% !important; }` inside the mobile media query. | |
| 109 | - | - **Observation:** the same media query (≤768 px) applies to a 768 × 1024 portrait tablet. A modal at 768 px wide is uncomfortable to read; the line lengths are too long. | |
| 110 | - | - **Recommendation:** add a sub-breakpoint or use `min(100%, 600px)`: `.modal-container { width: 100%; max-width: min(100%, 600px); margin: 0 auto; }`. On phones nothing changes; on tablets the modal is centered with reasonable line length. | |
| 111 | - | ||
| 112 | - | ### 8. The mobile-create-button (`+`) is context-dependent with no visual change | |
| 113 | - | - **Category:** Anticipation, Visibility. (**Universal — reconfirmed Phase 1 #5**) | |
| 114 | - | - **Location:** `#mobile-create-btn` in tab bar; `navigation.js#newItemForCurrentView` dispatches based on active tab. | |
| 115 | - | - **Observation:** Phase 1 noted this; Phase 6 confirms it across the parallel implementation. The button is always a green `+`. It creates a task in Work, an event in Time, an email in Messages — and the button gives no signal of which. | |
| 116 | - | - **Recommendation:** as Phase 1 — change the button's icon or label per active tab. Easier on mobile because the button is parallel markup; only one place to update. | |
| 117 | - | ||
| 118 | - | ### 9. Settings sidebar collapses to a horizontal wrap-row on mobile | |
| 119 | - | - **Category:** Hierarchy, Fitts. (**Universal**) | |
| 120 | - | - **Location:** `styles.css` lines 9051–9067 — `.settings-page-layout { flex-direction: column; }`, `.settings-sidebar { flex-direction: row; flex-wrap: wrap; }`. | |
| 121 | - | - **Observation:** on mobile, the 6-section sidebar becomes a row that wraps to 2 or 3 lines on smaller phones. Each section button is a wrap target; the user has to read tiny pill-labels to find the section they want. Phone settings apps (iOS Settings, Android Settings) use a list-and-detail pattern (root list → tap → detail), not a wrap-row of tabs. | |
| 122 | - | - **Why it matters:** the wrap-row is hard to scan and inefficient at narrow widths. With 6 items + a Back button, it fills most of the visible content area on a phone. | |
| 123 | - | - **Recommendation:** | |
| 124 | - | - Convert to a list-and-detail pattern on mobile: the settings view first shows a list of sections; tapping one navigates (push-style) to that section's content. Back arrow returns to the list. | |
| 125 | - | - Alternative: hide the sidebar entirely and use the in-content header to navigate (Section Title with a dropdown arrow → tap opens an action sheet of sections). | |
| 126 | - | ||
| 127 | - | ### 10. The mobile-view-title is set imperatively and can drift from the in-content `.page-title` (Phase 1 #8 — reconfirmed) | |
| 128 | - | - **Category:** Consistency, Visibility of state. (**Universal — reconfirmed**) | |
| 129 | - | - **Location:** `navigation.js#updateMobileViewTitle` writes to `#mobile-view-title`; each subview writes its own `.page-title` separately. | |
| 130 | - | - **Observation:** the parallel markup makes drift possible. If a subview changes its title at runtime ("Tasks" → "Snoozed Tasks") but doesn't call `updateMobileViewTitle`, the mobile header shows the stale label. | |
| 131 | - | - **Recommendation:** see Phase 1 #8 — make `.page-title` the source of truth via a MutationObserver, or route every set through one helper. | |
| 132 | - | ||
| 133 | - | ### 11. Multiple breakpoints (5 of them: 1024, 900, 768, 640, 600) without a documented system | |
| 134 | - | - **Category:** Consistency. (**Polish/Universal**) | |
| 135 | - | - **Location:** `styles.css` — `@media` blocks at 1024, 900, 768, 640, 600. | |
| 136 | - | - **Observation:** five breakpoints across the stylesheet, each touching different rules. No `:root` token defines them. A future contributor adding a responsive rule has to guess which threshold to use. Some rules at 600 px (calendar mobile day) conflict conceptually with 768 px (everything else mobile) — what's the right call for a 700-px-wide window? | |
| 137 | - | - **Recommendation:** | |
| 138 | - | - Document the breakpoint system in `design-system.md`: e.g., 480 px (phone-small), 768 px (phone-large / tablet-edge), 1024 px (tablet-large / desktop-edge). Map every existing `@media` rule to one of three. | |
| 139 | - | - Optionally CSS custom-property the breakpoints, though CSS doesn't natively support `@media (max-width: var(...))` — keep them as constants but document. | |
| 140 | - | ||
| 141 | - | ### 12. `@media (hover: none)` removes `:hover` rules — verify kebab/action affordances aren't lost | |
| 142 | - | - **Category:** Discoverability. (**Universal**) | |
| 143 | - | - **Location:** `styles.css` line ~7758 — `@media (hover: none) { /* :hover overrides */ }`. | |
| 144 | - | - **Observation:** the rule correctly removes hover-only styling on touch devices (no hover state exists). But many GoingsOn affordances rely on hover-to-reveal: kebab buttons that fade in on row hover, action buttons that brighten on hover. With hover removed, those affordances may be always-on (full opacity) — which is correct, but verify the desktop fade-in isn't the *only* way to discover them. | |
| 145 | - | - **Recommendation:** audit `:hover` rules and check that the touch fallback (always-visible) is the intended state, not the residue of "fade only desktop is allowed to use". | |
| 146 | - | ||
| 147 | - | --- | |
| 148 | - | ||
| 149 | - | ## Minor (worth fixing during normal cleanup) | |
| 150 | - | ||
| 151 | - | ### 13. Mobile sort/filter has its own markup parallel to the desktop filter bar | |
| 152 | - | - **Category:** Consistency. | |
| 153 | - | - **Location:** `#task-mobile-sort` in `index.html:71–80`; desktop `.filter-bar` in the same view. | |
| 154 | - | - **Observation:** mobile gets a slim dropdown + "Filters" toggle; desktop gets a full filter bar. The mobile filter modal is presumably the same content as the desktop bar but in a popover. Worth verifying parity — does mobile expose every filter desktop has? | |
| 155 | - | - **Recommendation:** verify parity; if features are missing on mobile, file as feature gaps. | |
| 156 | - | ||
| 157 | - | ### 14. Touch swipe row has no haptic feedback on iOS | |
| 158 | - | - **Category:** Feedback. (**Polish**) | |
| 159 | - | - **Location:** `mobile.js#addSwipeActions` doesn't emit haptics on threshold cross. | |
| 160 | - | - **Observation:** native iOS apps fire a haptic tick when a swipe action crosses the action threshold. GoingsOn doesn't. | |
| 161 | - | - **Recommendation:** Tauri's `tauri-plugin-haptics` (or the equivalent for the platform) on iOS supports `selectionChanged()`. Wire on swipe threshold crossing. Small change, big perceived-quality lift. | |
| 162 | - | ||
| 163 | - | ### 15. Pull-to-refresh has no visible indicator pre-threshold | |
| 164 | - | - **Category:** Feedback. | |
| 165 | - | - **Location:** `mobile.js#addPullToRefresh`. | |
| 166 | - | - **Observation:** verified the gesture exists; the inventory doesn't show whether it visually surfaces during the pull (e.g., a "Pull to refresh" arrow appearing as the user pulls). | |
| 167 | - | - **Recommendation:** show a small downward arrow that rotates 180 ° when the threshold is reached, then becomes a spinner during refresh. Standard pattern. | |
| 168 | - | ||
| 169 | - | ### 16. Mobile more-popover has only "Settings"; under-utilizes the affordance | |
| 170 | - | - **Category:** Anticipation. (**Polish**) | |
| 171 | - | - **Location:** `#mobile-more-popover` markup; `navigation.js:303–318` populates it with Settings only. | |
| 172 | - | - **Observation:** the More popover is the natural place for desktop-header affordances (Search, Help, About, version). On mobile, the search button doesn't exist in the bottom nav and isn't in More — there's no way to invoke Cmd+K equivalent. | |
| 173 | - | - **Recommendation:** populate More with Search, Help, Account, About in addition to Settings. The More popover is the iOS-equivalent of the desktop header right-actions cluster. | |
| 174 | - | ||
| 175 | - | ### 17. No status-bar-color customization (Finding #5 sub-case) | |
| 176 | - | - See Finding #5. | |
| 177 | - | ||
| 178 | - | ### 18. Action-sheet `z-index: 10001` vs modal-overlay implicit — verify stacking | |
| 179 | - | - **Category:** Polish. | |
| 180 | - | - **Location:** action sheet at 10001; modal at lower (10000-ish implicit). | |
| 181 | - | - **Observation:** the action sheet sits above the modal. If a modal opens an action sheet (e.g., long-press a row inside a modal), the action sheet correctly appears above. Confirm there's no case where a modal needs to appear above an action sheet (e.g., a confirm dialog spawned from an action sheet action). | |
| 182 | - | - **Recommendation:** confirm. If broken, hoist the modal above the sheet conditionally. | |
| 183 | - | ||
| 184 | - | ### 19. Welcome-flow mobile/desktop branch is a string-template fork | |
| 185 | - | - **Category:** Maintainability. (**Polish**) | |
| 186 | - | - **Location:** `app.js#showWelcome`. | |
| 187 | - | - **Observation:** each step has two template-string variants. Easy to drift (touch up the desktop variant, forget the touch variant). Phase 1 already touched this; Phase 6 confirms it's a real maintenance issue. | |
| 188 | - | - **Recommendation:** consolidate the gesture/keyboard distinction into a single template that conditionally renders "tap +" or "press q" inside the same step. Less repetition. | |
| 189 | - | ||
| 190 | - | --- | |
| 191 | - | ||
| 192 | - | ## Polish | |
| 193 | - | ||
| 194 | - | ### 20. Drag-to-dismiss has no visible chevron / handle hint | |
| 195 | - | - **Category:** Discoverability. | |
| 196 | - | - **Location:** `.modal-drag-handle` and `.action-sheet-handle` markup. | |
| 197 | - | - **Observation:** there's a `<div class="modal-drag-handle">` in the modal markup. Verify it's styled visibly (a small horizontal bar at the top of the modal sheet that signals "you can grab this"). If invisible, users don't know they can swipe down to close. | |
| 198 | - | - **Recommendation:** style as a 36 × 4 px rounded-rectangle, `--text-muted` color, centered, 8 px from the top edge. Standard iOS sheet handle. | |
| 199 | - | ||
| 200 | - | ### 21. Bottom-tab labels are uppercase with `letter-spacing` — small letters at edge cases | |
| 201 | - | - **Category:** Polish. | |
| 202 | - | - **Location:** `.mobile-tab-label` — uppercase, 0.7 rem, letter-spacing 0.05em. | |
| 203 | - | - **Observation:** "MESSAGES" at 0.7rem letter-spaced may push the label off the tab on a 320 px phone (iPhone SE 1st gen). Verify. | |
| 204 | - | - **Recommendation:** drop letter-spacing or reduce font to 0.65 rem on the smallest viewport. | |
| 205 | - | ||
| 206 | - | ### 22. `mobile.js` has its own keyboard-avoidance routine; verify it doesn't fight Tauri's | |
| 207 | - | - **Category:** Polish. | |
| 208 | - | - **Location:** `mobile.js:187–209` — `visualViewport` resize handler. | |
| 209 | - | - **Observation:** iOS Tauri webview has some default keyboard-avoidance; the app adds its own. On some devices both may fire and overshoot the scroll. | |
| 210 | - | - **Recommendation:** test on an iPhone 14 / 15 with various input positions. Add `passive: true` listeners where possible to avoid layout thrash. | |
| 211 | - | ||
| 212 | - | --- | |
| 213 | - | ||
| 214 | - | ## Cross-cutting / flat-design check | |
| 215 | - | ||
| 216 | - | The four meta-patterns now appear with reinforcement: | |
| 217 | - | ||
| 218 | - | - **State-by-color-alone** — `.mobile-tab.active` is a color shift only (Phase 1 #4); reconfirmed. | |
| 219 | - | - **Filter / view state not persisted** — no change here, but the mobile sort dropdown writes to nowhere persistent (verify). | |
| 220 | - | - **No-undo on touch swipe actions** — swipe-to-complete a task on mobile: does it show an undo toast? Phase 2 #2 said tasks complete has undo on desktop; verify the swipe-complete on mobile follows the same path. If it skips the toast (because the swipe gesture already feels confirmatory), that's a regression vs. desktop. | |
| 221 | - | - **Native dialog survivors** — `window.prompt` in contacts bulk-tag (Phase 5 #2) doesn't have a touch variant. On iOS this is a stronger issue — Safari/WKWebView often returns null from `prompt()` without showing UI. Promote Phase 5 #2 to high priority. | |
| 222 | - | ||
| 223 | - | **New pattern raised here:** **parallel-implementation drift** — the meta-finding (#2 above) is that every per-surface mobile gap is a child of one architectural choice. This becomes the design-system rule: "mobile is responsive CSS by default; JS branches require explicit justification and `docs/design-system.md` entry." | |
| 224 | - | ||
| 225 | - | --- | |
| 226 | - | ||
| 227 | - | ## Summary | |
| 228 | - | ||
| 229 | - | Mobile is functionally extensive — 8 touch gestures, comprehensive safe-area-inset, parallel-nav markup, action-sheet-replaces-context-menu dispatch, dedicated compose modal, day-plan slot-height bump, keyboard avoidance. It is also a parallel implementation (~70 % branched, ~30 % restyled), which is the root cause of every per-phase mobile-parity finding the audit has surfaced. The single largest visible bug is the broken Kanban (Finding #1) — hide it on mobile until touch drag-drop works. The single largest architectural debt is the parallel implementation itself (Finding #2) — start surfacing every touch branch in the design-system charter so contributors can see and audit the drift. The single largest user-facing missing feature is touch-gesture discoverability (Finding #4) — peek-labels on swipe + a gestures reference in the `?` overlay would unlock 8 features users currently can't find. After Phase 6, the audit is one phase away from complete; Phase 7 will roll up the four meta-patterns and the theme conformance sweep into a single closing report. |