Skip to main content

max / goingson

GO-30-CSP W0+W1: delegated event dispatch + convert index/compose Root cause of dead clicks on Linux: Tauri v2 injects a nonce into the CSP script-src, which makes 'unsafe-inline' ignored, so inline on* handlers are refused by WebKitGTK (macOS WKWebView is lenient). Fix is to migrate off inline handlers to delegation. - js/dispatch.js: one delegated listener per event type (click/change/input/ submit/keydown/contextmenu/mousedown) reading data-act + data-a1..N args, calling fn(...args, {event,el}); innermost [data-act] wins via closest(). - index.html (81) + compose.html (4) converted to data-* attributes. - Temp scaffold: dangerousDisableAssetCspModification ["script-src"] keeps the app usable during the migration; removed at finalize when 'unsafe-inline' is dropped from script-src. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-13 20:38 UTC
Commit: 84076c5243e9e8206a547731c1725693bacd4af6
Parent: 22921e5
4 files changed, +179 insertions, -86 deletions
@@ -8,19 +8,19 @@
8 8 </head>
9 9 <body class="compose-window">
10 10 <div class="compose-toolbar">
11 - <button class="btn btn-primary" id="send-btn" onclick="sendEmail()">Send</button>
11 + <button class="btn btn-primary" id="send-btn">Send</button>
12 12 <span id="reply-indicator" class="reply-indicator"></span>
13 - <button class="btn btn-secondary" onclick="saveDraft()">Save Draft</button>
13 + <button class="btn btn-secondary" id="save-draft-btn">Save Draft</button>
14 14 <button class="btn btn-secondary" id="attach-btn">Attach</button>
15 15 <div class="toolbar-spacer"></div>
16 - <button class="btn btn-secondary" onclick="discardAndClose()">Discard</button>
16 + <button class="btn btn-secondary" id="discard-btn">Discard</button>
17 17 </div>
18 18
19 19 <!-- Fields rendered at DOMContentLoaded by composeForm.buildFieldsHtml.
20 20 Attachments bar stays outside the form (window-only chrome ordering);
21 21 the modal opts into the in-template attachments-bar instead.
22 22 toggle-cc binds via JS, not an inline onclick. -->
23 - <form class="compose-form" id="compose-form" onsubmit="event.preventDefault(); return false;"></form>
23 + <form class="compose-form" id="compose-form"></form>
24 24
25 25 <div class="compose-attachments hidden" id="attachments-bar"></div>
26 26 <div class="status-bar" id="status-bar">Ready</div>
@@ -207,6 +207,13 @@
207 207 showAttachments: false, // window keeps attachments-bar outside the form
208 208 });
209 209
210 + // Toolbar + form wiring (was inline on* attributes; moved to
211 + // addEventListener so the CSP nonce can drop 'unsafe-inline').
212 + document.getElementById('send-btn').addEventListener('click', sendEmail);
213 + document.getElementById('save-draft-btn').addEventListener('click', saveDraft);
214 + document.getElementById('discard-btn').addEventListener('click', discardAndClose);
215 + form.addEventListener('submit', (e) => e.preventDefault());
216 +
210 217 await initTauri();
211 218 await loadAccounts();
212 219 await loadContactEmails();
@@ -78,12 +78,12 @@
78 78 </a>
79 79 </nav>
80 80 <div class="header-actions">
81 - <button class="sync-indicator row-flex row-flex-2 hidden" id="sync-indicator" onclick="GoingsOn.settings.openCloudSync()" title="Cloud Sync" aria-label="Cloud sync status">
81 + <button class="sync-indicator row-flex row-flex-2 hidden" id="sync-indicator" data-act="settings.openCloudSync" title="Cloud Sync" aria-label="Cloud sync status">
82 82 <span class="sync-dot" id="sync-dot"></span>
83 83 <span class="sync-label" id="sync-label"></span>
84 84 </button>
85 - <button class="btn" onclick="GoingsOn.settings.open()" title="Settings" aria-label="Open settings">Settings</button>
86 - <button class="btn shortcut-hint-btn" onclick="GoingsOn.keyboard.showShortcuts()" title="Keyboard shortcuts (?)" aria-label="Show keyboard shortcuts">?</button>
85 + <button class="btn" data-act="settings.open" title="Settings" aria-label="Open settings">Settings</button>
86 + <button class="btn shortcut-hint-btn" data-act="keyboard.showShortcuts" title="Keyboard shortcuts (?)" aria-label="Show keyboard shortcuts">?</button>
87 87 </div>
88 88 </header>
89 89
@@ -102,25 +102,25 @@
102 102 <h2 class="page-title">Tasks</h2>
103 103 <div class="row-flex row-flex-4">
104 104 <div class="view-toggle" id="task-view-toggle">
105 - <button class="view-toggle-btn active" data-mode="list" onclick="GoingsOn.tasks.setViewMode('list')">List</button>
106 - <button class="view-toggle-btn" data-mode="board" onclick="GoingsOn.tasks.setViewMode('board')">Board</button>
105 + <button class="view-toggle-btn active" data-mode="list" data-act="tasks.setViewMode" data-a1="list">List</button>
106 + <button class="view-toggle-btn" data-mode="board" data-act="tasks.setViewMode" data-a1="board">Board</button>
107 107 </div>
108 - <button class="btn btn-secondary mobile-hide" onclick="GoingsOn.keyboard.openQuickAddModal()" title="Quick add (q)">Quick Add</button>
109 - <button class="btn btn-primary" onclick="GoingsOn.tasks.openNew()" title="New task (n)">+ New Task <kbd class="kbd-hint">n</kbd></button>
108 + <button class="btn btn-secondary mobile-hide" data-act="keyboard.openQuickAddModal" title="Quick add (q)">Quick Add</button>
109 + <button class="btn btn-primary" data-act="tasks.openNew" title="New task (n)">+ New Task <kbd class="kbd-hint">n</kbd></button>
110 110 </div>
111 111 </div>
112 112 <div id="task-bulk-actions" class="bulk-actions-bar hidden" role="toolbar" aria-label="Bulk task actions">
113 113 <span id="task-bulk-count" class="bulk-count">0 selected</span>
114 - <button class="btn btn-sm" onclick="GoingsOn.bulk.completeTasks()">Complete</button>
115 - <button class="btn btn-sm" onclick="GoingsOn.bulk.setProjectTasks()">Set Project</button>
116 - <button class="btn btn-sm" onclick="GoingsOn.bulk.setPriorityTasks()">Set Priority</button>
117 - <button class="btn btn-sm" onclick="GoingsOn.bulk.snoozeTasks()">Snooze</button>
118 - <button class="btn btn-sm btn-danger" onclick="GoingsOn.bulk.deleteTasks()">Delete</button>
119 - <button class="btn btn-sm bulk-select-all" onclick="GoingsOn.bulk.selectAllTasks()">Select All</button>
114 + <button class="btn btn-sm" data-act="bulk.completeTasks">Complete</button>
115 + <button class="btn btn-sm" data-act="bulk.setProjectTasks">Set Project</button>
116 + <button class="btn btn-sm" data-act="bulk.setPriorityTasks">Set Priority</button>
117 + <button class="btn btn-sm" data-act="bulk.snoozeTasks">Snooze</button>
118 + <button class="btn btn-sm btn-danger" data-act="bulk.deleteTasks">Delete</button>
119 + <button class="btn btn-sm bulk-select-all" data-act="bulk.selectAllTasks">Select All</button>
120 120 </div>
121 121 <div class="mobile-sort-bar" id="task-mobile-sort">
122 - <button class="mobile-filter-toggle" onclick="GoingsOn.tasks.toggleMobileFilters()">Filters</button>
123 - <select class="filter-select" onchange="GoingsOn.tasks.mobileSortChange(this.value)">
122 + <button class="mobile-filter-toggle" data-act="tasks.toggleMobileFilters">Filters</button>
123 + <select class="filter-select" data-change="tasks.mobileSortChange">
124 124 <option value="urgency">Sort: Urgency</option>
125 125 <option value="description">Sort: Name</option>
126 126 <option value="project">Sort: Project</option>
@@ -129,49 +129,49 @@
129 129 </select>
130 130 </div>
131 131 <div class="filter-bar" id="task-filter-bar" role="search" aria-label="Task filters">
132 - <select id="filter-status" class="filter-select" onchange="GoingsOn.tasks.applyFilters()">
132 + <select id="filter-status" class="filter-select" data-change="tasks.applyFilters">
133 133 <option value="">All Status</option>
134 134 <option value="pending" selected>Pending</option>
135 135 <option value="started">Started</option>
136 136 <option value="completed">Completed</option>
137 137 </select>
138 - <select id="filter-project" class="filter-select" onchange="GoingsOn.tasks.applyFilters()">
138 + <select id="filter-project" class="filter-select" data-change="tasks.applyFilters">
139 139 <option value="">All Projects</option>
140 140 </select>
141 - <select id="filter-priority" class="filter-select" onchange="GoingsOn.tasks.applyFilters()">
141 + <select id="filter-priority" class="filter-select" data-change="tasks.applyFilters">
142 142 <option value="">All Priority</option>
143 143 <option value="H">High</option>
144 144 <option value="M">Medium</option>
145 145 <option value="L">Low</option>
146 146 </select>
147 - <select id="filter-milestone" class="filter-select hidden" onchange="GoingsOn.tasks.applyFilters()">
147 + <select id="filter-milestone" class="filter-select hidden" data-change="tasks.applyFilters">
148 148 <option value="">All Milestones</option>
149 149 </select>
150 150 <label class="filter-checkbox" title="Include tasks hidden until a later date">
151 - <input type="checkbox" id="filter-snoozed" onchange="GoingsOn.tasks.applyFilters()">
151 + <input type="checkbox" id="filter-snoozed" data-change="tasks.applyFilters">
152 152 Show Snoozed
153 153 </label>
154 154 <label class="filter-checkbox" title="Tasks waiting on someone else to respond">
155 - <input type="checkbox" id="filter-waiting" onchange="GoingsOn.tasks.applyFilters()">
155 + <input type="checkbox" id="filter-waiting" data-change="tasks.applyFilters">
156 156 Waiting Only
157 157 </label>
158 158 <div class="filter-actions">
159 159 <span id="task-count" class="filter-count" aria-live="polite"></span>
160 - <button class="btn btn-link" onclick="GoingsOn.tasks.clearFilters()">Clear filters</button>
160 + <button class="btn btn-link" data-act="tasks.clearFilters">Clear filters</button>
161 161 </div>
162 162 </div>
163 163 <div class="task-table" id="task-table" role="grid" aria-label="Tasks list">
164 164 <div class="task-header-row" role="row">
165 - <div class="task-cell sortable" data-sort="description" onclick="GoingsOn.tasks.sort('description')" role="columnheader" tabindex="0">
165 + <div class="task-cell sortable" data-sort="description" data-act="tasks.sort" data-a1="description" role="columnheader" tabindex="0">
166 166 Description <span class="sort-arrow"></span>
167 167 </div>
168 - <div class="task-cell sortable" data-sort="project" onclick="GoingsOn.tasks.sort('project')" role="columnheader" tabindex="0">
168 + <div class="task-cell sortable" data-sort="project" data-act="tasks.sort" data-a1="project" role="columnheader" tabindex="0">
169 169 Project <span class="sort-arrow"></span>
170 170 </div>
171 - <div class="task-cell sortable" data-sort="priority" onclick="GoingsOn.tasks.sort('priority')" role="columnheader" tabindex="0" aria-label="Priority">
171 + <div class="task-cell sortable" data-sort="priority" data-act="tasks.sort" data-a1="priority" role="columnheader" tabindex="0" aria-label="Priority">
172 172 Priority <span class="sort-arrow"></span>
173 173 </div>
174 - <div class="task-cell sortable" data-sort="due" onclick="GoingsOn.tasks.sort('due')" role="columnheader" tabindex="0">
174 + <div class="task-cell sortable" data-sort="due" data-act="tasks.sort" data-a1="due" role="columnheader" tabindex="0">
175 175 Due <span class="sort-arrow"></span>
176 176 </div>
177 177 <div class="task-cell" role="columnheader">Recurs</div>
@@ -192,9 +192,9 @@
192 192 </div>
193 193 <!-- Pagination -->
194 194 <div id="task-pagination" class="pagination-controls hidden" role="navigation" aria-label="Task list pagination">
195 - <button class="btn btn-sm" id="task-prev-page" onclick="GoingsOn.tasks.goToPage('prev')" disabled aria-label="Previous page">&larr; Previous</button>
195 + <button class="btn btn-sm" id="task-prev-page" data-act="tasks.goToPage" data-a1="prev" disabled aria-label="Previous page">&larr; Previous</button>
196 196 <span id="task-page-info" class="pagination-info">Page 1 of 1</span>
197 - <button class="btn btn-sm" id="task-next-page" onclick="GoingsOn.tasks.goToPage('next')" disabled aria-label="Next page">Next &rarr;</button>
197 + <button class="btn btn-sm" id="task-next-page" data-act="tasks.goToPage" data-a1="next" disabled aria-label="Next page">Next &rarr;</button>
198 198 </div>
199 199 </div>
200 200
@@ -202,7 +202,7 @@
202 202 <div id="projects-view" class="subview hidden" role="tabpanel" aria-labelledby="projects-tab">
203 203 <div class="page-header">
204 204 <h2 class="page-title">Projects</h2>
205 - <button class="btn btn-primary" onclick="GoingsOn.projects.openNew()" title="New project (n)">+ New Project <kbd class="kbd-hint">n</kbd></button>
205 + <button class="btn btn-primary" data-act="projects.openNew" title="New project (n)">+ New Project <kbd class="kbd-hint">n</kbd></button>
206 206 </div>
207 207 <div class="cards-grid" id="projects-grid">
208 208 <div class="skeleton-shimmer" aria-label="Loading projects">
@@ -217,10 +217,10 @@
217 217 <div id="project-dashboard-view" class="subview hidden">
218 218 <div class="page-header">
219 219 <div class="row-flex row-flex-4">
220 - <button class="btn btn-secondary" onclick="GoingsOn.projects.closeDashboard()">&larr; Back</button>
220 + <button class="btn btn-secondary" data-act="projects.closeDashboard">&larr; Back</button>
221 221 <h2 class="page-title" id="project-dashboard-title">Project</h2>
222 222 </div>
223 - <button class="btn btn-secondary" id="edit-project-btn" onclick="GoingsOn.projects.editCurrent()">Edit Project</button>
223 + <button class="btn btn-secondary" id="edit-project-btn" data-act="projects.editCurrent">Edit Project</button>
224 224 </div>
225 225 <p id="project-dashboard-description" class="project-dashboard-desc"></p>
226 226 <div id="project-milestones-section" class="milestones-section"></div>
@@ -228,7 +228,7 @@
228 228 <div class="dashboard-column">
229 229 <div class="dashboard-column-header">
230 230 <h3>Tasks</h3>
231 - <button class="btn btn-sm btn-primary" onclick="GoingsOn.projects.addTask()">+ Add</button>
231 + <button class="btn btn-sm btn-primary" data-act="projects.addTask">+ Add</button>
232 232 </div>
233 233 <div id="project-tasks-list" class="dashboard-list">
234 234 <div class="skeleton-shimmer" aria-label="Loading tasks"><div class="skeleton-row"><div class="skeleton-lines"><div class="skeleton-line long"></div><div class="skeleton-line short"></div></div></div><div class="skeleton-row"><div class="skeleton-lines"><div class="skeleton-line medium"></div><div class="skeleton-line long"></div></div></div></div>
@@ -237,7 +237,7 @@
237 237 <div class="dashboard-column">
238 238 <div class="dashboard-column-header">
239 239 <h3>Events</h3>
240 - <button class="btn btn-sm btn-primary" onclick="GoingsOn.projects.addEvent()">+ Add</button>
240 + <button class="btn btn-sm btn-primary" data-act="projects.addEvent">+ Add</button>
241 241 </div>
242 242 <div id="project-events-list" class="dashboard-list">
243 243 <div class="skeleton-shimmer" aria-label="Loading events"><div class="skeleton-row"><div class="skeleton-lines"><div class="skeleton-line medium"></div><div class="skeleton-line short"></div></div></div><div class="skeleton-row"><div class="skeleton-lines"><div class="skeleton-line long"></div><div class="skeleton-line medium"></div></div></div></div>
@@ -246,7 +246,7 @@
246 246 <div class="dashboard-column">
247 247 <div class="dashboard-column-header">
248 248 <h3>Emails</h3>
249 - <button class="btn btn-sm btn-primary" onclick="GoingsOn.projects.linkEmail()">+ Link</button>
249 + <button class="btn btn-sm btn-primary" data-act="projects.linkEmail">+ Link</button>
250 250 </div>
251 251 <div id="project-emails-list" class="dashboard-list">
252 252 <div class="skeleton-shimmer" aria-label="Loading emails"><div class="skeleton-row"><div class="skeleton-lines"><div class="skeleton-line long"></div><div class="skeleton-line medium"></div></div></div><div class="skeleton-row"><div class="skeleton-lines"><div class="skeleton-line short"></div><div class="skeleton-line long"></div></div></div></div>
@@ -267,7 +267,7 @@
267 267 <div id="task-overview-view" class="subview hidden">
268 268 <div class="page-header">
269 269 <div class="row-flex row-flex-4">
270 - <button class="btn btn-secondary" onclick="GoingsOn.taskOverview.close()">&larr; Back</button>
270 + <button class="btn btn-secondary" data-act="taskOverview.close">&larr; Back</button>
271 271 <h2 class="page-title" id="task-overview-title">Task Overview</h2>
272 272 </div>
273 273 <div id="task-overview-actions"></div>
@@ -296,10 +296,10 @@
296 296 <div id="day-plan-view" class="subview" role="tabpanel" aria-labelledby="day-plan-tab">
297 297 <div class="page-header">
298 298 <div class="day-plan-nav">
299 - <button class="btn btn-secondary" onclick="GoingsOn.dayPlan.previousDay()" title="Previous day ([)">&larr;</button>
300 - <button class="btn btn-secondary" onclick="GoingsOn.dayPlan.goToToday()">Today</button>
301 - <button class="btn btn-secondary" onclick="GoingsOn.dayPlan.nextDay()" title="Next day (])">&rarr;</button>
302 - <input type="date" id="day-plan-date" class="day-plan-date-picker" onchange="GoingsOn.dayPlan.onDatePickerChange()">
299 + <button class="btn btn-secondary" data-act="dayPlan.previousDay" title="Previous day ([)">&larr;</button>
300 + <button class="btn btn-secondary" data-act="dayPlan.goToToday">Today</button>
301 + <button class="btn btn-secondary" data-act="dayPlan.nextDay" title="Next day (])">&rarr;</button>
302 + <input type="date" id="day-plan-date" class="day-plan-date-picker" data-change="dayPlan.onDatePickerChange">
303 303 <span id="day-plan-date-display" class="day-plan-date-display"></span>
304 304 </div>
305 305 <span id="day-review-status-badge" class="review-status hidden"></span>
@@ -317,7 +317,7 @@
317 317 </div>
318 318 <div class="day-plan-sidebar">
319 319 <div id="time-summary-container"></div>
320 - <button class="day-plan-sidebar-toggle" onclick="GoingsOn.dayPlan.toggleSidebar()">
320 + <button class="day-plan-sidebar-toggle" data-act="dayPlan.toggleSidebar">
321 321 <span>Tasks to Schedule</span>
322 322 <span class="toggle-arrow">&#x25BC;</span>
323 323 </button>
@@ -331,7 +331,7 @@
331 331 </div>
332 332 </div>
333 333 <div class="finish-review-bar">
334 - <button class="btn btn-primary finish-review-btn" id="day-finish-review-btn" onclick="GoingsOn.dayPlanSchedule.openFinishReviewModal()">
334 + <button class="btn btn-primary finish-review-btn" id="day-finish-review-btn" data-act="dayPlanSchedule.openFinishReviewModal">
335 335 Finish &amp; Review
336 336 </button>
337 337 </div>
@@ -355,9 +355,9 @@
355 355 <div id="monthly-review-view" class="subview hidden" role="tabpanel" aria-labelledby="monthly-review-tab">
356 356 <div class="page-header">
357 357 <div class="monthly-review-nav">
358 - <button class="btn btn-secondary" onclick="GoingsOn.monthlyReview.previousMonth()" title="Previous month">&larr;</button>
359 - <button class="btn btn-secondary" onclick="GoingsOn.monthlyReview.goToCurrentMonth()">This Month</button>
360 - <button class="btn btn-secondary" onclick="GoingsOn.monthlyReview.nextMonth()" title="Next month">&rarr;</button>
358 + <button class="btn btn-secondary" data-act="monthlyReview.previousMonth" title="Previous month">&larr;</button>
359 + <button class="btn btn-secondary" data-act="monthlyReview.goToCurrentMonth">This Month</button>
360 + <button class="btn btn-secondary" data-act="monthlyReview.nextMonth" title="Next month">&rarr;</button>
361 361 <span id="monthly-review-month-display" class="monthly-review-month-display"></span>
362 362 </div>
363 363 <span id="month-review-status-badge" class="review-status hidden"></span>
@@ -384,19 +384,19 @@
384 384 <div id="events-view" class="subview hidden" role="tabpanel" aria-labelledby="events-tab">
385 385 <div class="page-header">
386 386 <h2 class="page-title">Events</h2>
387 - <button class="btn btn-primary" onclick="GoingsOn.events.openNew()" title="New event (n)">+ New Event <kbd class="kbd-hint">n</kbd></button>
387 + <button class="btn btn-primary" data-act="events.openNew" title="New event (n)">+ New Event <kbd class="kbd-hint">n</kbd></button>
388 388 </div>
389 389 <div class="filter-bar" id="events-filter-bar">
390 390 <label class="filter-checkbox" title="Include events hidden until a later date">
391 - <input type="checkbox" id="filter-events-snoozed" onchange="GoingsOn.events.onFilterChange()">
391 + <input type="checkbox" id="filter-events-snoozed" data-change="events.onFilterChange">
392 392 Show Snoozed
393 393 </label>
394 394 </div>
395 395 <div id="events-bulk-bar" class="bulk-actions-bar hidden">
396 396 <span class="bulk-count">0 selected</span>
397 - <button class="btn btn-sm" onclick="GoingsOn.events.selectAllEvents()">Select All</button>
398 - <button class="btn btn-sm btn-danger" onclick="GoingsOn.events.bulkDelete()">Delete</button>
399 - <button class="btn btn-sm" onclick="GoingsOn.events.clearEventSelection()">Cancel</button>
397 + <button class="btn btn-sm" data-act="events.selectAllEvents">Select All</button>
398 + <button class="btn btn-sm btn-danger" data-act="events.bulkDelete">Delete</button>
399 + <button class="btn btn-sm" data-act="events.clearEventSelection">Cancel</button>
400 400 </div>
401 401 <div class="events-list" id="events-list">
402 402 <details class="past-events-section hidden" id="recurring-events-section" open>
@@ -464,27 +464,27 @@
464 464 <div class="page-header">
465 465 <h2 class="page-title">Emails</h2>
466 466 <div class="row-flex row-flex-2">
467 - <button class="btn btn-secondary mobile-hide" onclick="GoingsOn.emails.openDrafts()">Drafts</button>
468 - <button class="btn btn-primary" onclick="GoingsOn.emails.openCompose()" title="Compose email (n)">+ Compose</button>
467 + <button class="btn btn-secondary mobile-hide" data-act="emails.openDrafts">Drafts</button>
468 + <button class="btn btn-primary" data-act="emails.openCompose" title="Compose email (n)">+ Compose</button>
469 469 </div>
470 470 </div>
471 471 <div class="email-filter-row mb-2">
472 - <input type="text" class="form-input" id="email-search" placeholder="Search emails..." oninput="GoingsOn.emails.search(this.value)">
473 - <select class="form-select" id="email-folder-filter" onchange="GoingsOn.emails.filterByFolder(this.value)">
472 + <input type="text" class="form-input" id="email-search" placeholder="Search emails..." data-input="emails.search">
473 + <select class="form-select" id="email-folder-filter" data-change="emails.filterByFolder">
474 474 <option value="">All folders</option>
475 475 </select>
476 - <select class="form-select" id="email-label-filter" onchange="GoingsOn.emails.filterByLabel(this.value)">
476 + <select class="form-select" id="email-label-filter" data-change="emails.filterByLabel">
477 477 <option value="">All labels</option>
478 478 </select>
479 479 <span id="email-count" class="filter-count" aria-live="polite"></span>
480 480 </div>
481 481 <div id="email-bulk-actions" class="bulk-actions-bar hidden" role="toolbar" aria-label="Bulk email actions">
482 482 <span id="email-bulk-count" class="bulk-count">0 selected</span>
483 - <button class="btn btn-sm" onclick="GoingsOn.bulk.markEmailsRead()">Mark Read</button>
484 - <button class="btn btn-sm" onclick="GoingsOn.bulk.archiveEmails()">Archive</button>
485 - <button class="btn btn-sm" onclick="GoingsOn.bulk.snoozeEmails()">Snooze</button>
486 - <button class="btn btn-sm btn-danger" onclick="GoingsOn.bulk.deleteEmails()">Delete</button>
487 - <button class="btn btn-sm bulk-select-all" onclick="GoingsOn.bulk.selectAllEmails()">Select All</button>
483 + <button class="btn btn-sm" data-act="bulk.markEmailsRead">Mark Read</button>
484 + <button class="btn btn-sm" data-act="bulk.archiveEmails">Archive</button>
485 + <button class="btn btn-sm" data-act="bulk.snoozeEmails">Snooze</button>
486 + <button class="btn btn-sm btn-danger" data-act="bulk.deleteEmails">Delete</button>
487 + <button class="btn btn-sm bulk-select-all" data-act="bulk.selectAllEmails">Select All</button>
488 488 </div>
489 489 <div class="card card--shell email-list" id="email-list-wrapper" role="list">
490 490 <div class="email-list-container" id="email-list" aria-live="polite">
@@ -498,9 +498,9 @@
498 498 </div>
499 499 <!-- Pagination -->
500 500 <div id="email-pagination" class="pagination-controls hidden" role="navigation" aria-label="Email list pagination">
501 - <button class="btn btn-sm" id="email-prev-page" onclick="GoingsOn.emails.goToPage('prev')" disabled aria-label="Previous page">&larr; Previous</button>
501 + <button class="btn btn-sm" id="email-prev-page" data-act="emails.goToPage" data-a1="prev" disabled aria-label="Previous page">&larr; Previous</button>
502 502 <span id="email-page-info" class="pagination-info">Page 1 of 1</span>
503 - <button class="btn btn-sm" id="email-next-page" onclick="GoingsOn.emails.goToPage('next')" disabled aria-label="Next page">Next &rarr;</button>
503 + <button class="btn btn-sm" id="email-next-page" data-act="emails.goToPage" data-a1="next" disabled aria-label="Next page">Next &rarr;</button>
504 504 </div>
505 505 </div>
506 506
@@ -509,19 +509,19 @@
509 509 <div class="page-header">
510 510 <h2 class="page-title">Contacts</h2>
511 511 <div class="contacts-filter-row">
512 - <input type="text" class="form-input" id="contacts-search" placeholder="Search contacts..." oninput="GoingsOn.contacts.filterBySearch(this.value)">
513 - <select class="filter-select" id="contacts-tag-filter" onchange="GoingsOn.contacts.filterByTag(this.value)">
512 + <input type="text" class="form-input" id="contacts-search" placeholder="Search contacts..." data-input="contacts.filterBySearch">
513 + <select class="filter-select" id="contacts-tag-filter" data-change="contacts.filterByTag">
514 514 <option value="">All Tags</option>
515 515 </select>
516 - <button class="btn btn-primary" onclick="GoingsOn.contacts.openNew()" title="New contact (n)">+ New Contact <kbd class="kbd-hint">n</kbd></button>
516 + <button class="btn btn-primary" data-act="contacts.openNew" title="New contact (n)">+ New Contact <kbd class="kbd-hint">n</kbd></button>
517 517 </div>
518 518 </div>
519 519 <div id="contacts-bulk-bar" class="bulk-actions-bar hidden">
520 520 <span class="bulk-count">0 selected</span>
521 - <button class="btn btn-sm" onclick="GoingsOn.contacts.selectAll()">Select All</button>
522 - <button class="btn btn-sm" onclick="GoingsOn.contacts.bulkTag()">Tag</button>
523 - <button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.bulkDelete()">Delete</button>
524 - <button class="btn btn-sm" onclick="GoingsOn.contacts.clearSelection()">Cancel</button>
521 + <button class="btn btn-sm" data-act="contacts.selectAll">Select All</button>
522 + <button class="btn btn-sm" data-act="contacts.bulkTag">Tag</button>
523 + <button class="btn btn-sm btn-danger" data-act="contacts.bulkDelete">Delete</button>
524 + <button class="btn btn-sm" data-act="contacts.clearSelection">Cancel</button>
525 525 </div>
526 526 <div class="cards-grid" id="contacts-grid">
527 527 <div class="skeleton-shimmer" aria-label="Loading contacts">
@@ -535,7 +535,7 @@
535 535 <div id="contact-dashboard-view" class="subview hidden">
536 536 <div class="page-header">
537 537 <div class="row-flex row-flex-4">
538 - <button class="btn btn-secondary" onclick="GoingsOn.contactDashboard.close()">&larr; Back</button>
538 + <button class="btn btn-secondary" data-act="contactDashboard.close">&larr; Back</button>
539 539 <h2 class="page-title" id="contact-dashboard-title">Contact</h2>
540 540 </div>
541 541 <div id="contact-dashboard-actions"></div>
@@ -556,20 +556,20 @@
556 556
557 557 <!-- Settings Overlay (Phase 7 Tier 6 — settings as modeless drawer) -->
558 558 <div id="settings-overlay" class="settings-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="settings-overlay-title" aria-hidden="true">
559 - <div class="settings-overlay-backdrop" onclick="GoingsOn.settings.goBack()" aria-hidden="true"></div>
559 + <div class="settings-overlay-backdrop" data-act="settings.goBack" aria-hidden="true"></div>
560 560 <div id="settings-view" class="view settings-overlay-card">
561 561 <div class="settings-page-layout">
562 562 <nav class="settings-sidebar">
563 - <button class="btn-link mb-1 settings-back" onclick="GoingsOn.settings.goBack()" aria-label="Close settings">&larr; Close</button>
563 + <button class="btn-link mb-1 settings-back" data-act="settings.goBack" aria-label="Close settings">&larr; Close</button>
564 564 <h2 class="sr-only" id="settings-overlay-title">Settings</h2>
565 565 <div class="settings-nav-items">
566 - <button class="settings-nav-item active" data-section="appearance" onclick="GoingsOn.settings.showSection('appearance')">Appearance</button>
567 - <button class="settings-nav-item" data-section="notifications" onclick="GoingsOn.settings.showSection('notifications')">Notifications</button>
568 - <button class="settings-nav-item" data-section="email" onclick="GoingsOn.settings.showSection('email')">Email Accounts</button>
569 - <button class="settings-nav-item" data-section="planning" onclick="GoingsOn.settings.showSection('planning')">Planning &amp; Review</button>
570 - <button class="settings-nav-item" data-section="sync" onclick="GoingsOn.settings.showSection('sync')">Cloud Sync</button>
571 - <button class="settings-nav-item" data-section="data" onclick="GoingsOn.settings.showSection('data')">Import &amp; Export</button>
572 - <button class="settings-nav-item" data-section="about" onclick="GoingsOn.settings.showSection('about')">About</button>
566 + <button class="settings-nav-item active" data-section="appearance" data-act="settings.showSection" data-a1="appearance">Appearance</button>
567 + <button class="settings-nav-item" data-section="notifications" data-act="settings.showSection" data-a1="notifications">Notifications</button>
568 + <button class="settings-nav-item" data-section="email" data-act="settings.showSection" data-a1="email">Email Accounts</button>
569 + <button class="settings-nav-item" data-section="planning" data-act="settings.showSection" data-a1="planning">Planning &amp; Review</button>
570 + <button class="settings-nav-item" data-section="sync" data-act="settings.showSection" data-a1="sync">Cloud Sync</button>
571 + <button class="settings-nav-item" data-section="data" data-act="settings.showSection" data-a1="data">Import &amp; Export</button>
572 + <button class="settings-nav-item" data-section="about" data-act="settings.showSection" data-a1="about">About</button>
573 573 </div>
574 574 </nav>
575 575 <div class="settings-content" id="settings-content">
@@ -585,7 +585,7 @@
585 585 <div class="modal-drag-handle"></div>
586 586 <div class="modal-header">
587 587 <h2 id="modal-title">Modal Title</h2>
588 - <button class="modal-close" onclick="GoingsOn.ui.closeModal()" aria-label="Close modal">&times;</button>
588 + <button class="modal-close" data-act="ui.closeModal" aria-label="Close modal">&times;</button>
589 589 </div>
590 590 <div id="modal-content" class="modal-content"></div>
591 591 </div>
@@ -599,7 +599,7 @@
599 599 <!-- Task Detail Drawer (Phase 7 Tier 6 — side-drawer task detail) -->
600 600 <aside id="task-detail-drawer" class="task-drawer" role="dialog" aria-modal="false" aria-labelledby="task-drawer-title" aria-hidden="true">
601 601 <div class="task-drawer-header">
602 - <button class="btn btn-sm btn-secondary task-drawer-close" onclick="GoingsOn.taskOverview.close()" title="Close (Esc)" aria-label="Close task detail">&times;</button>
602 + <button class="btn btn-sm btn-secondary task-drawer-close" data-act="taskOverview.close" title="Close (Esc)" aria-label="Close task detail">&times;</button>
603 603 <h2 class="task-drawer-title" id="task-drawer-title">Task</h2>
604 604 <div class="task-drawer-actions" id="task-drawer-actions"></div>
605 605 </div>
@@ -631,13 +631,14 @@
631 631 <div class="action-sheet-container">
632 632 <div class="action-sheet-handle"></div>
633 633 <div class="action-sheet-content" id="action-sheet-content"></div>
634 - <button class="action-sheet-cancel" onclick="GoingsOn.ui.hideActionSheet()">Cancel</button>
634 + <button class="action-sheet-cancel" data-act="ui.hideActionSheet">Cancel</button>
635 635 </div>
636 636 </div>
637 637
638 638
639 639 <!-- Namespace (must load first) -->
640 640 <script src="js/goingson.js"></script>
641 + <script src="js/dispatch.js"></script>
641 642 <script src="js/viewport.js"></script>
642 643
643 644 <!-- Core Layer -->
@@ -0,0 +1,84 @@
1 + /**
2 + * @fileoverview Delegated event dispatch — the CSP-safe replacement for inline
3 + * `on*` HTML attributes.
4 + *
5 + * Tauri v2 injects a nonce into the CSP `script-src`, which makes
6 + * `'unsafe-inline'` ignored, so inline `onclick="..."` handlers are refused by
7 + * a compliant WebView (WebKitGTK on Linux). This module registers one delegated
8 + * listener per event type on `document`; markup opts in with data attributes.
9 + *
10 + * Convention — replace:
11 + * onclick="GoingsOn.tasks.open('123')"
12 + * with:
13 + * data-act="tasks.open" data-a1="123"
14 + *
15 + * - `data-act` (click), `data-change`, `data-input`, `data-submit`,
16 + * `data-keydown`, `data-contextmenu`, `data-mousedown` hold a dot-path
17 + * resolved against `window.GoingsOn`.
18 + * - Positional args come from `data-a1`, `data-a2`, … (strings), or a single
19 + * `data-args` holding a JSON array (for typed / null / multi args).
20 + * - Handlers are called `fn(...args, ctx)` with `ctx = { event, el }`. Methods
21 + * that need the event or element read the trailing `ctx`.
22 + * - `change` / `input` pass the element's value first: `fn(value, ...args, ctx)`,
23 + * matching the old `onchange="foo(this.value)"` idiom.
24 + * - Nested clickables: the innermost `[data-act]` wins via `closest()`, so no
25 + * `event.stopPropagation()` is needed — a row and a button inside it can both
26 + * carry `data-act` and only the button fires.
27 + */
28 +
29 + (function () {
30 + 'use strict';
31 +
32 + /** Resolve a dot-path (e.g. "tasks.open") to a function on GoingsOn. */
33 + function resolve(path) {
34 + if (!path) return null;
35 + var parts = path.split('.');
36 + var obj = window.GoingsOn;
37 + for (var i = 0; i < parts.length && obj != null; i++) {
38 + obj = obj[parts[i]];
39 + }
40 + return typeof obj === 'function' ? obj : null;
41 + }
42 +
43 + /** Collect positional args: `data-args` JSON array, else data-a1..aN. */
44 + function argsFrom(el) {
45 + if (el.dataset.args !== undefined) {
46 + try { return JSON.parse(el.dataset.args); } catch (e) { return []; }
47 + }
48 + var out = [], i = 1, v;
49 + while ((v = el.dataset['a' + i]) !== undefined) { out.push(v); i++; }
50 + return out;
51 + }
52 +
53 + /**
54 + * Register a delegated listener for `eventType`, dispatching elements that
55 + * carry `data-<attr>`.
56 + * @param {string} eventType DOM event name.
57 + * @param {string} attr dataset key holding the method path.
58 + * @param {{valueFirst?:boolean, preventDefault?:boolean}} [opts]
59 + */
60 + function bind(eventType, attr, opts) {
61 + opts = opts || {};
62 + var selector = '[data-' + attr + ']';
63 + document.addEventListener(eventType, function (event) {
64 + var start = event.target;
65 + if (!start || !start.closest) return;
66 + var el = start.closest(selector);
67 + if (!el) return;
68 + var fn = resolve(el.dataset[attr]);
69 + if (!fn) return;
70 + var args = argsFrom(el);
71 + if (opts.valueFirst) args = [el.value].concat(args);
72 + if (opts.preventDefault) event.preventDefault();
73 + fn.apply(el, args.concat([{ event: event, el: el }]));
74 + });
75 + }
76 +
77 + bind('click', 'act');
78 + bind('change', 'change', { valueFirst: true });
79 + bind('input', 'input', { valueFirst: true });
80 + bind('submit', 'submit', { preventDefault: true });
81 + bind('keydown', 'keydown');
82 + bind('contextmenu', 'contextmenu');
83 + bind('mousedown', 'mousedown');
84 + })();
@@ -23,7 +23,8 @@
23 23 }
24 24 ],
25 25 "security": {
26 - "csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: asset: http://asset.localhost; font-src 'self'; connect-src 'self' ipc: http://ipc.localhost; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'"
26 + "csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: asset: http://asset.localhost; font-src 'self'; connect-src 'self' ipc: http://ipc.localhost; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'",
27 + "dangerousDisableAssetCspModification": ["script-src"]
27 28 }
28 29 },
29 30 "bundle": {