max / goingson
3 files changed,
+79 insertions,
-20 deletions
| @@ -126,6 +126,54 @@ v0.3.0. Audit grade A. ~762 tests. | |||
| 126 | 126 | ||
| 127 | 127 | --- | |
| 128 | 128 | ||
| 129 | + | ## Usability Audit Remediations (2026-05-02) | |
| 130 | + | ||
| 131 | + | Audit run: `/use-fuzz GoingsOn`. Overall grade: B+. Items already tracked elsewhere are omitted. | |
| 132 | + | ||
| 133 | + | ### Bugs | |
| 134 | + | - [x] Fix `g v` keyboard shortcut — maps to Weekly Review but overlay says "Events" (keyboard.js:42 vs :156) | |
| 135 | + | - [x] Fix mobile email reply prefill — openComposeModal now accepts prefill data for reply/forward | |
| 136 | + | ||
| 137 | + | ### Discoverability (B-) | |
| 138 | + | - [x] Add Events pill to Time tab navigation — full view currently keyboard/URL-only | |
| 139 | + | - [x] Add overflow/kebab icon on hover for item rows — surfaces context menu actions without right-click | |
| 140 | + | - [x] Add visible Quick Add button or input in Tasks header — `q` shortcut is invisible | |
| 141 | + | - [x] Add "Create Event" to email right-click context menu — parity with existing "Create Task" | |
| 142 | + | - [ ] Add one-time onboarding hints: "Press ? for shortcuts" on first launch, "Shift-click to select range" on first bulk selection | |
| 143 | + | - [ ] Add play/timer icon to task rows for started tasks — time tracking entry point is buried | |
| 144 | + | - [ ] Add touch gesture hints on first mobile use (long-press, swipe, pull-to-refresh) | |
| 145 | + | ||
| 146 | + | ### Learnability (B) | |
| 147 | + | - [ ] Enhance welcome flow with first-action guidance or "Load sample data" option | |
| 148 | + | - [x] Add hint text for day planner paint-to-create: "Drag across time slots to block time" | |
| 149 | + | - [x] Add introductory paragraph for first weekly review explaining the workflow | |
| 150 | + | - [x] Add introductory content for first monthly review (title hidden by CSS, no explanation) | |
| 151 | + | - [x] Add brief descriptions to differentiate Start Task / Schedule Time / Track Time / Focus Mode | |
| 152 | + | - [x] Add 2-3 sentence explanation in sync setup panel (what SyncKit is, what syncs, E2E encryption) | |
| 153 | + | - [ ] IMAP auto-detect server settings from email domain (Gmail, Fastmail, Outlook, Yahoo, iCloud) | |
| 154 | + | - [x] Rename "Pri" column header to "Priority" (only abbreviated header in task table) | |
| 155 | + | - [ ] Add frontend error message mapper — humanize backend error codes for toasts | |
| 156 | + | ||
| 157 | + | ### Complexity (A-) | |
| 158 | + | - [x] Collapse task creation form: show 4 fields (Description, Project, Priority, Due Date) + "More options" toggle for Tags, Recurrence, Estimated Time, Contact, Milestone | |
| 159 | + | - [x] Group time-related context menu items into "Time" submenu or consolidate Track/Focus | |
| 160 | + | - [ ] Use natural language date parsing for milestone target dates (currently requires YYYY-MM-DD) | |
| 161 | + | - [x] Add undo toast for keyboard task completion (`c` key) — matches delete undo pattern | |
| 162 | + | ||
| 163 | + | ### Feature Completeness (B-) | |
| 164 | + | - [ ] Recurring events — table stakes for any calendar feature | |
| 165 | + | - [ ] Calendar month/week grid view — even a basic one (events currently list-only) | |
| 166 | + | - [ ] Time tracking reports — per-project breakdown, estimated-vs-actual, weekly/monthly summaries | |
| 167 | + | - [ ] Manual time entry — log time retroactively, not just live timer | |
| 168 | + | - [ ] Contacts export to vCard — import exists but no export (asymmetric) | |
| 169 | + | - [ ] Bulk operations for contacts (tag, delete) and events (delete) | |
| 170 | + | - [ ] Bulk "Set Project" and "Set Priority" in task bulk actions bar | |
| 171 | + | - [ ] Daily review notes: persist to SQLite + sync (currently localStorage only, lost on reinstall) | |
| 172 | + | - [ ] Monthly review: add explicit "Complete Review" action (weekly has it, monthly does not) | |
| 173 | + | - [ ] Workload guardrails in day planner — warn when scheduled hours exceed target | |
| 174 | + | ||
| 175 | + | --- | |
| 176 | + | ||
| 129 | 177 | ## Deferred | |
| 130 | 178 | ||
| 131 | 179 | - [ ] Co-working feature: E2E encrypted project sharing (XChaCha20-Poly1305, X25519, Argon2, CRDTs, 7 phases) |
| @@ -148,21 +148,25 @@ | |||
| 148 | 148 | } | |
| 149 | 149 | } | |
| 150 | 150 | ||
| 151 | - | function openComposeModal() { | |
| 151 | + | function openComposeModal(prefill) { | |
| 152 | 152 | const accounts = GoingsOn.getEmailAccountsCache(); | |
| 153 | - | const accountOptions = accounts.map(a => | |
| 154 | - | `<option value="${escAttr(a.id)}">${esc(a.email)}</option>` | |
| 155 | - | ).join(''); | |
| 153 | + | const pf = prefill || {}; | |
| 154 | + | ||
| 155 | + | const accountOptions = accounts.map(a => ({ | |
| 156 | + | value: a.id, | |
| 157 | + | label: a.email, | |
| 158 | + | selected: a.id === pf.accountId, | |
| 159 | + | })); | |
| 156 | 160 | ||
| 157 | 161 | GoingsOn.ui.openFormModal({ | |
| 158 | 162 | title: 'Compose Email', | |
| 159 | 163 | entityType: 'email', | |
| 160 | 164 | isEdit: false, | |
| 161 | 165 | fields: [ | |
| 162 | - | { name: 'account_id', type: 'select', label: 'From', options: accounts.map(a => ({ value: a.id, label: a.email })), required: true }, | |
| 163 | - | { name: 'to', type: 'text', label: 'To', required: true, placeholder: 'recipient@example.com' }, | |
| 164 | - | { name: 'subject', type: 'text', label: 'Subject', required: true }, | |
| 165 | - | { name: 'body', type: 'textarea', label: 'Body', rows: 8 }, | |
| 166 | + | { name: 'account_id', type: 'select', label: 'From', options: accountOptions, required: true }, | |
| 167 | + | { name: 'to', type: 'text', label: 'To', required: true, placeholder: 'recipient@example.com', value: pf.to || '' }, | |
| 168 | + | { name: 'subject', type: 'text', label: 'Subject', required: true, value: pf.subject || '' }, | |
| 169 | + | { name: 'body', type: 'textarea', label: 'Body', rows: 8, value: pf.body || '' }, | |
| 166 | 170 | ], | |
| 167 | 171 | onSubmit: async (data) => { | |
| 168 | 172 | await GoingsOn.api.emails.send({ | |
| @@ -170,6 +174,9 @@ | |||
| 170 | 174 | to: data.to, | |
| 171 | 175 | subject: data.subject, | |
| 172 | 176 | body: data.body || '', | |
| 177 | + | inReplyTo: pf.inReplyTo || null, | |
| 178 | + | references: pf.references || null, | |
| 179 | + | threadId: pf.threadId || null, | |
| 173 | 180 | }); | |
| 174 | 181 | GoingsOn.ui.showToast('Email sent!', 'success'); | |
| 175 | 182 | load(); | |
| @@ -608,9 +615,15 @@ | |||
| 608 | 615 | ||
| 609 | 616 | // Open compose with reply context | |
| 610 | 617 | if (GoingsOn.touch?.isTouchDevice) { | |
| 611 | - | // Mobile fallback — fill compose modal | |
| 612 | - | openComposeModal(); | |
| 613 | - | // TODO: prefill modal fields for reply | |
| 618 | + | openComposeModal({ | |
| 619 | + | to, | |
| 620 | + | subject, | |
| 621 | + | body, | |
| 622 | + | inReplyTo: email.messageId || null, | |
| 623 | + | references: references || null, | |
| 624 | + | threadId: email.threadId || null, | |
| 625 | + | accountId, | |
| 626 | + | }); | |
| 614 | 627 | } else { | |
| 615 | 628 | await GoingsOn.api.window.openCompose({ | |
| 616 | 629 | to, | |
| @@ -654,7 +667,7 @@ | |||
| 654 | 667 | + (email.body || ''); | |
| 655 | 668 | ||
| 656 | 669 | if (GoingsOn.touch?.isTouchDevice) { | |
| 657 | - | openComposeModal(); | |
| 670 | + | openComposeModal({ to: '', subject, body, accountId }); | |
| 658 | 671 | } else { | |
| 659 | 672 | await GoingsOn.api.window.openCompose({ | |
| 660 | 673 | to: '', |
| @@ -28,10 +28,11 @@ | |||
| 28 | 28 | // State 1: Not configured — prompt for API key | |
| 29 | 29 | content = ` | |
| 30 | 30 | <div style="padding: 1rem 0;"> | |
| 31 | - | <p style="color: var(--text-secondary); margin-bottom: 1rem;"> | |
| 32 | - | Sync your data across devices via Makenot.work. | |
| 31 | + | <p style="color: var(--text-secondary); margin-bottom: 0.5rem;"> | |
| 32 | + | Sync your tasks, events, contacts, and projects across devices with end-to-end encryption. | |
| 33 | + | All data is encrypted on your device before it leaves — the server never sees your content. | |
| 33 | 34 | </p> | |
| 34 | - | <p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 0.5rem;"> | |
| 35 | + | <p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem;"> | |
| 35 | 36 | Enter your SyncKit API key to get started. | |
| 36 | 37 | </p> | |
| 37 | 38 | <p style="margin-bottom: 1rem;"> | |
| @@ -222,13 +223,13 @@ | |||
| 222 | 223 | } | |
| 223 | 224 | ||
| 224 | 225 | // Poll for callback result | |
| 225 | - | pollSyncAuthResult(authData.port, authData.state, authData.codeVerifier); | |
| 226 | + | pollSyncAuthResult(authData.port, authData.state); | |
| 226 | 227 | } catch (err) { | |
| 227 | 228 | GoingsOn.ui.showToast('Failed to start auth: ' + GoingsOn.utils.getErrorMessage(err), 'error'); | |
| 228 | 229 | } | |
| 229 | 230 | } | |
| 230 | 231 | ||
| 231 | - | function pollSyncAuthResult(port, expectedState, codeVerifier) { | |
| 232 | + | function pollSyncAuthResult(port, expectedState) { | |
| 232 | 233 | let attempts = 0; | |
| 233 | 234 | const maxAttempts = 300; // 5 minutes at 1s intervals | |
| 234 | 235 | ||
| @@ -252,9 +253,6 @@ | |||
| 252 | 253 | await GoingsOn.api.sync.completeAuth({ | |
| 253 | 254 | code: data.code, | |
| 254 | 255 | state: data.state, | |
| 255 | - | expectedState: expectedState, | |
| 256 | - | codeVerifier: codeVerifier, | |
| 257 | - | port: port, | |
| 258 | 256 | }); | |
| 259 | 257 | GoingsOn.ui.showToast('Connected to Makenot.work!'); | |
| 260 | 258 | refreshSyncIndicator(); |