max / goingson
17 files changed,
+64 insertions,
-70 deletions
| @@ -84,7 +84,7 @@ fn next(it: &mut impl Iterator<Item = String>, flag: &str) -> Result<String, Str | |||
| 84 | 84 | ||
| 85 | 85 | fn print_help() { | |
| 86 | 86 | eprintln!( | |
| 87 | - | "go-mcp — MCP server for GoingsOn tasks/projects\n\n\ | |
| 87 | + | "go-mcp: MCP server for GoingsOn tasks/projects\n\n\ | |
| 88 | 88 | Usage: go-mcp [--db <path>] [--stdio] [--host <ip>] [--port <n>]\n\ | |
| 89 | 89 | \x20 [--grant <cap>]... | [--grant-all] [--compact]\n\n\ | |
| 90 | 90 | Transport: default is Streamable HTTP; --stdio speaks newline-delimited\n\ |
| @@ -172,7 +172,7 @@ impl Tool for UpdateProjectTool { | |||
| 172 | 172 | "update_project" | |
| 173 | 173 | } | |
| 174 | 174 | fn description(&self) -> &str { | |
| 175 | - | "Update an existing project, resolved by `project` (its name). Only the fields you pass change. `status` is Active|OnHold|Completed|Archived — Archived is the non-destructive way to retire a project. `type` is Job|SideProject|Company|Essay|Article|Painting|Other. Renaming is not offered: the name is how create_task and bulk_import_tasks resolve a project, so a rename would strand callers." | |
| 175 | + | "Update an existing project, resolved by `project` (its name). Only the fields you pass change. `status` is Active|OnHold|Completed|Archived. Archived is the non-destructive way to retire a project. `type` is Job|SideProject|Company|Essay|Article|Painting|Other. Renaming is not offered: the name is how create_task and bulk_import_tasks resolve a project, so a rename would strand callers." | |
| 176 | 176 | } | |
| 177 | 177 | fn kind(&self) -> ToolKind { | |
| 178 | 178 | ToolKind::Write(caps::project_update()) |
| @@ -42,7 +42,7 @@ impl Tool for ListTasks { | |||
| 42 | 42 | "list_tasks" | |
| 43 | 43 | } | |
| 44 | 44 | fn description(&self) -> &str { | |
| 45 | - | "List tasks as compact rows. Optional filters: `project` (name), `status` (Pending|Started|Completed), `tag`. Paged: `limit` (default 50, max 200) and `offset`. Long descriptions are clipped and the row marked `truncated` — use `get_task` for the full text. The reply carries `total` (rows matching the filters) and, when more remain, `next_offset`." | |
| 45 | + | "List tasks as compact rows. Optional filters: `project` (name), `status` (Pending|Started|Completed), `tag`. Paged: `limit` (default 50, max 200) and `offset`. Long descriptions are clipped and the row marked `truncated`; use `get_task` for the full text. The reply carries `total` (rows matching the filters) and, when more remain, `next_offset`." | |
| 46 | 46 | } | |
| 47 | 47 | fn kind(&self) -> ToolKind { | |
| 48 | 48 | ToolKind::Read |
| @@ -1066,9 +1066,7 @@ fn parse_db_arg() -> Result<Option<PathBuf>> { | |||
| 1066 | 1066 | db = Some(PathBuf::from(v)); | |
| 1067 | 1067 | } | |
| 1068 | 1068 | "-h" | "--help" => { | |
| 1069 | - | println!( | |
| 1070 | - | "got — read-first terminal board for GoingsOn\n\nUsage: got [--db <path>]" | |
| 1071 | - | ); | |
| 1069 | + | println!("got: read-first terminal board for GoingsOn\n\nUsage: got [--db <path>]"); | |
| 1072 | 1070 | std::process::exit(0); | |
| 1073 | 1071 | } | |
| 1074 | 1072 | other => anyhow::bail!("unknown argument: {other}"), |
| @@ -1,4 +1,4 @@ | |||
| 1 | - | /* ============================================================================ | |
| 1 | + | /* | |
| 2 | 2 | GoingsOn: Neobrute Theme | |
| 3 | 3 | ||
| 4 | 4 | Build new UI by COMPOSING the existing vocabulary, in this order of preference: | |
| @@ -13,7 +13,7 @@ | |||
| 13 | 13 | 4. Extend a primitive with a modifier (.btn-primary, .btn-sm, | |
| 14 | 14 | .form-select--compact, | |
| 15 | 15 | .subtask-item--linked) | |
| 16 | - | 5. ONLY THEN consider a new class — and add it to the right | |
| 16 | + | 5. ONLY THEN consider a new class | and add it to the right | |
| 17 | 17 | BAND below | |
| 18 | 18 | ||
| 19 | 19 | A new class is a smell, not a goal. Before writing one: | |
| @@ -26,14 +26,13 @@ | |||
| 26 | 26 | others size to content) belongs in a NAMED LAYOUT with an explicit | |
| 27 | 27 | `grid-template-columns`, not in `.row-flex` + utilities. The utility form | |
| 28 | 28 | encodes intent in browser flex-shrink math rather than in CSS that reads | |
| 29 | - | as intent — especially fragile when the row contains a native `<select>`, | |
| 29 | + | as intent, especially fragile when the row contains a native `<select>`, | |
| 30 | 30 | whose intrinsic-min-content sizing fights `flex: 1`. Smell test: if you | |
| 31 | 31 | find yourself reaching for `.form-select--compact`, `style="width: auto;"`, | |
| 32 | 32 | or `min-width: 0 !important` to make a row lay out right, the layout | |
| 33 | 33 | itself wants a name. See `.email-filter-row`, `.contacts-filter-row`, and | |
| 34 | 34 | `.work-hours-row` (all 2026-05-24) for the canonical form. | |
| 35 | 35 | ||
| 36 | - | ---------------------------------------------------------------------------- | |
| 37 | 36 | FILE STRUCTURE: search by BAND name (uppercase anchors) to navigate. | |
| 38 | 37 | Per-section titles use `/* N. Title */` headers; numbered for stability, | |
| 39 | 38 | not because order matters. Bands group related sections; cascade order is | |
| @@ -42,8 +41,8 @@ | |||
| 42 | 41 | BAND: FOUNDATIONS | |
| 43 | 42 | 1. Font Face | |
| 44 | 43 | 2. CSS Reset & Base | |
| 45 | - | 3. Design System Variables (Neobrute) — tokens (:root) | |
| 46 | - | 4. Utility Classes — shadow/hover/border, | |
| 44 | + | 3. Design System Variables (Neobrute) | tokens (:root) | |
| 45 | + | 4. Utility Classes | shadow/hover/border, | |
| 47 | 46 | layout, form sizing, | |
| 48 | 47 | settings, subtask | |
| 49 | 48 | ||
| @@ -54,7 +53,7 @@ | |||
| 54 | 53 | 8. Main Content & Page Header | |
| 55 | 54 | ||
| 56 | 55 | BAND: COMPONENT PRIMITIVES & WIDGETS | |
| 57 | - | 9. Buttons — .btn + variants/sizes | |
| 56 | + | 9. Buttons | .btn + variants/sizes | |
| 58 | 57 | 10. Quick Add Input | |
| 59 | 58 | 11. Cards | |
| 60 | 59 | 12. Tags & Badges | |
| @@ -65,7 +64,7 @@ | |||
| 65 | 64 | 17. Email List | |
| 66 | 65 | 18. Toast Notifications | |
| 67 | 66 | 19. Modals | |
| 68 | - | 20. Form Elements — .form-input, | |
| 67 | + | 20. Form Elements | .form-input, | |
| 69 | 68 | .form-select, + | |
| 70 | 69 | JS-rendered layout | |
| 71 | 70 | utilities tail | |
| @@ -132,8 +131,7 @@ | |||
| 132 | 131 | BAND: SCOPED STYLESHEETS | |
| 133 | 132 | Compose window (compose.html standalone Tauri window) | |
| 134 | 133 | ||
| 135 | - | ---------------------------------------------------------------------------- | |
| 136 | - | EXISTING VOCABULARY — grep before inventing a new class. | |
| 134 | + | EXISTING VOCABULARY: grep before inventing a new class. | |
| 137 | 135 | ||
| 138 | 136 | UTILITIES | |
| 139 | 137 | Layout .flex-1, .row, .row-flex + .row-flex-{2,3,4}, | |
| @@ -169,34 +167,32 @@ | |||
| 169 | 167 | Intents: .btn-primary / -secondary / -danger | |
| 170 | 168 | Shapes: .form-select--compact / --ghost, .form-input--ghost | |
| 171 | 169 | ||
| 172 | - | ---------------------------------------------------------------------------- | |
| 173 | 170 | UI MODE STRATEGY (do not casually merge) | |
| 174 | 171 | ||
| 175 | 172 | UI mode is set ONCE at boot by index.html's inline script and exposed | |
| 176 | 173 | as `<html class="ui-mode-desktop">` or `<html class="ui-mode-mobile">`. | |
| 177 | - | Viewport width does NOT switch UI mode — desktop binaries stay desktop | |
| 174 | + | Viewport width does NOT switch UI mode; desktop binaries stay desktop | |
| 178 | 175 | even when the window is narrowed. | |
| 179 | 176 | ||
| 180 | - | §25 Mobile (base pass) — early `.ui-mode-mobile` rules; applies before | |
| 177 | + | §25 Mobile (base pass) | early `.ui-mode-mobile` rules; applies before | |
| 181 | 178 | feature CSS so feature rules can override. | |
| 182 | - | §59 Mobile (late pass) — late `.ui-mode-mobile` rules; runs AFTER | |
| 179 | + | §59 Mobile (late pass) | late `.ui-mode-mobile` rules; runs AFTER | |
| 183 | 180 | feature CSS so it wins conflicts. Add new | |
| 184 | 181 | mobile overrides here unless you need them | |
| 185 | 182 | early. | |
| 186 | - | §60 Touch hover disable — `@media (hover: none)`. INPUT-CAPABILITY axis, | |
| 183 | + | §60 Touch hover disable | `@media (hover: none)`. INPUT-CAPABILITY axis, | |
| 187 | 184 | not UI mode. Reserved for hover suppression | |
| 188 | 185 | only. Never put visibility or layout rules | |
| 189 | - | here — they belong under `.ui-mode-mobile`. | |
| 186 | + | here; they belong under `.ui-mode-mobile`. | |
| 190 | 187 | ||
| 191 | 188 | Intra-mode responsive: `@media (max-width: ...)` queries are allowed | |
| 192 | 189 | INSIDE a UI mode (e.g. `.ui-mode-desktop` rules at narrow desktop | |
| 193 | 190 | widths). Do not use a media query to switch BETWEEN UI modes. | |
| 194 | 191 | ||
| 195 | 192 | PRINT STRATEGY | |
| 196 | - | §48 Global print rules — early. | |
| 197 | - | §52 Weekly Review print — placed after §49/§50 so V2 grid resets win. | |
| 193 | + | §48 Global print rules | early. | |
| 194 | + | §52 Weekly Review print | placed after §49/§50 so V2 grid resets win. | |
| 198 | 195 | ||
| 199 | - | ---------------------------------------------------------------------------- | |
| 200 | 196 | WEEKLY REVIEW V1 + V2 COEXISTENCE | |
| 201 | 197 | ||
| 202 | 198 | §49 (V1: .weekly-review-content, .weekly-review-header, ...) and | |
| @@ -204,7 +200,7 @@ | |||
| 204 | 200 | and mobile.js still render V1 classes; the V2 grid is used by newer | |
| 205 | 201 | review surfaces. Do not delete §49 until those JS render paths migrate. | |
| 206 | 202 | ||
| 207 | - | ============================================================================ */ | |
| 203 | + | */ | |
| 208 | 204 | ||
| 209 | 205 | /* 1. Font Face */ | |
| 210 | 206 | @font-face { | |
| @@ -229,14 +225,14 @@ | |||
| 229 | 225 | positioning math in day-planning-render.js / day-planning.js. */ | |
| 230 | 226 | --timeline-slot-h: 12px; | |
| 231 | 227 | ||
| 232 | - | /* === NEOBRUTE DESIGN SYSTEM === */ | |
| 228 | + | /* --- NEOBRUTE DESIGN SYSTEM */ | |
| 233 | 229 | /* Clean neobrutalism: bold borders, flat by default, offset shadows on actions + floats */ | |
| 234 | 230 | ||
| 235 | - | /* === INTENT LAYER (themeable) === | |
| 231 | + | /* --- INTENT LAYER (themeable) | |
| 236 | 232 | Resolved intent tokens, applied at runtime by js/themes.js (setProperty | |
| 237 | 233 | on :root). Defaults below are the "goingson" titular theme for first | |
| 238 | - | paint; theme-common resolves every theme — including the derived states | |
| 239 | - | (action-hover/active, selection, row-stripe, status surfaces) — so this | |
| 234 | + | paint; theme-common resolves every theme, including the derived states | |
| 235 | + | (action-hover/active, selection, row-stripe, status surfaces), so this | |
| 240 | 236 | is one shared mapping across audiofiles / Balanced Breakfast / MNW. */ | |
| 241 | 237 | --surface-page: #E0E4FA; | |
| 242 | 238 | --surface-raised: #FFFFFF; | |
| @@ -264,8 +260,8 @@ | |||
| 264 | 260 | --category-six: #17A2B8; | |
| 265 | 261 | --overlay: rgba(1, 1, 8, 0.5); | |
| 266 | 262 | ||
| 267 | - | /* === APP-LOCAL (not theme intents) === | |
| 268 | - | GoingsOn's component CSS references the intent tokens above directly — | |
| 263 | + | /* --- APP-LOCAL (not theme intents) | |
| 264 | + | GoingsOn's component CSS references the intent tokens above directly; | |
| 269 | 265 | no brand aliases. Only the neobrute border width is app-local now; | |
| 270 | 266 | overlay/scrim is a theme intent (theme-common 0.7.0). */ | |
| 271 | 267 | --border-width: 2px; | |
| @@ -348,7 +344,7 @@ html { | |||
| 348 | 344 | } | |
| 349 | 345 | ||
| 350 | 346 | /* 4. Utility Classes (Shadow, Hover, Border) */ | |
| 351 | - | /* === LAYOUT UTILITIES === */ | |
| 347 | + | /* --- LAYOUT UTILITIES */ | |
| 352 | 348 | /* Compose: .row-flex + .row-flex-{2,3,4} for align-items:center rows. | |
| 353 | 349 | .row for raw flex with no align. .flex-1 to grow a child. */ | |
| 354 | 350 | .flex-1 { flex: 1; } | |
| @@ -359,15 +355,15 @@ html { | |||
| 359 | 355 | .mb-1 { margin-bottom: 1rem; } | |
| 360 | 356 | .mb-2 { margin-bottom: var(--space-2); } | |
| 361 | 357 | ||
| 362 | - | /* === FORM SIZING UTILITIES === */ | |
| 358 | + | /* --- FORM SIZING UTILITIES */ | |
| 363 | 359 | .form-select--compact { width: auto; min-width: 120px; } | |
| 364 | 360 | ||
| 365 | - | /* === SETTINGS UTILITIES === */ | |
| 361 | + | /* --- SETTINGS UTILITIES */ | |
| 366 | 362 | .settings-divider { margin-top: 1.5rem; padding-top: 1.5rem; border-top: 2px solid var(--border); } | |
| 367 | 363 | .settings-heading { margin-bottom: 1rem; font-family: var(--font-heading); } | |
| 368 | 364 | .settings-desc { font-size: 0.875rem; color: var(--content-secondary); margin-bottom: 1rem; } | |
| 369 | 365 | ||
| 370 | - | /* === SUBTASK UTILITIES === */ | |
| 366 | + | /* --- SUBTASK UTILITIES */ | |
| 371 | 367 | /* .subtask-item is the base row; --linked is a modifier for subtasks that | |
| 372 | 368 | reference another task (raised bg + accent border). Use both classes | |
| 373 | 369 | together: <div class="subtask-item subtask-item--linked">. */ | |
| @@ -403,7 +399,7 @@ body { | |||
| 403 | 399 | /* Interactive chrome is not text. Buttons, tabs, nav, badges, and labels | |
| 404 | 400 | shouldn't show a text selection on press-and-hold (notably the mobile | |
| 405 | 401 | bottom tab bar, where a long-press otherwise highlights the label and | |
| 406 | - | you have to dodge it). Content text — inputs, textareas, contenteditable, | |
| 402 | + | you have to dodge it). Content text: inputs, textareas, contenteditable, | |
| 407 | 403 | and anything explicitly marked .selectable, stays selectable. */ | |
| 408 | 404 | .btn, | |
| 409 | 405 | .tab, | |
| @@ -1896,7 +1892,7 @@ textarea, | |||
| 1896 | 1892 | /* Pin a form's action row to the bottom of the modal's scroll area so | |
| 1897 | 1893 | primary/cancel buttons stay visible even on short viewports. Without | |
| 1898 | 1894 | this, on small windows or iOS with the keyboard up the button row sits | |
| 1899 | - | below the fold and looks cut off — there's no visible scrollbar on | |
| 1895 | + | below the fold and looks cut off; there's no visible scrollbar on | |
| 1900 | 1896 | macOS by default so users don't realize they can scroll to it. */ | |
| 1901 | 1897 | .modal-content > .form-actions:last-child, | |
| 1902 | 1898 | .modal-content form > .form-actions:last-child { | |
| @@ -2980,7 +2976,7 @@ textarea, | |||
| 2980 | 2976 | } | |
| 2981 | 2977 | ||
| 2982 | 2978 | /* Hide the List / Board toggle in mobile UI. Kanban drag-drop has no touch | |
| 2983 | - | fallback (Phase 6 #1) — showing the toggle would expose a broken feature. | |
| 2979 | + | fallback (Phase 6 #1): showing the toggle would expose a broken feature. | |
| 2984 | 2980 | Keyed off UI mode rather than `hover: none` so a desktop touch laptop | |
| 2985 | 2981 | with a working mouse still gets the toggle. Re-enable when touch-drag lands. */ | |
| 2986 | 2982 | .ui-mode-mobile #task-view-toggle { display: none; } | |
| @@ -5213,7 +5209,7 @@ kbd { | |||
| 5213 | 5209 | } | |
| 5214 | 5210 | ||
| 5215 | 5211 | /* Work-hours row in Settings: two small selects with a "to" label | |
| 5216 | - | between. Same lens as .email-filter-row — selects size to their | |
| 5212 | + | between. Same lens as .email-filter-row: selects size to their | |
| 5217 | 5213 | short content; the grid columns express the intent explicitly. */ | |
| 5218 | 5214 | .work-hours-row { | |
| 5219 | 5215 | display: grid; | |
| @@ -5230,7 +5226,7 @@ kbd { | |||
| 5230 | 5226 | flex-shrink: 0; | |
| 5231 | 5227 | } | |
| 5232 | 5228 | ||
| 5233 | - | /* 46. Saved Views Sidebar (desktop UI only — hidden on mobile) */ | |
| 5229 | + | /* 46. Saved Views Sidebar (desktop UI only, hidden on mobile) */ | |
| 5234 | 5230 | .ui-mode-desktop .saved-views-sidebar { | |
| 5235 | 5231 | width: 200px; | |
| 5236 | 5232 | flex-shrink: 0; | |
| @@ -7704,7 +7700,7 @@ html.ui-mode-mobile, .ui-mode-mobile body { | |||
| 7704 | 7700 | display: none; | |
| 7705 | 7701 | } | |
| 7706 | 7702 | ||
| 7707 | - | /* === Mobile toolbar grids === | |
| 7703 | + | /* --- mobile toolbar grids | |
| 7708 | 7704 | Toolbar rows become equal-column grids on mobile so children share | |
| 7709 | 7705 | the row predictably (no orphans, no ragged wrapping). Search inputs | |
| 7710 | 7706 | always span the full grid row. */ | |
| @@ -7996,7 +7992,7 @@ html.ui-mode-mobile, .ui-mode-mobile body { | |||
| 7996 | 7992 | } | |
| 7997 | 7993 | ||
| 7998 | 7994 | /* Mobile uses different modal animations (slide up/down from bottom rather | |
| 7999 | - | than fade-and-scale). @keyframes can't be scoped — defined globally under | |
| 7995 | + | than fade-and-scale). @keyframes can't be scoped; defined globally under | |
| 8000 | 7996 | distinct names and applied via animation-name override below. */ | |
| 8001 | 7997 | @keyframes modalSlideInMobile { | |
| 8002 | 7998 | from { transform: translateY(100%); } |
| @@ -220,7 +220,7 @@ if (window.__TAURI__) { | |||
| 220 | 220 | ||
| 221 | 221 | // Cloud sync: subscription required (402 from server) | |
| 222 | 222 | listen('sync:subscription-required', () => { | |
| 223 | - | GoingsOn.ui.showToast('Cloud sync paused — subscription required', 'error', { | |
| 223 | + | GoingsOn.ui.showToast('Cloud sync paused: subscription required', 'error', { | |
| 224 | 224 | action: { label: 'Subscribe', fn: () => GoingsOn.settings.openCloudSync() }, | |
| 225 | 225 | duration: 10000, | |
| 226 | 226 | }); |
| @@ -123,7 +123,7 @@ | |||
| 123 | 123 | return { | |
| 124 | 124 | ok: false, | |
| 125 | 125 | field: 'attachments', | |
| 126 | - | message: `Attachments exceed ${formatBytes(ATTACHMENT_HARD_CAP_BYTES)} — remove some files or use a file-share link.`, | |
| 126 | + | message: `Attachments exceed ${formatBytes(ATTACHMENT_HARD_CAP_BYTES)}. Remove some files or use a file-share link.`, | |
| 127 | 127 | }; | |
| 128 | 128 | } | |
| 129 | 129 | return { ok: true }; | |
| @@ -415,9 +415,9 @@ | |||
| 415 | 415 | : cap.warn ? 'compose-attachment-total over-warn' | |
| 416 | 416 | : 'compose-attachment-total'; | |
| 417 | 417 | const warn = cap.over | |
| 418 | - | ? '<span class="compose-attachment-warn">— most mail servers will reject this. Remove some files or use a file-share link.</span>' | |
| 418 | + | ? '<span class="compose-attachment-warn">. Most mail servers will reject this. Remove some files or use a file-share link.</span>' | |
| 419 | 419 | : cap.warn | |
| 420 | - | ? '<span class="compose-attachment-warn">— large attachment; your mail server may reject this.</span>' | |
| 420 | + | ? '<span class="compose-attachment-warn">. Large attachment; your mail server may reject this.</span>' | |
| 421 | 421 | : ''; | |
| 422 | 422 | const totalLine = `<div class="${cls}"><span>Total: ${formatBytes(cap.totalBytes)} / ${formatBytes(ATTACHMENT_HARD_CAP_BYTES)}</span>${warn}</div>`; | |
| 423 | 423 | attachmentsBar.classList.remove('hidden'); |
| @@ -103,7 +103,7 @@ async function sendEmail() { | |||
| 103 | 103 | input, | |
| 104 | 104 | delaySeconds: 5, | |
| 105 | 105 | }); | |
| 106 | - | setStatus(isReply ? 'Reply queued — undo in main window' : 'Email queued — undo in main window', 'success'); | |
| 106 | + | setStatus(isReply ? 'Reply queued; undo in main window' : 'Email queued; undo in main window', 'success'); | |
| 107 | 107 | setTimeout(() => { | |
| 108 | 108 | window.__TAURI__.webviewWindow.getCurrentWebviewWindow().close(); | |
| 109 | 109 | }, 250); |
| @@ -125,13 +125,13 @@ | |||
| 125 | 125 | // App-password notes shared across providers that require them. The note is shown | |
| 126 | 126 | // beneath the email field when the domain is detected; the link opens in the OS | |
| 127 | 127 | // browser via the regular anchor (Tauri allows external http(s) targets). | |
| 128 | - | const NOTE_APPLE = 'Apple requires an <strong>app-specific password</strong> for third-party mail apps — your normal iCloud password will not work. Generate one at <a href="https://appleid.apple.com/account/manage" target="_blank" rel="noopener">appleid.apple.com</a> → Sign-In and Security → App-Specific Passwords, then paste it in the Password field below.'; | |
| 128 | + | const NOTE_APPLE = 'Apple requires an <strong>app-specific password</strong> for third-party mail apps. Your normal iCloud password will not work. Generate one at <a href="https://appleid.apple.com/account/manage" target="_blank" rel="noopener">appleid.apple.com</a> → Sign-In and Security → App-Specific Passwords, then paste it in the Password field below.'; | |
| 129 | 129 | const NOTE_FASTMAIL = 'Fastmail requires an <strong>app password</strong> (not your normal password). Create one at <a href="https://app.fastmail.com/settings/security/integrations" target="_blank" rel="noopener">fastmail.com → Settings → Privacy & Security → Integrations</a>, then paste it in the Password field below.'; | |
| 130 | 130 | const NOTE_GOOGLE = 'Gmail requires an <strong>app password</strong> (your normal Google password will not work). With 2-Step Verification enabled, create one at <a href="https://myaccount.google.com/apppasswords" target="_blank" rel="noopener">myaccount.google.com/apppasswords</a> and paste it below.'; | |
| 131 | 131 | const NOTE_YAHOO = 'Yahoo requires an <strong>app password</strong>. Create one at <a href="https://login.yahoo.com/account/security" target="_blank" rel="noopener">Yahoo Account Security → Generate app password</a> and paste it below.'; | |
| 132 | 132 | const NOTE_AOL = 'AOL requires an <strong>app password</strong>. Create one at <a href="https://login.aol.com/account/security" target="_blank" rel="noopener">AOL Account Security → Generate app password</a> and paste it below.'; | |
| 133 | 133 | const NOTE_OUTLOOK = 'If your Microsoft account has 2-Step Verification on, generate an <strong>app password</strong> at <a href="https://account.microsoft.com/security" target="_blank" rel="noopener">account.microsoft.com/security</a> → Advanced security options. Otherwise your normal password works.'; | |
| 134 | - | const NOTE_PROTON = 'Proton Mail does not allow direct IMAP/SMTP — you must run <a href="https://proton.me/mail/bridge" target="_blank" rel="noopener">Proton Bridge</a> locally and use the bridge-generated credentials. Bridge is a paid plan feature.'; | |
| 134 | + | const NOTE_PROTON = 'Proton Mail does not allow direct IMAP/SMTP. You must run <a href="https://proton.me/mail/bridge" target="_blank" rel="noopener">Proton Bridge</a> locally and use the bridge-generated credentials. Bridge is a paid plan feature.'; | |
| 135 | 135 | ||
| 136 | 136 | const PROVIDER_SETTINGS = { | |
| 137 | 137 | 'gmail.com': { imap: 'imap.gmail.com', imapPort: 993, smtp: 'smtp.gmail.com', smtpPort: 587, archive: '[Gmail]/All Mail', name: 'Gmail', note: NOTE_GOOGLE }, | |
| @@ -173,7 +173,7 @@ | |||
| 173 | 173 | ||
| 174 | 174 | if (!settings) { | |
| 175 | 175 | if (statusEl && domain.includes('.')) { | |
| 176 | - | statusEl.textContent = 'Unknown provider — fill in server details under Advanced settings'; | |
| 176 | + | statusEl.textContent = 'Unknown provider: fill in server details under Advanced settings'; | |
| 177 | 177 | statusEl.style.color = 'var(--content-secondary)'; | |
| 178 | 178 | } | |
| 179 | 179 | if (noteEl) { noteEl.classList.add('hidden'); noteEl.innerHTML = ''; } | |
| @@ -198,7 +198,7 @@ | |||
| 198 | 198 | if (archiveEl && (archiveEl.value === 'Archive' || !archiveEl.value)) archiveEl.value = settings.archive; | |
| 199 | 199 | ||
| 200 | 200 | if (statusEl) { | |
| 201 | - | statusEl.textContent = `Detected ${settings.name || domain} — server settings auto-filled`; | |
| 201 | + | statusEl.textContent = `Detected ${settings.name || domain}: server settings auto-filled`; | |
| 202 | 202 | statusEl.style.color = 'var(--success)'; | |
| 203 | 203 | } | |
| 204 | 204 | if (noteEl) { | |
| @@ -355,7 +355,7 @@ | |||
| 355 | 355 | const hasOAuth = oauthProviders.length > 0; | |
| 356 | 356 | ||
| 357 | 357 | const imapIntro = ` | |
| 358 | - | <p class="settings-desc">Enter your account details below. Most providers (Gmail, Fastmail, iCloud, Yahoo, Outlook with 2-step verification) require an <strong>app password</strong> instead of your normal password — type your email address and GoingsOn shows the exact link to create one.</p> | |
| 358 | + | <p class="settings-desc">Enter your account details below. Most providers (Gmail, Fastmail, iCloud, Yahoo, Outlook with 2-step verification) require an <strong>app password</strong> instead of your normal password. Type your email address and GoingsOn shows the exact link to create one.</p> | |
| 359 | 359 | `; | |
| 360 | 360 | ||
| 361 | 361 | const oauthButtons = hasOAuth |
| @@ -390,7 +390,7 @@ function queueSend({ input, delaySeconds = 5 }) { | |||
| 390 | 390 | GoingsOn.api.window.openCompose(prefill).catch(() => openComposeModal(prefill)); | |
| 391 | 391 | } | |
| 392 | 392 | if (hadAttachments) { | |
| 393 | - | GoingsOn.ui.showToast('Re-attach your files — attachments cleared on undo.', 'info', { duration: 8000 }); | |
| 393 | + | GoingsOn.ui.showToast('Re-attach your files; attachments cleared on undo.', 'info', { duration: 8000 }); | |
| 394 | 394 | } | |
| 395 | 395 | }, | |
| 396 | 396 | }); |
| @@ -1016,7 +1016,7 @@ | |||
| 1016 | 1016 | } | |
| 1017 | 1017 | const noun = total === 1 ? 'thread' : 'threads'; | |
| 1018 | 1018 | if (shown < total) { | |
| 1019 | - | el.textContent = `${shown} of ${total} ${noun} — narrow with filters`; | |
| 1019 | + | el.textContent = `${shown} of ${total} ${noun}, narrow with filters`; | |
| 1020 | 1020 | el.classList.add('filter-count--capped'); | |
| 1021 | 1021 | } else { | |
| 1022 | 1022 | el.textContent = `${total} ${noun}`; |
| @@ -130,7 +130,7 @@ | |||
| 130 | 130 | type: 'checkbox', | |
| 131 | 131 | label: 'All day', | |
| 132 | 132 | value: isAllDay, | |
| 133 | - | hint: 'Removes the time component — the event spans the whole day.', | |
| 133 | + | hint: 'Removes the time component. The event spans the whole day.', | |
| 134 | 134 | }, | |
| 135 | 135 | { | |
| 136 | 136 | name: 'title', | |
| @@ -642,7 +642,7 @@ | |||
| 642 | 642 | const past = action === 'delete' ? 'deleted' : 'edited'; | |
| 643 | 643 | return GoingsOn.ui.showConfirmDialog( | |
| 644 | 644 | `${verb} recurring event`, | |
| 645 | - | `This event repeats (${pattern}). The whole series will be ${past} — per-occurrence overrides aren't supported yet.`, | |
| 645 | + | `This event repeats (${pattern}). The whole series will be ${past}. Per-occurrence overrides aren't supported yet.`, | |
| 646 | 646 | { confirmText: `${verb} entire series`, cancelText: 'Cancel', danger: action === 'delete' } | |
| 647 | 647 | ); | |
| 648 | 648 | } |
| @@ -417,18 +417,18 @@ | |||
| 417 | 417 | if (!status.authenticated) { | |
| 418 | 418 | dot.classList.add('warn'); | |
| 419 | 419 | labelText = 'Connect sync'; | |
| 420 | - | titleText = 'Cloud Sync — sign in to start syncing'; | |
| 420 | + | titleText = 'Cloud Sync: sign in to start syncing'; | |
| 421 | 421 | } else if (!status.encryptionReady) { | |
| 422 | 422 | dot.classList.add('warn'); | |
| 423 | 423 | labelText = 'Set up encryption'; | |
| 424 | - | titleText = 'Cloud Sync — encryption setup needed'; | |
| 424 | + | titleText = 'Cloud Sync: encryption setup needed'; | |
| 425 | 425 | } else { | |
| 426 | 426 | dot.classList.add('connected'); | |
| 427 | 427 | const ago = _formatSyncAgo(status.lastSyncAt); | |
| 428 | 428 | labelText = ago ? `Synced ${ago}` : 'Sync ready'; | |
| 429 | 429 | titleText = ago | |
| 430 | - | ? `Cloud Sync — last sync ${ago}${status.pendingChanges ? ` · ${status.pendingChanges} pending` : ''}` | |
| 431 | - | : 'Cloud Sync — ready'; | |
| 430 | + | ? `Cloud Sync: last sync ${ago}${status.pendingChanges ? ` · ${status.pendingChanges} pending` : ''}` | |
| 431 | + | : 'Cloud Sync: ready'; | |
| 432 | 432 | } | |
| 433 | 433 | if (label) label.textContent = labelText; | |
| 434 | 434 | indicator.setAttribute('title', titleText); |
| @@ -392,7 +392,7 @@ | |||
| 392 | 392 | name: 'milestone_id', | |
| 393 | 393 | type: 'select', | |
| 394 | 394 | label: 'Milestone', | |
| 395 | - | hint: 'Group tasks into project phases — milestones are managed per project', | |
| 395 | + | hint: 'Group tasks into project phases; milestones are managed per project', | |
| 396 | 396 | options: [ | |
| 397 | 397 | { value: '', label: 'No Milestone' }, | |
| 398 | 398 | ], |
| @@ -207,7 +207,7 @@ | |||
| 207 | 207 | const date = new Date(inst.completedAt); | |
| 208 | 208 | const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); | |
| 209 | 209 | const time = inst.actualMinutes > 0 ? ` (${inst.actualMinutes}m tracked)` : ''; | |
| 210 | - | html += `<div class="task-overview-completion-item">${esc(dateStr)} — completed${esc(time)}</div>`; | |
| 210 | + | html += `<div class="task-overview-completion-item">${esc(dateStr)}: completed${esc(time)}</div>`; | |
| 211 | 211 | } | |
| 212 | 212 | html += '</div>'; | |
| 213 | 213 | } | |
| @@ -428,7 +428,7 @@ | |||
| 428 | 428 | const dateStr = start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); | |
| 429 | 429 | const timeStr = start.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); | |
| 430 | 430 | const duration = s.durationMinutes != null ? `${s.durationMinutes}m` : 'active'; | |
| 431 | - | html += `<div class="task-overview-session">${esc(dateStr)} ${esc(timeStr)} — ${esc(duration)}</div>`; | |
| 431 | + | html += `<div class="task-overview-session">${esc(dateStr)} ${esc(timeStr)}, ${esc(duration)}</div>`; | |
| 432 | 432 | } | |
| 433 | 433 | html += '</div>'; | |
| 434 | 434 | } |
| @@ -42,7 +42,7 @@ const ERROR_CODE_LABELS = { | |||
| 42 | 42 | const ERROR_CODE_HINTS = { | |
| 43 | 43 | VALIDATION_ERROR: 'Check that all required fields are filled in correctly.', | |
| 44 | 44 | AUTH_ERROR: 'Check your credentials or reconnect your account in Settings.', | |
| 45 | - | PARSE_ERROR: 'Try a simpler format — e.g. "tomorrow 3pm" or "2026-12-25".', | |
| 45 | + | PARSE_ERROR: 'Try a simpler format, e.g. "tomorrow 3pm" or "2026-12-25".', | |
| 46 | 46 | EXTERNAL_SERVICE_ERROR: 'The remote service may be temporarily unavailable. Try again in a moment.', | |
| 47 | 47 | CONFLICT: 'This item was modified elsewhere. Reload and try again.', | |
| 48 | 48 | }; |
| @@ -268,7 +268,7 @@ pub async fn open_attachment( | |||
| 268 | 268 | let blob_path = state.data_dir.join("blobs").join(&attachment.blob_hash); | |
| 269 | 269 | if !blob_path.exists() { | |
| 270 | 270 | return Err(ApiError::bad_request( | |
| 271 | - | "Blob not available locally — sync required", | |
| 271 | + | "Blob not available locally; sync required", | |
| 272 | 272 | )); | |
| 273 | 273 | } | |
| 274 | 274 | ||
| @@ -332,7 +332,7 @@ pub async fn save_attachment( | |||
| 332 | 332 | let blob_path = state.data_dir.join("blobs").join(&attachment.blob_hash); | |
| 333 | 333 | if !blob_path.exists() { | |
| 334 | 334 | return Err(ApiError::bad_request( | |
| 335 | - | "Blob not available locally — sync required", | |
| 335 | + | "Blob not available locally; sync required", | |
| 336 | 336 | )); | |
| 337 | 337 | } | |
| 338 | 338 | ||
| @@ -390,7 +390,7 @@ pub async fn convert_email_attachments( | |||
| 390 | 390 | tracing::warn!( | |
| 391 | 391 | blob_hash = %meta.blob_hash, | |
| 392 | 392 | filename = %meta.filename, | |
| 393 | - | "Attachment blob missing — skipping" | |
| 393 | + | "Attachment blob missing; skipping" | |
| 394 | 394 | ); | |
| 395 | 395 | continue; | |
| 396 | 396 | } | |
| @@ -453,7 +453,7 @@ pub async fn open_email_blob( | |||
| 453 | 453 | let blob_path = state.data_dir.join("blobs").join(&blob_hash); | |
| 454 | 454 | if !blob_path.exists() { | |
| 455 | 455 | return Err(ApiError::bad_request( | |
| 456 | - | "Attachment not available locally — sync required", | |
| 456 | + | "Attachment not available locally; sync required", | |
| 457 | 457 | )); | |
| 458 | 458 | } | |
| 459 | 459 | ||
| @@ -504,7 +504,7 @@ pub async fn save_email_blob( | |||
| 504 | 504 | let blob_path = state.data_dir.join("blobs").join(&blob_hash); | |
| 505 | 505 | if !blob_path.exists() { | |
| 506 | 506 | return Err(ApiError::bad_request( | |
| 507 | - | "Attachment not available locally — sync required", | |
| 507 | + | "Attachment not available locally; sync required", | |
| 508 | 508 | )); | |
| 509 | 509 | } | |
| 510 | 510 |