Skip to main content

max / goingson

Frontend cold spots R4: escapeHandlerArg sweep + gate rule, email preview to Rust - swap escapeAttrValue -> escapeHandlerArg on 183 inline-handler args across 23 files (escapeAttrValue does not escape ' or \, leaving the single-quoted JS-string layer exposed) - add build-failing JS gate rule catching attr-escaper use inside on*= handler args (56 JS tests; verified it flags a reintroduced violation) - EmailResponse.bodyPreview computed char-safe in Rust; emails.js drops e.body.substring(0,100) list-side slicing Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-04 18:52 UTC
Commit: 513c4297360e5c2f5f4b130160b2b0e65952fa10
Parent: 797a045
25 files changed, +215 insertions, -169 deletions
@@ -67,13 +67,13 @@
67 67 </div>
68 68 <div class="attachment-actions">
69 69 ${a.hasLocalBlob ? `
70 - <button class="btn btn-sm btn-secondary" onclick="GoingsOn.attachments.open('${escAttr(a.id)}')" title="Open">Open</button>
71 - <button class="btn btn-sm btn-secondary" onclick="GoingsOn.attachments.saveAs('${escAttr(a.id)}', '${escArg(a.filename)}')" title="Save As">Save</button>
70 + <button class="btn btn-sm btn-secondary" onclick="GoingsOn.attachments.open('${escArg(a.id)}')" title="Open">Open</button>
71 + <button class="btn btn-sm btn-secondary" onclick="GoingsOn.attachments.saveAs('${escArg(a.id)}', '${escArg(a.filename)}')" title="Save As">Save</button>
72 72 ` : `
73 73 <span class="attachment-sync-warning">Sync needed</span>
74 74 `}
75 75 <button class="btn btn-sm btn-secondary text-accent-red"
76 - onclick="GoingsOn.attachments.remove('${escAttr(a.id)}', '${tid}', '${pid}')" title="Delete">×</button>
76 + onclick="GoingsOn.attachments.remove('${escArg(a.id)}', '${tid}', '${pid}')" title="Delete">×</button>
77 77 </div>
78 78 </div>
79 79 `).join('');
@@ -7,6 +7,7 @@
7 7 'use strict';
8 8 const esc = GoingsOn.utils.escapeHtml;
9 9 const escAttr = GoingsOn.utils.escapeAttrValue;
10 + const escArg = GoingsOn.utils.escapeHandlerArg;
10 11
11 12 /**
12 13 * Show or hide the bulk actions bars based on current selection state.
@@ -55,7 +56,7 @@
55 56
56 57 for (const opt of options) {
57 58 optionsHtml += `
58 - <button class="snooze-option" onclick="GoingsOn.bulk._snoozeCallback('${escAttr(opt.time)}')">
59 + <button class="snooze-option" onclick="GoingsOn.bulk._snoozeCallback('${escArg(opt.time)}')">
59 60 <span class="snooze-option-label">${esc(opt.label)}</span>
60 61 <span class="snooze-option-time">${esc(opt.formatted)}</span>
61 62 </button>
@@ -199,7 +200,7 @@
199 200 let optionsHtml = `<p class="bulk-modal-prompt bulk-modal-prompt--wide">Set project for ${selectedTaskIds.size} tasks:</p>`;
200 201 optionsHtml += `<button class="btn btn-sm text-left w-full bulk-modal-option-btn" onclick="GoingsOn.bulk._applyProject(null)">No Project</button>`;
201 202 for (const p of projects) {
202 - optionsHtml += `<button class="btn btn-sm text-left w-full bulk-modal-option-btn" onclick="GoingsOn.bulk._applyProject('${GoingsOn.utils.escapeAttrValue(p.id)}')">${GoingsOn.utils.escapeHtml(p.name)}</button>`;
203 + optionsHtml += `<button class="btn btn-sm text-left w-full bulk-modal-option-btn" onclick="GoingsOn.bulk._applyProject('${escArg(p.id)}')">${GoingsOn.utils.escapeHtml(p.name)}</button>`;
203 204 }
204 205 GoingsOn.ui.openModal('Set Project', `<div class="bulk-modal-scroll">${optionsHtml}</div>`);
205 206 }
@@ -9,6 +9,7 @@
9 9 'use strict';
10 10 const esc = GoingsOn.utils.escapeHtml;
11 11 const escAttr = GoingsOn.utils.escapeAttrValue;
12 + const escArg = GoingsOn.utils.escapeHandlerArg;
12 13
13 14 let currentContactId = null;
14 15
@@ -59,9 +60,9 @@
59 60 : '';
60 61
61 62 let onclick = '';
62 - if (item.kind === 'task') onclick = `GoingsOn.taskOverview.open('${escAttr(item.id)}')`;
63 - else if (item.kind === 'event') onclick = `GoingsOn.events.open('${escAttr(item.id)}')`;
64 - else if (item.kind === 'email') onclick = `GoingsOn.emails.open('${escAttr(item.id)}')`;
63 + if (item.kind === 'task') onclick = `GoingsOn.taskOverview.open('${escArg(item.id)}')`;
64 + else if (item.kind === 'event') onclick = `GoingsOn.events.open('${escArg(item.id)}')`;
65 + else if (item.kind === 'email') onclick = `GoingsOn.emails.open('${escArg(item.id)}')`;
65 66
66 67 return `
67 68 <div class="contact-timeline-item row-flex row-flex-3" onclick="${onclick}" role="button" tabindex="0">
@@ -80,12 +81,12 @@
80 81 // Actions bar
81 82 if (contact.isImplicit) {
82 83 actionsEl.innerHTML = `
83 - <button class="btn btn-primary" onclick="GoingsOn.contactDashboard.promote('${escAttr(contact.id)}')">Save as Contact</button>
84 + <button class="btn btn-primary" onclick="GoingsOn.contactDashboard.promote('${escArg(contact.id)}')">Save as Contact</button>
84 85 `;
85 86 } else {
86 87 actionsEl.innerHTML = `
87 - <button class="btn btn-secondary" onclick="GoingsOn.contacts.openEdit('${escAttr(contact.id)}')">Edit</button>
88 - <button class="btn btn-secondary text-accent-red" onclick="GoingsOn.contacts.delete('${escAttr(contact.id)}')">Delete</button>
88 + <button class="btn btn-secondary" onclick="GoingsOn.contacts.openEdit('${escArg(contact.id)}')">Edit</button>
89 + <button class="btn btn-secondary text-accent-red" onclick="GoingsOn.contacts.delete('${escArg(contact.id)}')">Delete</button>
89 90 `;
90 91 }
91 92
@@ -7,6 +7,7 @@
7 7 'use strict';
8 8 const esc = GoingsOn.utils.escapeHtml;
9 9 const escAttr = GoingsOn.utils.escapeAttrValue;
10 + const escArg = GoingsOn.utils.escapeHandlerArg;
10 11 const escAttrVal = GoingsOn.utils.escapeAttrValue;
11 12 const safeUrl = GoingsOn.utils.safeUrl;
12 13
@@ -39,11 +40,11 @@
39 40 ).join('');
40 41
41 42 return `
42 - <div class="card contact-card" onclick="GoingsOn.contacts.open('${escAttr(c.id)}')"
43 + <div class="card contact-card" onclick="GoingsOn.contacts.open('${escArg(c.id)}')"
43 44 tabindex="0" role="button" aria-label="Open contact ${esc(c.displayName)}">
44 45 <div class="card-header contact-card-header">
45 46 <input type="checkbox" class="bulk-checkbox contact-select-cb" data-id="${escAttr(c.id)}"
46 - onclick="event.stopPropagation(); GoingsOn.contacts.toggleSelection('${escAttr(c.id)}', event)"
47 + onclick="event.stopPropagation(); GoingsOn.contacts.toggleSelection('${escArg(c.id)}', event)"
47 48 aria-label="Select ${esc(c.displayName)}">
48 49 <div class="avatar">${esc(initials)}</div>
49 50 <div class="contact-card-body">
@@ -51,7 +52,7 @@
51 52 ${nickname}
52 53 ${company}
53 54 </div>
54 - <button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); GoingsOn.contacts.openEdit('${escAttr(c.id)}')" title="Edit">...</button>
55 + <button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); GoingsOn.contacts.openEdit('${escArg(c.id)}')" title="Edit">...</button>
55 56 </div>
56 57 ${emailLine ? `<p class="card-description">${emailLine}</p>` : ''}
57 58 ${tagPills ? `<div class="card-meta">${tagPills}</div>` : ''}
@@ -85,7 +86,7 @@
85 86 const emailRows = (contact.emails || []).map(e => `
86 87 <div class="sub-item">
87 88 <span>${esc(e.address)} ${e.label ? `<small>(${esc(e.label)})</small>` : ''} ${e.isPrimary ? '<strong>Primary</strong>' : ''}</span>
88 - <button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.removeEmail('${escAttr(contact.id)}', '${escAttr(e.id)}')" title="Remove">x</button>
89 + <button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.removeEmail('${escArg(contact.id)}', '${escArg(e.id)}')" title="Remove">x</button>
89 90 </div>
90 91 `).join('');
91 92
@@ -93,7 +94,7 @@
93 94 const phoneRows = (contact.phones || []).map(p => `
94 95 <div class="sub-item">
95 96 <span>${esc(p.number)} ${p.label ? `<small>(${esc(p.label)})</small>` : ''} ${p.isPrimary ? '<strong>Primary</strong>' : ''}</span>
96 - <button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.removePhone('${escAttr(contact.id)}', '${escAttr(p.id)}')" title="Remove">x</button>
97 + <button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.removePhone('${escArg(contact.id)}', '${escArg(p.id)}')" title="Remove">x</button>
97 98 </div>
98 99 `).join('');
99 100
@@ -101,7 +102,7 @@
101 102 const socialRows = (contact.socialHandles || []).map(s => `
102 103 <div class="sub-item">
103 104 <span><strong>${esc(s.platform)}:</strong> ${s.url ? `<a href="${escAttrVal(safeUrl(s.url))}" target="_blank">${esc(s.handle)}</a>` : esc(s.handle)}</span>
104 - <button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.removeSocialHandle('${escAttr(contact.id)}', '${escAttr(s.id)}')" title="Remove">x</button>
105 + <button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.removeSocialHandle('${escArg(contact.id)}', '${escArg(s.id)}')" title="Remove">x</button>
105 106 </div>
106 107 `).join('');
107 108
@@ -109,7 +110,7 @@
109 110 const customFieldRows = (contact.customFields || []).map(f => `
110 111 <div class="sub-item">
111 112 <span><strong>${esc(f.label)}:</strong> ${f.url ? `<a href="${escAttrVal(safeUrl(f.url))}" target="_blank">${esc(f.value)}</a>` : esc(f.value)}</span>
112 - <button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.removeCustomField('${escAttr(contact.id)}', '${escAttr(f.id)}')" title="Remove">x</button>
113 + <button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.removeCustomField('${escArg(contact.id)}', '${escArg(f.id)}')" title="Remove">x</button>
113 114 </div>
114 115 `).join('');
115 116
@@ -130,7 +131,7 @@
130 131 <div class="sub-collection">
131 132 <div class="sub-collection-header">
132 133 <h4>Email Addresses</h4>
133 - <button class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddEmail('${escAttr(contact.id)}')">+ Add</button>
134 + <button class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddEmail('${escArg(contact.id)}')">+ Add</button>
134 135 </div>
135 136 ${emailRows || '<div class="sub-empty">No email addresses</div>'}
136 137 </div>
@@ -138,7 +139,7 @@
138 139 <div class="sub-collection">
139 140 <div class="sub-collection-header">
140 141 <h4>Phone Numbers</h4>
141 - <button class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddPhone('${escAttr(contact.id)}')">+ Add</button>
142 + <button class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddPhone('${escArg(contact.id)}')">+ Add</button>
142 143 </div>
143 144 ${phoneRows || '<div class="sub-empty">No phone numbers</div>'}
144 145 </div>
@@ -146,7 +147,7 @@
146 147 <div class="sub-collection">
147 148 <div class="sub-collection-header">
148 149 <h4>Social Handles</h4>
149 - <button class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddSocial('${escAttr(contact.id)}')">+ Add</button>
150 + <button class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddSocial('${escArg(contact.id)}')">+ Add</button>
150 151 </div>
151 152 ${socialRows || '<div class="sub-empty">No social handles</div>'}
152 153 </div>
@@ -154,13 +155,13 @@
154 155 <div class="sub-collection">
155 156 <div class="sub-collection-header">
156 157 <h4>Custom Fields</h4>
157 - <button class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddCustomField('${escAttr(contact.id)}')">+ Add</button>
158 + <button class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddCustomField('${escArg(contact.id)}')">+ Add</button>
158 159 </div>
159 160 ${customFieldRows || '<div class="sub-empty">No custom fields</div>'}
160 161 </div>
161 162
162 163 <div class="form-actions form-actions--spaced">
163 - <button class="btn btn-secondary" onclick="GoingsOn.contacts.openEdit('${escAttr(contact.id)}')">Edit Contact</button>
164 + <button class="btn btn-secondary" onclick="GoingsOn.contacts.openEdit('${escArg(contact.id)}')">Edit Contact</button>
164 165 </div>
165 166 </div>
166 167 `;
@@ -7,6 +7,7 @@
7 7 'use strict';
8 8 const esc = GoingsOn.utils.escapeHtml;
9 9 const escAttr = GoingsOn.utils.escapeAttrValue;
10 + const escArg = GoingsOn.utils.escapeHandlerArg;
10 11
11 12 // ============ Selection State ============
12 13
@@ -292,8 +293,8 @@
292 293 const submitFn = isEdit ? EDIT_SUBMIT_FN[type] : ADD_SUBMIT_FN[type];
293 294 const submitText = isEdit ? (config.editButtonText || 'Save') : config.submitButtonText;
294 295 const onclick = isEdit
295 - ? `GoingsOn.contacts.${submitFn}('${escAttr(contactId)}', '${escAttr(editingId)}')`
296 - : `GoingsOn.contacts.${submitFn}('${escAttr(contactId)}')`;
296 + ? `GoingsOn.contacts.${submitFn}('${escArg(contactId)}', '${escArg(editingId)}')`
297 + : `GoingsOn.contacts.${submitFn}('${escArg(contactId)}')`;
297 298
298 299 return `
299 300 <form id="${escAttr(config.formId)}">
@@ -628,8 +629,8 @@
628 629 // `GoingsOn.state.contacts` rather than smuggled through HTML attrs.
629 630 const rowActions = (kind, rowId) => `
630 631 <span class="sub-item-actions">
631 - <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.contacts.openEdit${kind}('${escAttr(id)}', '${escAttr(rowId)}')" title="Edit">Edit</button>
632 - <button type="button" class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.remove${kind === 'Social' ? 'SocialHandle' : kind}('${escAttr(id)}', '${escAttr(rowId)}')" title="Remove">x</button>
632 + <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.contacts.openEdit${kind}('${escArg(id)}', '${escArg(rowId)}')" title="Edit">Edit</button>
633 + <button type="button" class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.remove${kind === 'Social' ? 'SocialHandle' : kind}('${escArg(id)}', '${escArg(rowId)}')" title="Remove">x</button>
633 634 </span>
634 635 `;
635 636
@@ -661,37 +662,37 @@
661 662 <div class="edit-sub-section">
662 663 <div class="edit-sub-header">
663 664 <strong>Emails</strong>
664 - <button type="button" class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddEmail('${escAttr(id)}')">+ Add</button>
665 + <button type="button" class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddEmail('${escArg(id)}')">+ Add</button>
665 666 </div>
666 667 ${emailSummary}
667 668 </div>
668 669 <div class="edit-sub-section">
669 670 <div class="edit-sub-header">
670 671 <strong>Phones</strong>
671 - <button type="button" class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddPhone('${escAttr(id)}')">+ Add</button>
672 + <button type="button" class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddPhone('${escArg(id)}')">+ Add</button>
672 673 </div>
673 674 ${phoneSummary}
674 675 </div>
675 676 <div class="edit-sub-section">
676 677 <div class="edit-sub-header">
677 678 <strong>Social</strong>
678 - <button type="button" class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddSocial('${escAttr(id)}')">+ Add</button>
679 + <button type="button" class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddSocial('${escArg(id)}')">+ Add</button>
679 680 </div>
680 681 ${socialSummary}
681 682 </div>
682 683 <div class="edit-sub-section">
683 684 <div class="edit-sub-header">
684 685 <strong>Custom Fields</strong>
685 - <button type="button" class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddCustomField('${escAttr(id)}')">+ Add</button>
686 + <button type="button" class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddCustomField('${escArg(id)}')">+ Add</button>
686 687 </div>
687 688 ${customFieldSummary}
688 689 </div>
689 690 <div style="margin-top: 0.5rem;">
690 - <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.contacts.open('${escAttr(id)}')">View Full Detail</button>
691 + <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.contacts.open('${escArg(id)}')">View Full Detail</button>
691 692 </div>
692 693 </div>
693 694 <div style="margin-top: 1rem;">
694 - <button type="button" class="btn btn-danger" onclick="GoingsOn.contacts.deleteContact('${escAttr(id)}')">Delete Contact</button>
695 + <button type="button" class="btn btn-danger" onclick="GoingsOn.contacts.deleteContact('${escArg(id)}')">Delete Contact</button>
695 696 </div>
696 697 `,
697 698 });
@@ -7,6 +7,7 @@
7 7 'use strict';
8 8 const esc = GoingsOn.utils.escapeHtml;
9 9 const escAttr = GoingsOn.utils.escapeAttrValue;
10 + const escArg = GoingsOn.utils.escapeHandlerArg;
10 11
11 12 // ============ Constants ============
12 13
@@ -65,15 +66,15 @@
65 66 // Desktop: drag-paint via mouse events. Touch: tap via onclick, long-press wired post-render.
66 67 const paintHandlers = isTouch
67 68 ? ''
68 - : ` onmousedown="GoingsOn.dayPlan.onPaintStart(event, ${slotIdx}, '${escAttr(slotTimestamp)}')"
69 - onmouseenter="GoingsOn.dayPlan.onPaintMove(event, ${slotIdx}, '${escAttr(slotTimestamp)}')"`;
69 + : ` onmousedown="GoingsOn.dayPlan.onPaintStart(event, ${slotIdx}, '${escArg(slotTimestamp)}')"
70 + onmouseenter="GoingsOn.dayPlan.onPaintMove(event, ${slotIdx}, '${escArg(slotTimestamp)}')"`;
70 71
71 72 slotsHtml += `
72 73 <div class="timeline-slot${isHourStart ? ' hour-start' : ''}"
73 74 data-time="${escAttr(slotTimestamp)}"
74 75 data-hour="${hour}"
75 76 data-slot-index="${slotIdx}"${paintHandlers}
76 - onclick="GoingsOn.dayPlan.onSlotTap(event, ${slotIdx}, '${escAttr(slotTimestamp)}')">
77 + onclick="GoingsOn.dayPlan.onSlotTap(event, ${slotIdx}, '${escArg(slotTimestamp)}')">
77 78 <div class="timeline-time">${isHourStart ? timeStr : ''}</div>
78 79 <div class="timeline-slot-area"></div>
79 80 </div>
@@ -116,7 +117,7 @@
116 117 // Touch: tap opens, long-press opens action sheet (wired post-render). No mouse drag.
117 118 const dragHandler = isTouch
118 119 ? ''
119 - : ` onmousedown="GoingsOn.dayPlan.onItemDragStart(event, '${escAttr(item.id)}', '${escAttr(item.itemType)}')"`;
120 + : ` onmousedown="GoingsOn.dayPlan.onItemDragStart(event, '${escArg(item.id)}', '${escArg(item.itemType)}')"`;
120 121 const titleHint = isTouch ? '' : ' (drag to reschedule)';
121 122 itemsHtml += `
122 123 <div class="timeline-item ${item.itemType} ${blockClass} ${hasConflict ? 'conflict' : ''}"
@@ -124,8 +125,8 @@
124 125 data-id="${escAttr(item.id)}"
125 126 data-type="${escAttr(item.itemType)}"
126 127 data-duration="${duration}"
127 - onclick="GoingsOn.dayPlan.openTimelineItem('${escAttr(item.id)}', '${escAttr(item.itemType)}')"${dragHandler}
128 - onkeydown="GoingsOn.dayPlan.handleTimelineItemKeydown(event, '${escAttr(item.id)}', '${escAttr(item.itemType)}')"
128 + onclick="GoingsOn.dayPlan.openTimelineItem('${escArg(item.id)}', '${escArg(item.itemType)}')"${dragHandler}
129 + onkeydown="GoingsOn.dayPlan.handleTimelineItemKeydown(event, '${escArg(item.id)}', '${escArg(item.itemType)}')"
129 130 title="${escAttr(item.title)}${titleHint}${keyboardHint}"
130 131 tabindex="0" role="button" aria-label="${escAttr(item.title)}${keyboardHint}">
131 132 <div class="timeline-item-title">${esc(item.title)}</div>
@@ -145,16 +146,16 @@
145 146 return `
146 147 <div class="unscheduled-task priority-${task.priority.toLowerCase()}"
147 148 data-id="${escAttr(task.id)}"
148 - onclick="GoingsOn.tasks.openSubtasks('${escAttr(task.id)}')"
149 - onkeydown="GoingsOn.dayPlan.handleUnscheduledTaskKeydown(event, '${escAttr(task.id)}')"
149 + onclick="GoingsOn.tasks.openSubtasks('${escArg(task.id)}')"
150 + onkeydown="GoingsOn.dayPlan.handleUnscheduledTaskKeydown(event, '${escArg(task.id)}')"
150 151 tabindex="0" role="listitem" aria-label="Unscheduled task: ${esc(task.description)} (Press S to schedule)">
151 152 <div class="unscheduled-task-title">${esc(task.description)}</div>
152 153 <div class="unscheduled-task-meta">
153 154 ${task.projectName ? esc(task.projectName) + ' - ' : ''}${task.priority}
154 155 </div>
155 156 <div class="unscheduled-task-actions" onclick="event.stopPropagation()">
156 - <button class="btn btn-sm btn-ghost" onclick="GoingsOn.timeTracking.startTimer('${escAttr(task.id)}')" title="Track Time">Track</button>
157 - <button class="btn btn-sm btn-ghost" onclick="GoingsOn.focusTimer.start('${escAttr(task.id)}')" title="Focus Mode">Focus</button>
157 + <button class="btn btn-sm btn-ghost" onclick="GoingsOn.timeTracking.startTimer('${escArg(task.id)}')" title="Track Time">Track</button>
158 + <button class="btn btn-sm btn-ghost" onclick="GoingsOn.focusTimer.start('${escArg(task.id)}')" title="Focus Mode">Focus</button>
158 159 </div>
159 160 </div>
160 161 `;
@@ -7,6 +7,7 @@
7 7 'use strict';
8 8 const esc = GoingsOn.utils.escapeHtml;
9 9 const escAttr = GoingsOn.utils.escapeAttrValue;
10 + const escArg = GoingsOn.utils.escapeHandlerArg;
10 11
11 12 // ============ Schedule Task Modal ============
12 13
@@ -70,7 +71,7 @@
70 71
71 72 <div class="form-actions">
72 73 <button type="button" class="btn btn-secondary" onclick="GoingsOn.ui.closeModal()">Cancel</button>
73 - <button type="button" class="btn btn-primary" onclick="GoingsOn.dayPlan.scheduleTaskFromModal('${escAttr(id)}')">Schedule Task</button>
74 + <button type="button" class="btn btn-primary" onclick="GoingsOn.dayPlan.scheduleTaskFromModal('${escArg(id)}')">Schedule Task</button>
74 75 </div>
75 76 </div>
76 77 `;
@@ -8,6 +8,7 @@
8 8 'use strict';
9 9 const esc = GoingsOn.utils.escapeHtml;
10 10 const escAttr = GoingsOn.utils.escapeAttrValue;
11 + const escArg = GoingsOn.utils.escapeHandlerArg;
11 12
12 13 // ============ Account Form Builder ============
13 14
@@ -254,8 +255,8 @@
254 255 : '';
255 256
256 257 const editBtn = isOAuth
257 - ? `<button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.reconnectOAuth('${escAttr(a.id)}')">Reconnect</button>`
258 - : `<button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.editAccount('${escAttr(a.id)}')">Edit</button>`;
258 + ? `<button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.reconnectOAuth('${escArg(a.id)}')">Reconnect</button>`
259 + : `<button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.editAccount('${escArg(a.id)}')">Edit</button>`;
259 260
260 261 return `
261 262 <div class="account-row">
@@ -266,12 +267,12 @@
266 267 <div class="account-row-sync">Last sync: ${a.lastSyncFormatted}</div>
267 268 </div>
268 269 ${editBtn}
269 - <button class="btn btn-sm btn-danger" onclick="GoingsOn.emails.deleteAccount('${escAttr(a.id)}')" aria-label="Delete account">&times;</button>
270 + <button class="btn btn-sm btn-danger" onclick="GoingsOn.emails.deleteAccount('${escArg(a.id)}')" aria-label="Delete account">&times;</button>
270 271 </div>
271 272 <div class="account-row-quick">
272 - <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.testAccount('${escAttr(a.id)}')">Test</button>
273 - <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.syncAccount('${escAttr(a.id)}', false)">Sync New</button>
274 - <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.syncAccount('${escAttr(a.id)}', true)">Full Sync</button>
273 + <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.testAccount('${escArg(a.id)}')">Test</button>
274 + <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.syncAccount('${escArg(a.id)}', false)">Sync New</button>
275 + <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.syncAccount('${escArg(a.id)}', true)">Full Sync</button>
275 276 </div>
276 277 </div>
277 278 `;
@@ -364,7 +365,7 @@
364 365 <div class="oauth-block">
365 366 <div class="oauth-buttons">
366 367 ${oauthProviders.map(p => `
367 - <button type="button" class="btn btn-secondary" onclick="GoingsOn.emails.startOAuth('${escAttr(p.id)}')">
368 + <button type="button" class="btn btn-secondary" onclick="GoingsOn.emails.startOAuth('${escArg(p.id)}')">
368 369 ${esc(p.name)}
369 370 </button>
370 371 `).join('')}
@@ -431,7 +432,7 @@
431 432 const content = buildAccountFormHtml({
432 433 formId: 'edit-email-account-form',
433 434 idPrefix: 'edit-acct',
434 - onSubmit: `GoingsOn.emails.updateAccount(event, '${escAttr(id)}')`,
435 + onSubmit: `GoingsOn.emails.updateAccount(event, '${escArg(id)}')`,
435 436 values: account,
436 437 isEdit: true,
437 438 submitLabel: 'Save Changes',
@@ -238,17 +238,17 @@
238 238 return `
239 239 <div class="email-item email-item-with-checkbox ${thread.hasUnread ? 'unread' : ''} ${isSnoozed ? 'email-snoozed' : ''}"
240 240 data-id="${escAttr(e.id)}"
241 - oncontextmenu="GoingsOn.contextMenus.showEmail(event, '${escAttr(e.id)}')"
241 + oncontextmenu="GoingsOn.contextMenus.showEmail(event, '${escArg(e.id)}')"
242 242 data-email-archived="${e.isArchived}" data-email-read="${e.isRead}"
243 243 data-email-snoozed="${isSnoozed}"
244 244 tabindex="0" role="listitem" aria-label="Email from ${esc(e.from)}">
245 245 <div class="email-checkbox-cell" onclick="event.stopPropagation();">
246 246 <input type="checkbox" class="bulk-checkbox" data-id="${escAttr(e.id)}"
247 247 ${isSelected ? 'checked' : ''}
248 - onchange="GoingsOn.emails.toggleSelection('${escAttr(e.id)}', this, event)"
248 + onchange="GoingsOn.emails.toggleSelection('${escArg(e.id)}', this, event)"
249 249 aria-label="Select email">
250 250 </div>
251 - <div class="email-content" onclick="GoingsOn.emails.open('${escAttr(e.id)}')" role="button">
251 + <div class="email-content" onclick="GoingsOn.emails.open('${escArg(e.id)}')" role="button">
252 252 <div class="email-header">
253 253 <span class="email-from">${esc(e.from)}</span>
254 254 ${threadBadge}
@@ -256,9 +256,9 @@
256 256 <span class="email-date">${e.receivedFormatted}</span>
257 257 </div>
258 258 <div class="email-subject">${esc(e.subject)}${labelBadges ? ' ' + labelBadges : ''}</div>
259 - <div class="email-preview">${esc(e.body.substring(0, 100))}...</div>
259 + <div class="email-preview">${esc(e.bodyPreview)}...</div>
260 260 </div>
261 - <button class="btn-icon kebab-btn" onclick="event.stopPropagation(); GoingsOn.contextMenus.showEmail(event, '${escAttr(e.id)}')" title="Actions" aria-label="Email actions">&#x22EE;</button>
261 + <button class="btn-icon kebab-btn" onclick="event.stopPropagation(); GoingsOn.contextMenus.showEmail(event, '${escArg(e.id)}')" title="Actions" aria-label="Email actions">&#x22EE;</button>
262 262 </div>
263 263 `;
264 264 }
@@ -461,14 +461,14 @@
461 461 // Build action buttons for the most recent email
462 462 const latestEmail = threadEmails[threadEmails.length - 1];
463 463 const archiveBtn = latestEmail.isArchived
464 - ? `<button class="btn btn-secondary" onclick="GoingsOn.emails.unarchive('${escAttr(latestEmail.id)}')">Unarchive</button>`
465 - : `<button class="btn btn-secondary" onclick="GoingsOn.emails.archive('${escAttr(latestEmail.id)}')">Archive</button>`;
464 + ? `<button class="btn btn-secondary" onclick="GoingsOn.emails.unarchive('${escArg(latestEmail.id)}')">Unarchive</button>`
465 + : `<button class="btn btn-secondary" onclick="GoingsOn.emails.archive('${escArg(latestEmail.id)}')">Archive</button>`;
466 466
467 467 // Use pre-computed field from backend
468 468 const isSnoozed = latestEmail.isSnoozed;
469 469 const snoozeBtn = isSnoozed
470 - ? `<button class="btn btn-secondary" onclick="GoingsOn.snooze.unsnooze('email', '${escAttr(latestEmail.id)}')">Unsnooze</button>`
471 - : `<button class="btn btn-secondary" onclick="GoingsOn.snooze.openModal('email', '${escAttr(latestEmail.id)}')">Snooze</button>`;
470 + ? `<button class="btn btn-secondary" onclick="GoingsOn.snooze.unsnooze('email', '${escArg(latestEmail.id)}')">Unsnooze</button>`
471 + : `<button class="btn btn-secondary" onclick="GoingsOn.snooze.openModal('email', '${escArg(latestEmail.id)}')">Snooze</button>`;
472 472
473 473 // Look up contact from sender email
474 474 const parsed = GoingsOn.utils.parseEmailAddress(email.from);
@@ -494,7 +494,7 @@
494 494 <span class="email-sender-name">${esc(senderContact.displayName || senderContact.display_name)}</span>
495 495 ${company ? `<span class="email-sender-company">${company}</span>` : ''}
496 496 </div>
497 - <button class="btn btn-sm btn-secondary" onclick="GoingsOn.ui.closeModal(); GoingsOn.contacts.open('${escAttr(senderContact.id)}')">View Contact</button>
497 + <button class="btn btn-sm btn-secondary" onclick="GoingsOn.ui.closeModal(); GoingsOn.contacts.open('${escArg(senderContact.id)}')">View Contact</button>
498 498 </div>
499 499 `;
500 500 } else if (parsed.email) {
@@ -504,7 +504,7 @@
504 504 <div class="email-sender-info">
505 505 <span class="email-sender-name">${esc(parsed.name || parsed.email)}</span>
506 506 </div>
507 - <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.createContactFromSender('${escAttr(id)}')">+ Save Contact</button>
507 + <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.createContactFromSender('${escArg(id)}')">+ Save Contact</button>
508 508 </div>
509 509 `;
510 510 }
@@ -523,8 +523,8 @@
523 523 <span class="attachment-filename email-attachment-name"
524 524 title="${escAttr(a.filename)}">${esc(a.filename)}</span>
525 525 <span class="email-attachment-size">${esc(a.sizeFormatted)}</span>
526 - <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.openBlob('${escAttr(a.blobHash)}', '${escArg(a.filename)}')" title="Open">Open</button>
527 - <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.saveBlob('${escAttr(a.blobHash)}', '${escArg(a.filename)}')" title="Save">Save</button>
526 + <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.openBlob('${escArg(a.blobHash)}', '${escArg(a.filename)}')" title="Open">Open</button>
527 + <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.saveBlob('${escArg(a.blobHash)}', '${escArg(a.filename)}')" title="Save">Save</button>
528 528 </div>
529 529 `;
530 530 }).join('');
@@ -548,7 +548,7 @@
548 548 const truncatedNotice = e.bodyTruncated
549 549 ? `<div class="email-body-truncated" id="email-trunc-${escAttr(e.id)}">
550 550 <span>Message truncated.</span>
551 - <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.loadFullBody('${escAttr(e.id)}')">Load full message</button>
551 + <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.loadFullBody('${escArg(e.id)}')">Load full message</button>
552 552 </div>`
553 553 : '';
554 554
@@ -586,34 +586,34 @@
586 586 ${threadContent}
587 587 </div>
588 588 <div class="form-actions email-actions-bar">
589 - <button class="btn btn-primary" onclick="GoingsOn.emails.reply('${escAttr(latestEmail.id)}')">Reply</button>
590 - <button class="btn btn-secondary" onclick="GoingsOn.emails.replyAll('${escAttr(latestEmail.id)}')">Reply All</button>
591 - <button class="btn btn-secondary" onclick="GoingsOn.emails.forward('${escAttr(latestEmail.id)}')">Forward</button>
592 - <button class="btn btn-secondary text-accent-red" onclick="GoingsOn.emails.delete('${escAttr(latestEmail.id)}')">Delete</button>
589 + <button class="btn btn-primary" onclick="GoingsOn.emails.reply('${escArg(latestEmail.id)}')">Reply</button>
590 + <button class="btn btn-secondary" onclick="GoingsOn.emails.replyAll('${escArg(latestEmail.id)}')">Reply All</button>
591 + <button class="btn btn-secondary" onclick="GoingsOn.emails.forward('${escArg(latestEmail.id)}')">Forward</button>
592 + <button class="btn btn-secondary text-accent-red" onclick="GoingsOn.emails.delete('${escArg(latestEmail.id)}')">Delete</button>
593 593 ${archiveBtn}
594 594 ${snoozeBtn}
595 - <button class="btn btn-secondary" onclick="GoingsOn.emails.createTaskFromEmail('${escAttr(latestEmail.id)}')">Create Task</button>
595 + <button class="btn btn-secondary" onclick="GoingsOn.emails.createTaskFromEmail('${escArg(latestEmail.id)}')">Create Task</button>
596 596 <div class="dropdown" style="position: relative;">
597 597 <button class="btn btn-secondary" onclick="this.nextElementSibling.classList.toggle('show')">
598 598 Actions ▾
599 599 </button>
600 600 <div class="dropdown-menu">
601 - <button class="dropdown-item" onclick="GoingsOn.emails.createTaskFromEmail('${escAttr(latestEmail.id)}'); this.parentElement.classList.remove('show');">
601 + <button class="dropdown-item" onclick="GoingsOn.emails.createTaskFromEmail('${escArg(latestEmail.id)}'); this.parentElement.classList.remove('show');">
602 602 Convert to Task
603 603 </button>
604 - <button class="dropdown-item" onclick="GoingsOn.emails.createEventFromEmail('${escAttr(latestEmail.id)}'); this.parentElement.classList.remove('show');">
604 + <button class="dropdown-item" onclick="GoingsOn.emails.createEventFromEmail('${escArg(latestEmail.id)}'); this.parentElement.classList.remove('show');">
605 605 Convert to Event
606 606 </button>
607 - <button class="dropdown-item" onclick="GoingsOn.emails.editLabels('${escAttr(latestEmail.id)}', ${escAttrVal(JSON.stringify(latestEmail.labels || []))}); this.parentElement.classList.remove('show');">
607 + <button class="dropdown-item" onclick="GoingsOn.emails.editLabels('${escArg(latestEmail.id)}', ${escAttrVal(JSON.stringify(latestEmail.labels || []))}); this.parentElement.classList.remove('show');">
608 608 Edit Labels
609 609 </button>
610 - <button class="dropdown-item" onclick="GoingsOn.emails.moveToFolder('${escAttr(latestEmail.id)}'); this.parentElement.classList.remove('show');">
610 + <button class="dropdown-item" onclick="GoingsOn.emails.moveToFolder('${escArg(latestEmail.id)}'); this.parentElement.classList.remove('show');">
611 611 Move to Folder
612 612 </button>
613 613 </div>
614 614 </div>
615 615 <div class="flex-1"></div>
616 - <button class="btn btn-secondary" onclick="GoingsOn.emails.openInBrowser('${escAttr(latestEmail.id)}')" title="Open in browser">Open in Browser</button>
616 + <button class="btn btn-secondary" onclick="GoingsOn.emails.openInBrowser('${escArg(latestEmail.id)}')" title="Open in browser">Open in Browser</button>
617 617 </div>
618 618 </div>
619 619 `;
@@ -1059,7 +1059,7 @@
1059 1059 const subject = d.subject || '(no subject)';
1060 1060 return `
1061 1061 <div class="email-item email-draft-item"
1062 - onclick="GoingsOn.emails.openDraft('${escAttr(d.id)}')">
1062 + onclick="GoingsOn.emails.openDraft('${escArg(d.id)}')">
1063 1063 <div class="email-draft-subject">${esc(subject)}</div>
1064 1064 <div class="email-draft-meta">To: ${esc(to)} · ${d.receivedFormatted}</div>
1065 1065 </div>
@@ -1206,7 +1206,7 @@
1206 1206 async function editLabels(emailId, currentLabels) {
1207 1207 const existing = await GoingsOn.api.emails.listLabels();
1208 1208 const content = `
1209 - <form id="label-form" onsubmit="event.preventDefault(); GoingsOn.emails._saveLabels('${escAttr(emailId)}');">
1209 + <form id="label-form" onsubmit="event.preventDefault(); GoingsOn.emails._saveLabels('${escArg(emailId)}');">
1210 1210 <div class="form-group">
1211 1211 <label class="form-label">Labels (comma-separated)</label>
1212 1212 <input type="text" class="form-input" id="label-input" value="${escAttrVal((currentLabels || []).join(', '))}"
@@ -1252,7 +1252,7 @@
1252 1252 const folders = await GoingsOn.api.emails.listFolders();
1253 1253 // Also try to get IMAP folders from account
1254 1254 const content = `
1255 - <form onsubmit="event.preventDefault(); GoingsOn.emails._doMoveToFolder('${escAttr(emailId)}');">
1255 + <form onsubmit="event.preventDefault(); GoingsOn.emails._doMoveToFolder('${escArg(emailId)}');">
1256 1256 <div class="form-group">
1257 1257 <label class="form-label">Move to folder</label>
1258 1258 <input type="text" class="form-input" id="move-folder-input" placeholder="INBOX, Archive, Sent, ..."
@@ -1355,7 +1355,7 @@
1355 1355 if (emailScroller) { emailScroller.destroy(); emailScroller = null; }
1356 1356 container.innerHTML = response.results.map(r => `
1357 1357 <div class="email-item" data-id="${escAttr(r.id)}"
1358 - onclick="GoingsOn.emails.open('${escAttr(r.id)}')"
1358 + onclick="GoingsOn.emails.open('${escArg(r.id)}')"
1359 1359 tabindex="0" role="listitem">
1360 1360 <div class="email-content">
1361 1361 <div class="email-header">
@@ -7,6 +7,7 @@
7 7 'use strict';
8 8 const esc = GoingsOn.utils.escapeHtml;
9 9 const escAttr = GoingsOn.utils.escapeAttrValue;
10 + const escArg = GoingsOn.utils.escapeHandlerArg;
10 11 const escAttrVal = GoingsOn.utils.escapeAttrValue;
11 12
12 13 let currentMonthDate = new Date();
@@ -121,13 +122,13 @@
121 122 if (!isCurrentMonth) classes.push('other-month');
122 123 if (isToday) classes.push('today');
123 124
124 - html += `<div class="${classes.join(' ')}" data-date="${escAttr(dateKey)}" onclick="GoingsOn.eventsCalendar.toggleDayDetail('${escAttr(dateKey)}')">`;
125 + html += `<div class="${classes.join(' ')}" data-date="${escAttr(dateKey)}" onclick="GoingsOn.eventsCalendar.toggleDayDetail('${escArg(dateKey)}')">`;
125 126 html += `<div class="cal-month-cell-header"><span class="cal-day-number">${cursor.getDate()}</span></div>`;
126 127
127 128 const maxShow = 3;
128 129 dayEvents.slice(0, maxShow).forEach(e => {
129 130 const blockClass = e.blockType ? `block-${e.blockType}` : '';
130 - html += `<div class="cal-event-chip ${blockClass}" onclick="event.stopPropagation(); GoingsOn.events.open('${escAttr(e.id)}')" title="${escAttrVal(e.title)}">${esc(truncate(e.title, 18))}</div>`;
131 + html += `<div class="cal-event-chip ${blockClass}" onclick="event.stopPropagation(); GoingsOn.events.open('${escArg(e.id)}')" title="${escAttrVal(e.title)}">${esc(truncate(e.title, 18))}</div>`;
131 132 });
132 133 if (dayEvents.length > maxShow) {
133 134 html += `<div class="cal-event-more">+${dayEvents.length - maxShow} more</div>`;
@@ -165,7 +166,7 @@
165 166 } else {
166 167 dayEvents.forEach(e => {
167 168 const blockClass = e.blockType ? `block-${e.blockType}` : '';
168 - html += `<div class="cal-day-detail-event ${blockClass}" onclick="GoingsOn.events.open('${escAttr(e.id)}')">
169 + html += `<div class="cal-day-detail-event ${blockClass}" onclick="GoingsOn.events.open('${escArg(e.id)}')">
169 170 <span class="cal-detail-time">${esc(e.timeFormatted)}</span>
170 171 <span class="cal-detail-title">${esc(e.title)}</span>
171 172 ${e.location ? `<span class="cal-detail-location">${esc(e.location)}</span>` : ''}
@@ -248,7 +249,7 @@
248 249 html += '<div class="cal-week-allday-cell">';
249 250 dayEvts.forEach(e => {
250 251 const blockClass = e.blockType ? `block-${e.blockType}` : '';
251 - html += `<div class="cal-event-chip ${blockClass}" onclick="GoingsOn.events.open('${escAttr(e.id)}')" title="${escAttrVal(e.title)}">${esc(truncate(e.title, 14))}</div>`;
252 + html += `<div class="cal-event-chip ${blockClass}" onclick="GoingsOn.events.open('${escArg(e.id)}')" title="${escAttrVal(e.title)}">${esc(truncate(e.title, 14))}</div>`;
252 253 });
253 254 html += '</div>';
254 255 adCursor.setDate(adCursor.getDate() + 1);
@@ -292,7 +293,7 @@
292 293 const heightPx = Math.max((durationMinutes / 15) * SLOT_HEIGHT, SLOT_HEIGHT);
293 294 const blockClass = e.blockType ? `block-${e.blockType}` : '';
294 295
295 - html += `<div class="cal-week-event ${blockClass}" style="top: ${topPx}px; height: ${heightPx}px;" onclick="GoingsOn.events.open('${escAttr(e.id)}')" title="${escAttrVal(e.title + ' ' + e.timeFormatted)}">
296 + html += `<div class="cal-week-event ${blockClass}" style="top: ${topPx}px; height: ${heightPx}px;" onclick="GoingsOn.events.open('${escArg(e.id)}')" title="${escAttrVal(e.title + ' ' + e.timeFormatted)}">
296 297 <div class="cal-week-event-title">${esc(truncate(e.title, 20))}</div>
297 298 <div class="cal-week-event-time">${esc(e.timeFormatted)}</div>
298 299 </div>`;
@@ -346,7 +347,7 @@
346 347 html += '<div class="cal-mobile-allday">';
347 348 allDayEvts.forEach(e => {
348 349 const blockClass = e.blockType ? `block-${e.blockType}` : '';
349 - html += `<div class="cal-event-chip ${blockClass}" onclick="GoingsOn.events.open('${escAttr(e.id)}')">${esc(truncate(e.title, 30))}</div>`;
350 + html += `<div class="cal-event-chip ${blockClass}" onclick="GoingsOn.events.open('${escArg(e.id)}')">${esc(truncate(e.title, 30))}</div>`;
350 351 });
351 352 html += '</div>';
352 353 }
@@ -373,7 +374,7 @@
373 374 const topPx = ((startMinutes - HOURS_START * 60) / 15) * SLOT_HEIGHT;
374 375 const heightPx = Math.max((durationMinutes / 15) * SLOT_HEIGHT, SLOT_HEIGHT);
375 376 const blockClass = e.blockType ? `block-${e.blockType}` : '';
376 - html += `<div class="cal-week-event ${blockClass}" style="top: ${topPx}px; height: ${heightPx}px;" onclick="GoingsOn.events.open('${escAttr(e.id)}')">
377 + html += `<div class="cal-week-event ${blockClass}" style="top: ${topPx}px; height: ${heightPx}px;" onclick="GoingsOn.events.open('${escArg(e.id)}')">
377 378 <div class="cal-week-event-title">${esc(truncate(e.title, 30))}</div>
378 379 <div class="cal-week-event-time">${esc(e.timeFormatted)}</div>
379 380 </div>`;
@@ -9,6 +9,7 @@
9 9 'use strict';
10 10 const esc = GoingsOn.utils.escapeHtml;
11 11 const escAttr = GoingsOn.utils.escapeAttrValue;
12 + const escArg = GoingsOn.utils.escapeHandlerArg;
12 13
13 14 // ============ Reminder Presets ============
14 15
@@ -423,15 +424,15 @@
423 424 return `
424 425 <div class="event-row-virtual event-recurring"
425 426 data-id="${escAttr(e.id)}"
426 - onclick="GoingsOn.events.open('${escAttr(e.id)}')"
427 - oncontextmenu="GoingsOn.contextMenus.showEvent(event, '${escAttr(e.id)}')"
427 + onclick="GoingsOn.events.open('${escArg(e.id)}')"
428 + oncontextmenu="GoingsOn.contextMenus.showEvent(event, '${escArg(e.id)}')"
428 429 tabindex="0" role="row">
429 430 <div class="event-cell event-cell-date"><span class="event-recurrence-pattern">${esc(patternLabel)}</span></div>
430 431 <div class="event-cell event-cell-time">${e.timeFormatted}</div>
431 432 <div class="event-cell event-cell-title">${esc(displayTitle)}</div>
432 433 <div class="event-cell event-cell-location">${e.location ? esc(e.location) : '-'}</div>
433 434 <div class="event-cell" style="text-align: right;" onclick="event.stopPropagation();">
434 - <button class="btn-icon kebab-btn" onclick="event.stopPropagation(); GoingsOn.contextMenus.showEvent(event, '${escAttr(e.id)}')" title="Actions" aria-label="Event actions">&#x22EE;</button>
435 + <button class="btn-icon kebab-btn" onclick="event.stopPropagation(); GoingsOn.contextMenus.showEvent(event, '${escArg(e.id)}')" title="Actions" aria-label="Event actions">&#x22EE;</button>
435 436 </div>
436 437 </div>
437 438 `;
@@ -459,12 +460,12 @@
459 460 ${dateHeader}
460 461 <div class="event-row-virtual ${e.isPast || isPast ? 'event-past' : ''}"
461 462 data-id="${escAttr(e.id)}"
462 - onclick="GoingsOn.events.open('${escAttr(e.id)}')"
463 - oncontextmenu="GoingsOn.contextMenus.showEvent(event, '${escAttr(e.id)}')"
463 + onclick="GoingsOn.events.open('${escArg(e.id)}')"
464 + oncontextmenu="GoingsOn.contextMenus.showEvent(event, '${escArg(e.id)}')"
464 465 tabindex="0" role="row">
465 466 <div class="event-cell event-cell--shrink">
466 467 <input type="checkbox" class="bulk-checkbox event-select-cb" data-id="${escAttr(e.id)}"
467 - onclick="event.stopPropagation(); GoingsOn.events.toggleEventSelection('${escAttr(e.id)}', event)"
468 + onclick="event.stopPropagation(); GoingsOn.events.toggleEventSelection('${escArg(e.id)}', event)"
468 469 aria-label="Select event">
469 470 </div>
470 471 <div class="event-cell event-cell-date">
@@ -475,7 +476,7 @@
475 476 <div class="event-cell event-cell-title">${esc(displayTitle)}</div>
476 477 <div class="event-cell event-cell-location">${e.location ? esc(e.location) : '-'}</div>
477 478 <div class="event-cell" style="text-align: right;" onclick="event.stopPropagation();">
478 - <button class="btn-icon kebab-btn" onclick="event.stopPropagation(); GoingsOn.contextMenus.showEvent(event, '${escAttr(e.id)}')" title="Actions" aria-label="Event actions">&#x22EE;</button>
479 + <button class="btn-icon kebab-btn" onclick="event.stopPropagation(); GoingsOn.contextMenus.showEvent(event, '${escArg(e.id)}')" title="Actions" aria-label="Event actions">&#x22EE;</button>
479 480 </div>
480 481 </div>
481 482 `;
@@ -594,8 +595,8 @@
594 595 ? GoingsOn.snooze.formatTime(event.snoozedUntil)
595 596 : null;
596 597 const snoozeButton = event.isSnoozed
597 - ? `<button class="btn btn-secondary" onclick="GoingsOn.snooze.unsnooze('event', '${escAttr(id)}')">Unsnooze</button>`
598 - : `<button class="btn btn-secondary" onclick="GoingsOn.snooze.openModal('event', '${escAttr(id)}')">Snooze</button>`;
598 + ? `<button class="btn btn-secondary" onclick="GoingsOn.snooze.unsnooze('event', '${escArg(id)}')">Unsnooze</button>`
599 + : `<button class="btn btn-secondary" onclick="GoingsOn.snooze.openModal('event', '${escArg(id)}')">Snooze</button>`;
599 600 const snoozeStatus = snoozeUntilLabel
600 601 ? `<p><strong>Snoozed until:</strong> ${esc(snoozeUntilLabel)}</p>`
601 602 : '';
@@ -616,10 +617,10 @@
616 617 ${reminderStatus}
617 618 </div>
618 619 <div class="form-actions">
619 - <button class="btn btn-secondary text-accent-red" onclick="GoingsOn.events.delete('${escAttr(id)}')">Delete</button>
620 + <button class="btn btn-secondary text-accent-red" onclick="GoingsOn.events.delete('${escArg(id)}')">Delete</button>
620 621 <div class="form-actions-spacer"></div>
621 622 ${snoozeButton}
622 - <button class="btn btn-secondary" onclick="GoingsOn.events.openEdit('${escAttr(id)}')">Edit</button>
623 + <button class="btn btn-secondary" onclick="GoingsOn.events.openEdit('${escArg(id)}')">Edit</button>
623 624 <button class="btn btn-secondary" onclick="GoingsOn.ui.closeModal()">Close</button>
624 625 </div>
625 626 `;
@@ -7,7 +7,8 @@
7 7 'use strict';
8 8
9 9 const esc = GoingsOn.utils.escapeHtml;
10 - const escAttr = GoingsOn.utils.escapeAttrValue;
10 + const escAttr = GoingsOn.utils.escapeAttrValue;
11 + const escArg = GoingsOn.utils.escapeHandlerArg;
11 12
12 13 /**
13 14 * Truncate a string with an ellipsis.
@@ -49,7 +50,7 @@ function renderHeatMap(r) {
49 50 if (day.isVacation) classes.push('vacation');
50 51 classes.push(`intensity-${day.intensity}`);
51 52
52 - html += `<div class="${classes.join(' ')}" onclick="GoingsOn.monthlyReview.showDaySummary('${escAttr(day.date)}')" title="${day.completedCount} completed, ${day.eventCount} events" tabindex="0" role="button">`;
53 + html += `<div class="${classes.join(' ')}" onclick="GoingsOn.monthlyReview.showDaySummary('${escArg(day.date)}')" title="${day.completedCount} completed, ${day.eventCount} events" tabindex="0" role="button">`;
53 54 html += `<span class="month-heatmap-day-number">${day.dayNumber}</span>`;
54 55 if (day.completedCount > 0 || day.eventCount > 0) {
55 56 html += '<div class="month-heatmap-dots">';
@@ -209,9 +210,9 @@ function renderGoalItem(goal, position) {
209 210 let html = `<div class="scope-slot month-goal-item filled ${goal.status}">`;
210 211 html += `<span class="scope-slot-label">Goal #${position}</span>`;
211 212 html += `<div class="month-goal-body">`;
212 - html += `<button class="btn-icon month-goal-status-btn" onclick="GoingsOn.monthlyReview.cycleGoalStatus('${escAttr(goal.id)}')" title="Cycle status">${icon}</button>`;
213 + html += `<button class="btn-icon month-goal-status-btn" onclick="GoingsOn.monthlyReview.cycleGoalStatus('${escArg(goal.id)}')" title="Cycle status">${icon}</button>`;
213 214 html += `<span class="scope-slot-title month-goal-text">${esc(goal.text)}</span>`;
214 - html += `<button class="btn-icon month-goal-delete-btn" onclick="GoingsOn.monthlyReview.deleteGoal('${escAttr(goal.id)}')" title="Delete goal">&#x2715;</button>`;
215 + html += `<button class="btn-icon month-goal-delete-btn" onclick="GoingsOn.monthlyReview.deleteGoal('${escArg(goal.id)}')" title="Delete goal">&#x2715;</button>`;
215 216 html += `</div>`;
216 217 html += '</div>';
217 218 return html;
@@ -219,7 +220,7 @@ function renderGoalItem(goal, position) {
219 220
220 221 function renderEmptyGoalSlot(month, position) {
221 222 return `<div class="scope-slot month-goal-item empty"
222 - onclick="GoingsOn.monthlyReview.addGoal('${escAttr(month)}', ${position})"
223 + onclick="GoingsOn.monthlyReview.addGoal('${escArg(month)}', ${position})"
223 224 tabindex="0" role="button">
224 225 <span class="scope-slot-label">Goal #${position}</span>
225 226 <span class="scope-slot-empty">+ Add goal</span>
@@ -254,7 +254,8 @@ async function showDaySummary(dateStr) {
254 254 try {
255 255 const day = await GoingsOn.api.dayPlanning.getDay(dateStr);
256 256 const esc = GoingsOn.utils.escapeHtml;
257 - const escAttr = GoingsOn.utils.escapeAttrValue;
257 + const escAttr = GoingsOn.utils.escapeAttrValue;
258 + const escArg = GoingsOn.utils.escapeHandlerArg;
258 259
259 260 const dateDisplay = new Date(dateStr + 'T12:00:00')
260 261 .toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' });
@@ -287,7 +288,7 @@ async function showDaySummary(dateStr) {
287 288 html += `<p class="day-summary-empty">No scheduled items</p>`;
288 289 }
289 290
290 - html += `<button class="btn btn-primary day-summary-go-btn" onclick="GoingsOn.monthlyReview.navigateToDay('${escAttr(dateStr)}'); GoingsOn.ui.closeModal();">Go to Day</button>`;
291 + html += `<button class="btn btn-primary day-summary-go-btn" onclick="GoingsOn.monthlyReview.navigateToDay('${escArg(dateStr)}'); GoingsOn.ui.closeModal();">Go to Day</button>`;
291 292 html += `</div>`;
292 293
293 294 GoingsOn.ui.openModal(dateDisplay, html);
@@ -7,6 +7,7 @@
7 7 'use strict';
8 8 const esc = GoingsOn.utils.escapeHtml;
9 9 const escAttr = GoingsOn.utils.escapeAttrValue;
10 + const escArg = GoingsOn.utils.escapeHandlerArg;
10 11
11 12 /**
12 13 * Empty state for a dashboard column. Same canonical primitive as the rest
@@ -66,7 +67,7 @@
66 67 const progress = t.subtaskProgress ?? 0;
67 68
68 69 return `
69 - <div class="card card--list-item" onclick="GoingsOn.tasks.openSubtasks('${escAttr(t.id)}')"
70 + <div class="card card--list-item" onclick="GoingsOn.tasks.openSubtasks('${escArg(t.id)}')"
70 71 tabindex="0" role="button" aria-label="Open task ${esc(t.description)}">
71 72 <div class="dashboard-item-title">
72 73 ${esc(t.description)}
@@ -91,7 +92,7 @@
91 92 eventsEl.innerHTML = renderDashboardEmpty('No events linked yet.', 'events');
92 93 } else {
93 94 eventsEl.innerHTML = events.map(e => `
94 - <div class="card card--list-item" onclick="GoingsOn.events.open('${escAttr(e.id)}')"
95 + <div class="card card--list-item" onclick="GoingsOn.events.open('${escArg(e.id)}')"
95 96 tabindex="0" role="button" aria-label="Open event ${esc(e.title)}">
96 97 <div class="dashboard-item-title">${esc(e.title)}</div>
97 98 <div class="dashboard-item-meta">
@@ -106,7 +107,7 @@
106 107 emailsEl.innerHTML = renderDashboardEmpty('No emails linked yet.', 'emails');
107 108 } else {
108 109 emailsEl.innerHTML = emails.map(e => `
109 - <div class="card card--list-item ${e.is_read ? '' : 'unread'}" onclick="GoingsOn.emails.open('${escAttr(e.id)}')"
110 + <div class="card card--list-item ${e.is_read ? '' : 'unread'}" onclick="GoingsOn.emails.open('${escArg(e.id)}')"
110 111 tabindex="0" role="button" aria-label="Open email ${esc(e.subject)}">
111 112 <div class="dashboard-item-title">${esc(e.subject)}</div>
112 113 <div class="dashboard-item-meta">
@@ -122,11 +123,11 @@
122 123 attachmentsEl.innerHTML = renderDashboardEmpty(
123 124 'No attachments yet.',
124 125 'attachments',
125 - { label: 'Attach File', onClick: `GoingsOn.attachments.pickAndAttach(null, '${escAttr(projectId)}')` },
126 + { label: 'Attach File', onClick: `GoingsOn.attachments.pickAndAttach(null, '${escArg(projectId)}')` },
126 127 );
127 128 } else {
128 129 attachmentsEl.innerHTML = attachments.map(a => `
129 - <div class="card card--list-item" onclick="GoingsOn.attachments.openPanel(null, '${escAttr(projectId)}')"
130 + <div class="card card--list-item" onclick="GoingsOn.attachments.openPanel(null, '${escArg(projectId)}')"
130 131 tabindex="0" role="button" aria-label="View attachment ${esc(a.filename)}">
131 132 <div class="dashboard-item-title">${esc(a.filename)}</div>
132 133 <div class="dashboard-item-meta">${esc(a.fileSizeFormatted)}</div>
@@ -194,10 +195,10 @@
194 195 <span class="milestone-progress-text">${m.completedCount}/${m.taskCount}</span>
195 196 </div>
196 197 <div class="milestone-actions">
197 - ${idx > 0 ? `<button class="btn btn-sm btn-icon milestone-reorder-btn" onclick="event.stopPropagation(); GoingsOn.projects.moveMilestone('${escAttr(m.id)}', -1)" title="Move up">\u25B2</button>` : '<span class="btn btn-sm btn-icon milestone-reorder-btn" style="visibility:hidden">\u25B2</span>'}
198 - ${idx < openMilestones.length - 1 ? `<button class="btn btn-sm btn-icon milestone-reorder-btn" onclick="event.stopPropagation(); GoingsOn.projects.moveMilestone('${escAttr(m.id)}', 1)" title="Move down">\u25BC</button>` : '<span class="btn btn-sm btn-icon milestone-reorder-btn" style="visibility:hidden">\u25BC</span>'}
199 - <button class="btn btn-sm btn-secondary" onclick="GoingsOn.projects.openEditMilestone('${escAttr(m.id)}')">Edit</button>
200 - <button class="btn btn-sm btn-secondary" onclick="GoingsOn.projects.deleteMilestone('${escAttr(m.id)}')">Delete</button>
198 + ${idx > 0 ? `<button class="btn btn-sm btn-icon milestone-reorder-btn" onclick="event.stopPropagation(); GoingsOn.projects.moveMilestone('${escArg(m.id)}', -1)" title="Move up">\u25B2</button>` : '<span class="btn btn-sm btn-icon milestone-reorder-btn" style="visibility:hidden">\u25B2</span>'}
199 + ${idx < openMilestones.length - 1 ? `<button class="btn btn-sm btn-icon milestone-reorder-btn" onclick="event.stopPropagation(); GoingsOn.projects.moveMilestone('${escArg(m.id)}', 1)" title="Move down">\u25BC</button>` : '<span class="btn btn-sm btn-icon milestone-reorder-btn" style="visibility:hidden">\u25BC</span>'}
200 + <button class="btn btn-sm btn-secondary" onclick="GoingsOn.projects.openEditMilestone('${escArg(m.id)}')">Edit</button>
201 + <button class="btn btn-sm btn-secondary" onclick="GoingsOn.projects.deleteMilestone('${escArg(m.id)}')">Delete</button>
201 202 </div>
202 203 </div>
203 204 `).join('')}
@@ -218,8 +219,8 @@
218 219 <span class="milestone-complete-badge">Complete</span>
219 220 </div>
220 221 <div class="milestone-actions">
221 - <button class="btn btn-sm btn-secondary" onclick="GoingsOn.projects.openEditMilestone('${escAttr(m.id)}')">Edit</button>
222 - <button class="btn btn-sm btn-secondary" onclick="GoingsOn.projects.deleteMilestone('${escAttr(m.id)}')">Delete</button>
222 + <button class="btn btn-sm btn-secondary" onclick="GoingsOn.projects.openEditMilestone('${escArg(m.id)}')">Edit</button>
223 + <button class="btn btn-sm btn-secondary" onclick="GoingsOn.projects.deleteMilestone('${escArg(m.id)}')">Delete</button>
223 224 </div>
224 225 </div>
225 226 `).join('')}
@@ -9,6 +9,7 @@
9 9 'use strict';
10 10 const esc = GoingsOn.utils.escapeHtml;
11 11 const escAttr = GoingsOn.utils.escapeAttrValue;
12 + const escArg = GoingsOn.utils.escapeHandlerArg;
12 13
13 14 // ============ Form Field Definitions ============
14 15
@@ -95,12 +96,12 @@
95 96 }
96 97
97 98 grid.innerHTML = projects.map(p => `
98 - <div class="card project-card" onclick="GoingsOn.projects.open('${escAttr(p.id)}')"
99 - oncontextmenu="GoingsOn.contextMenus.showProject(event, '${escAttr(p.id)}')"
99 + <div class="card project-card" onclick="GoingsOn.projects.open('${escArg(p.id)}')"
100 + oncontextmenu="GoingsOn.contextMenus.showProject(event, '${escArg(p.id)}')"
100 101 tabindex="0" role="button" aria-label="Open project ${esc(p.name)}">
101 102 <div class="card-header">
102 103 <h3 class="card-title">${esc(p.name)}</h3>
103 - <button class="btn-icon kebab-btn" style="opacity: 1;" onclick="event.stopPropagation(); GoingsOn.contextMenus.showProject(event, '${escAttr(p.id)}')" title="Actions" aria-label="Project actions">&#x22EE;</button>
104 + <button class="btn-icon kebab-btn" style="opacity: 1;" onclick="event.stopPropagation(); GoingsOn.contextMenus.showProject(event, '${escArg(p.id)}')" title="Actions" aria-label="Project actions">&#x22EE;</button>
104 105 </div>
105 106 <div class="card-description markdown-content">${p.descriptionHtml || ''}</div>
106 107 <div class="card-meta">
@@ -166,7 +167,7 @@
166 167 onSubmit: (data) => update(id, data),
167 168 extraContent: `
168 169 <div style="margin-bottom: 1rem;">
169 - <button type="button" class="btn btn-danger" onclick="GoingsOn.projects.delete('${escAttr(id)}')">Delete Project</button>
170 + <button type="button" class="btn btn-danger" onclick="GoingsOn.projects.delete('${escArg(id)}')">Delete Project</button>
170 171 </div>
171 172 `,
172 173 });
@@ -10,6 +10,7 @@
10 10 'use strict';
11 11 const esc = GoingsOn.utils.escapeHtml;
12 12 const escAttr = GoingsOn.utils.escapeAttrValue;
13 + const escArg = GoingsOn.utils.escapeHandlerArg;
13 14
14 15 // DOM single-value filter controls <-> backend ViewFilters (Vec-valued) enums.
15 16 const STATUS_TO_ENUM = { pending: 'Pending', started: 'Started', completed: 'Completed' };
@@ -66,9 +67,9 @@
66 67 const pinLabel = v.isPinned ? 'Unpin' : 'Pin';
67 68 html += `
68 69 <div class="saved-views-row">
69 - <button type="button" class="btn btn-sm saved-views-apply" onclick="GoingsOn.savedViews._apply('${escAttr(v.id)}')">${esc(v.name)}${v.isPinned ? ' •' : ''}</button>
70 - <button type="button" class="btn btn-sm" onclick="GoingsOn.savedViews._togglePin('${escAttr(v.id)}')">${pinLabel}</button>
71 - <button type="button" class="btn btn-sm btn-danger" onclick="GoingsOn.savedViews._delete('${escAttr(v.id)}')">Delete</button>
70 + <button type="button" class="btn btn-sm saved-views-apply" onclick="GoingsOn.savedViews._apply('${escArg(v.id)}')">${esc(v.name)}${v.isPinned ? ' •' : ''}</button>
71 + <button type="button" class="btn btn-sm" onclick="GoingsOn.savedViews._togglePin('${escArg(v.id)}')">${pinLabel}</button>
72 + <button type="button" class="btn btn-sm btn-danger" onclick="GoingsOn.savedViews._delete('${escArg(v.id)}')">Delete</button>
72 73 </div>`;
73 74 }
74 75 html += `</div>`;
@@ -7,6 +7,7 @@
7 7 'use strict';
8 8 const esc = GoingsOn.utils.escapeHtml;
9 9 const escAttr = GoingsOn.utils.escapeAttrValue;
10 + const escArg = GoingsOn.utils.escapeHandlerArg;
10 11
11 12 const ITEM_LABEL = { task: 'Task', email: 'Email', event: 'Event' };
12 13
@@ -38,7 +39,7 @@
38 39
39 40 for (const opt of options) {
40 41 optionsHtml += `
41 - <button class="snooze-option" onclick="GoingsOn.snooze.snooze('${escAttr(itemType)}', '${escAttr(id)}', '${escAttr(opt.time)}')">
42 + <button class="snooze-option" onclick="GoingsOn.snooze.snooze('${escArg(itemType)}', '${escArg(id)}', '${escArg(opt.time)}')">
42 43 <span class="snooze-option-label">${esc(opt.label)}</span>
43 44 <span class="snooze-option-time">${esc(opt.formatted)}</span>
44 45 </button>
@@ -53,7 +54,7 @@
53 54 <input type="datetime-local" id="snooze-custom-datetime" class="form-input"
54 55 min="${minCustom}">
55 56 <button class="btn btn-primary" style="margin-top: 0.5rem; width: 100%;"
56 - onclick="GoingsOn.snooze.snoozeCustom('${escAttr(itemType)}', '${escAttr(id)}')">
57 + onclick="GoingsOn.snooze.snoozeCustom('${escArg(itemType)}', '${escArg(id)}')">
57 58 Snooze Until Custom Time
58 59 </button>
59 60 </div>
@@ -8,6 +8,7 @@
8 8 'use strict';
9 9 const esc = GoingsOn.utils.escapeHtml;
10 10 const escAttr = GoingsOn.utils.escapeAttrValue;
11 + const escArg = GoingsOn.utils.escapeHandlerArg;
11 12
12 13 let currentTaskId = null;
13 14 let heatmapMonth = null; // Date object for displayed month
@@ -368,7 +369,7 @@
368 369 for (const s of t.subtasks) {
369 370 const checked = s.isCompleted ? 'checked' : '';
370 371 html += `<div class="task-overview-subtask">`;
371 - html += `<input type="checkbox" class="bulk-checkbox" ${checked} onchange="GoingsOn.taskOverview.toggleSubtask('${escAttr(s.id)}')" ${s.linkedTaskId ? 'disabled' : ''}>`;
372 + html += `<input type="checkbox" class="bulk-checkbox" ${checked} onchange="GoingsOn.taskOverview.toggleSubtask('${escArg(s.id)}')" ${s.linkedTaskId ? 'disabled' : ''}>`;
372 373 html += `<span class="${s.isCompleted ? 'completed-text' : ''}">${esc(s.text)}</span>`;
373 374 if (s.linkedTaskId) html += ' <span class="badge">Linked</span>';
374 375 html += '</div>';
@@ -492,7 +493,7 @@
492 493 if (t.status !== 'Completed') {
493 494 html += `<button class="btn btn-primary" onclick="GoingsOn.taskOverview.completeTask()">Complete</button> `;
494 495 }
495 - html += `<button class="btn btn-secondary" onclick="GoingsOn.tasks.openEdit('${escAttr(t.id)}')">Edit</button> `;
496 + html += `<button class="btn btn-secondary" onclick="GoingsOn.tasks.openEdit('${escArg(t.id)}')">Edit</button> `;
496 497 html += `<button class="btn btn-secondary text-accent-red" onclick="GoingsOn.taskOverview.deleteTask()">Delete</button>`;
497 498 return html;
498 499 }
@@ -7,6 +7,7 @@
7 7 'use strict';
8 8 const esc = GoingsOn.utils.escapeHtml;
9 9 const escAttr = GoingsOn.utils.escapeAttrValue;
10 + const escArg = GoingsOn.utils.escapeHandlerArg;
10 11
11 12 const COLUMNS = [
12 13 { status: 'Pending', label: 'Pending' },
@@ -58,8 +59,8 @@
58 59 draggable="true" data-task-id="${escAttr(task.id)}" data-status="${task.status}"
59 60 ondragstart="GoingsOn.tasksKanban.onDragStart(event)"
60 61 ondragend="GoingsOn.tasksKanban.onDragEnd(event)"
61 - onclick="GoingsOn.tasks.openSubtasks('${escAttr(task.id)}')"
62 - oncontextmenu="GoingsOn.contextMenus.showTask(event, '${escAttr(task.id)}')">
62 + onclick="GoingsOn.tasks.openSubtasks('${escArg(task.id)}')"
63 + oncontextmenu="GoingsOn.contextMenus.showTask(event, '${escArg(task.id)}')">
63 64 <div class="kanban-card-title">${esc(displayDesc)}</div>
64 65 <div class="kanban-card-meta">
65 66 ${task.projectName ? `<span class="kanban-card-project">${esc(task.projectName)}</span>` : ''}
@@ -7,6 +7,7 @@
7 7 'use strict';
8 8 const esc = GoingsOn.utils.escapeHtml;
9 9 const escAttr = GoingsOn.utils.escapeAttrValue;
10 + const escArg = GoingsOn.utils.escapeHandlerArg;
10 11 const escAttrVal = GoingsOn.utils.escapeAttrValue;
11 12
12 13 // ============ Task Rendering Helpers ============
@@ -90,10 +91,10 @@
90 91 return `
91 92 <div class="task-row task-${t.status.toLowerCase()} ${t.isSnoozed ? 'task-snoozed' : ''} ${t.isOverdue ? 'task-overdue' : ''} ${isSelected ? 'selected' : ''}"
92 93 data-id="${escAttr(t.id)}"
93 - oncontextmenu="GoingsOn.contextMenus.showTask(event, '${escAttr(t.id)}')"
94 + oncontextmenu="GoingsOn.contextMenus.showTask(event, '${escArg(t.id)}')"
94 95 tabindex="0" role="row">
95 - <div class="task-cell task-description" onclick="GoingsOn.taskOverview.open('${escAttr(t.id)}')">
96 - ${isStarted ? `<span class="task-started-icon" title="Started - click to track time" onclick="event.stopPropagation(); GoingsOn.timeTracking.startTimer('${escAttr(t.id)}')"></span>` : ''}
96 + <div class="task-cell task-description" onclick="GoingsOn.taskOverview.open('${escArg(t.id)}')">
97 + ${isStarted ? `<span class="task-started-icon" title="Started - click to track time" onclick="event.stopPropagation(); GoingsOn.timeTracking.startTimer('${escArg(t.id)}')"></span>` : ''}
97 98 <span class="task-description-text">${esc(displayDesc)}</span>
98 99 ${renderTimeBadge(t)}
99 100 ${renderTaskBadges(t)}
@@ -116,9 +117,9 @@
116 117 <div class="task-cell task-actions-cell" onclick="event.stopPropagation();">
117 118 <input type="checkbox" class="bulk-checkbox" data-id="${escAttr(t.id)}"
118 119 ${isSelected ? 'checked' : ''}
119 - onchange="GoingsOn.tasks.toggleSelection('${escAttr(t.id)}', this, event)"
120 + onchange="GoingsOn.tasks.toggleSelection('${escArg(t.id)}', this, event)"
120 121 aria-label="Select task">
121 - <button class="btn-icon kebab-btn" onclick="event.stopPropagation(); GoingsOn.contextMenus.showTask(event, '${escAttr(t.id)}')" title="Actions" aria-label="Task actions">&#x22EE;</button>
122 + <button class="btn-icon kebab-btn" onclick="event.stopPropagation(); GoingsOn.contextMenus.showTask(event, '${escArg(t.id)}')" title="Actions" aria-label="Task actions">&#x22EE;</button>
122 123 </div>
123 124 </div>
124 125 `;
@@ -159,19 +160,19 @@
159 160 return `
160 161 <div class="subtask-item subtask-item--linked">
161 162 <input type="checkbox" ${isCompleted ? 'checked' : ''} disabled class="subtask-checkbox-disabled" title="Completion syncs with linked task">
162 - <span class="flex-1" style="cursor: pointer;${isCompleted ? ' text-decoration: line-through; opacity: 0.6;' : ''}" onclick="GoingsOn.tasks.openSubtasks('${escAttr(linkedTaskId)}')" title="Click to open linked task">
163 + <span class="flex-1" style="cursor: pointer;${isCompleted ? ' text-decoration: line-through; opacity: 0.6;' : ''}" onclick="GoingsOn.tasks.openSubtasks('${escArg(linkedTaskId)}')" title="Click to open linked task">
163 164 <span class="subtask-linked-tag">[Linked]</span> ${esc(s.text)}
164 165 </span>
165 - <button class="btn btn-sm text-accent-red" onclick="GoingsOn.tasks.deleteSubtask('${escAttr(taskId)}', '${escAttr(s.id)}')" aria-label="Unlink task">&times;</button>
166 + <button class="btn btn-sm text-accent-red" onclick="GoingsOn.tasks.deleteSubtask('${escArg(taskId)}', '${escArg(s.id)}')" aria-label="Unlink task">&times;</button>
166 167 </div>
167 168 `;
168 169 } else {
169 170 // Regular text subtask
170 171 return `
171 172 <div class="subtask-item">
172 - <input type="checkbox" ${isCompleted ? 'checked' : ''} onchange="GoingsOn.tasks.toggleSubtask('${escAttr(taskId)}', '${escAttr(s.id)}')" class="subtask-checkbox" aria-label="Toggle subtask completion">
173 + <input type="checkbox" ${isCompleted ? 'checked' : ''} onchange="GoingsOn.tasks.toggleSubtask('${escArg(taskId)}', '${escArg(s.id)}')" class="subtask-checkbox" aria-label="Toggle subtask completion">
173 174 <span class="flex-1${isCompleted ? ' subtask-text-done' : ''}">${esc(s.text)}</span>
174 - <button class="btn btn-sm text-accent-red" onclick="GoingsOn.tasks.deleteSubtask('${escAttr(taskId)}', '${escAttr(s.id)}')" aria-label="Delete subtask">&times;</button>
175 + <button class="btn btn-sm text-accent-red" onclick="GoingsOn.tasks.deleteSubtask('${escArg(taskId)}', '${escArg(s.id)}')" aria-label="Delete subtask">&times;</button>
175 176 </div>
176 177 `;
177 178 }
@@ -182,23 +183,23 @@
182 183 <div style="margin-bottom: 1rem; max-height: 300px; overflow-y: auto;">
183 184 ${subtasksList}
184 185 </div>
185 - <form onsubmit="GoingsOn.tasks.addSubtask(event, '${escAttr(taskId)}')" class="row-flex row-flex-2">
186 + <form onsubmit="GoingsOn.tasks.addSubtask(event, '${escArg(taskId)}')" class="row-flex row-flex-2">
186 187 <input type="text" class="form-input flex-1" name="subtask_text" placeholder="Add a subtask...">
187 188 <button type="submit" class="btn btn-primary">Add</button>
188 189 </form>
189 190 <div style="margin-top: 0.5rem;">
190 - <button type="button" class="btn btn-secondary" onclick="GoingsOn.tasks.openLinkTaskPicker('${escAttr(taskId)}')" style="width: 100%;">Link Another Task</button>
191 + <button type="button" class="btn btn-secondary" onclick="GoingsOn.tasks.openLinkTaskPicker('${escArg(taskId)}')" style="width: 100%;">Link Another Task</button>
191 192 </div>
192 193 <div class="task-actions-bar">
193 - <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.tasks.openEdit('${escAttr(taskId)}')" title="Edit task fields">Edit</button>
194 - <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.tasks.addAnnotation('${escAttr(taskId)}')" title="Add a note to this task">Note</button>
195 - <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.ui.closeModal(); GoingsOn.timeTracking.startTimer('${escAttr(taskId)}')" title="Start live timer">Track Time</button>
196 - <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.ui.closeModal(); GoingsOn.focusTimer.start('${escAttr(taskId)}')" title="Pomodoro-style focus session">Focus</button>
197 - <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.dayPlan.openScheduleTaskModal('${escAttr(taskId)}')" title="Block time on day planner">Schedule</button>
198 - <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.attachments.openPanel('${escAttr(taskId)}', null)" title="Manage file attachments">Files</button>
194 + <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.tasks.openEdit('${escArg(taskId)}')" title="Edit task fields">Edit</button>
195 + <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.tasks.addAnnotation('${escArg(taskId)}')" title="Add a note to this task">Note</button>
196 + <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.ui.closeModal(); GoingsOn.timeTracking.startTimer('${escArg(taskId)}')" title="Start live timer">Track Time</button>
197 + <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.ui.closeModal(); GoingsOn.focusTimer.start('${escArg(taskId)}')" title="Pomodoro-style focus session">Focus</button>
198 + <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.dayPlan.openScheduleTaskModal('${escArg(taskId)}')" title="Block time on day planner">Schedule</button>
199 + <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.attachments.openPanel('${escArg(taskId)}', null)" title="Manage file attachments">Files</button>
199 200 </div>
200 201 <div class="form-actions" style="margin-top: 0.75rem;">
201 - <button type="button" class="btn btn-secondary" onclick="GoingsOn.tasks.openActions('${escAttr(taskId)}')">All Actions</button>
202 + <button type="button" class="btn btn-secondary" onclick="GoingsOn.tasks.openActions('${escArg(taskId)}')">All Actions</button>
202 203 <div class="flex-1"></div>
203 204 <button type="button" class="btn btn-secondary" onclick="GoingsOn.tasks.closeSubtasksAndRefresh()">Close</button>
204 205 </div>