clocktopus 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dashboard/routes/calendar.js +7 -0
- package/dist/dashboard/routes/clockify.js +6 -1
- package/dist/dashboard/routes/data.js +25 -1
- package/dist/dashboard/routes/jira.js +6 -1
- package/dist/dashboard/routes/status.js +48 -37
- package/dist/dashboard/routes/timer.js +145 -47
- package/dist/dashboard/views.js +342 -32
- package/dist/index.js +110 -46
- package/dist/lib/credentials.js +18 -0
- package/dist/lib/credentials.test.js +13 -0
- package/dist/lib/db.js +32 -4
- package/dist/scripts/log-calendar-events.js +5 -0
- package/package.json +1 -1
package/dist/dashboard/views.js
CHANGED
|
@@ -125,9 +125,24 @@ export function indexPage() {
|
|
|
125
125
|
.toggle input:checked + .slider { background: #238636; }
|
|
126
126
|
.toggle input:checked + .slider::before { transform: translateX(16px); background: #fff; }
|
|
127
127
|
|
|
128
|
+
.empty-state-card { padding: 1.5rem; text-align: center; color: #8b949e; font-size: 0.9rem; }
|
|
129
|
+
.ticket-preview { margin-top: 0.5rem; padding: 0.6rem 0.8rem; border: 1px solid #30363d; border-radius: 6px; background: #161b22; color: #e1e4e8; font-size: 0.85rem; min-height: 2rem; }
|
|
130
|
+
.ticket-preview .ticket-id { color: #58a6ff; font-weight: 600; margin-right: 0.4rem; }
|
|
131
|
+
.ticket-preview .ticket-hint { color: #8b949e; font-style: italic; }
|
|
132
|
+
.ticket-preview .ticket-error { color: #f85149; }
|
|
133
|
+
.local-hint { margin-top: 0.4rem; color: #8b949e; font-size: 0.8rem; font-style: italic; }
|
|
134
|
+
.local-hint a { color: #58a6ff; }
|
|
135
|
+
|
|
136
|
+
#ctx-menu { position: fixed; display: none; z-index: 9999; background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 0.25rem; min-width: 140px; box-shadow: 0 4px 12px rgba(0,0,0,0.4); }
|
|
137
|
+
#ctx-menu button { display: block; width: 100%; margin: 0; text-align: left; background: transparent; border: none; color: #e1e4e8; padding: 0.4rem 0.7rem; border-radius: 4px; font-size: 0.85rem; cursor: pointer; }
|
|
138
|
+
#ctx-menu button:hover { background: #21262d; }
|
|
139
|
+
|
|
128
140
|
</style>
|
|
129
141
|
</head>
|
|
130
|
-
<body
|
|
142
|
+
<body>
|
|
143
|
+
<div id="ctx-menu">
|
|
144
|
+
<button type="button" onclick="location.reload()">Reload</button>
|
|
145
|
+
</div>
|
|
131
146
|
<div class="header">
|
|
132
147
|
<h1>Clocktopus</h1>
|
|
133
148
|
<div class="nav">
|
|
@@ -152,29 +167,33 @@ export function indexPage() {
|
|
|
152
167
|
|
|
153
168
|
<div class="cards home-cards">
|
|
154
169
|
<!-- Track Time -->
|
|
155
|
-
<div class="card">
|
|
156
|
-
<div class="track-tabs">
|
|
170
|
+
<div class="card" id="track-card">
|
|
171
|
+
<div class="track-tabs" id="track-tabs">
|
|
157
172
|
<button class="track-tab-btn active" data-mode="auto" onclick="switchTrackMode('auto')">Auto Track</button>
|
|
158
173
|
<button class="track-tab-btn" data-mode="manual" onclick="switchTrackMode('manual')">Manual Log</button>
|
|
159
174
|
</div>
|
|
160
175
|
|
|
161
176
|
<div id="track-auto">
|
|
162
177
|
<div id="start-timer-form">
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
<
|
|
166
|
-
|
|
178
|
+
<div id="timer-project-wrap">
|
|
179
|
+
<label for="project-select">Project</label>
|
|
180
|
+
<select id="project-select">
|
|
181
|
+
<option value="">Loading projects...</option>
|
|
182
|
+
</select>
|
|
183
|
+
</div>
|
|
167
184
|
<div class="form-row">
|
|
168
|
-
<div>
|
|
185
|
+
<div id="timer-description-wrap">
|
|
169
186
|
<label for="timer-description">Description</label>
|
|
170
187
|
<input type="text" id="timer-description" placeholder="What are you working on?" />
|
|
188
|
+
<div id="timer-local-hint" class="local-hint" style="display:none;">Time will be logged locally only. Configure Clockify or Jira in <a href="#" onclick="switchTab('settings');return false;">Settings</a> to sync.</div>
|
|
171
189
|
</div>
|
|
172
190
|
<div>
|
|
173
|
-
<label for="timer-jira">Jira Ticket (optional)</label>
|
|
191
|
+
<label for="timer-jira" id="timer-jira-label">Jira Ticket (optional)</label>
|
|
174
192
|
<input type="text" id="timer-jira" placeholder="e.g. PROJ-123" />
|
|
175
193
|
</div>
|
|
176
194
|
</div>
|
|
177
|
-
<
|
|
195
|
+
<div id="timer-description-preview" class="ticket-preview" style="display:none;"></div>
|
|
196
|
+
<label id="timer-billable-wrap" style="display:flex; align-items:center; gap:0.5rem; font-weight:normal; cursor:pointer;">
|
|
178
197
|
<input type="checkbox" id="timer-billable" checked style="width:auto; margin:0;" />
|
|
179
198
|
Billable
|
|
180
199
|
</label>
|
|
@@ -185,10 +204,12 @@ export function indexPage() {
|
|
|
185
204
|
</div>
|
|
186
205
|
|
|
187
206
|
<div id="track-manual" style="display:none;">
|
|
188
|
-
<
|
|
189
|
-
|
|
190
|
-
<
|
|
191
|
-
|
|
207
|
+
<div id="manual-project-wrap">
|
|
208
|
+
<label for="manual-project">Project</label>
|
|
209
|
+
<select id="manual-project">
|
|
210
|
+
<option value="">Loading projects...</option>
|
|
211
|
+
</select>
|
|
212
|
+
</div>
|
|
192
213
|
<div class="form-row">
|
|
193
214
|
<div>
|
|
194
215
|
<label for="manual-start">Start</label>
|
|
@@ -200,16 +221,18 @@ export function indexPage() {
|
|
|
200
221
|
</div>
|
|
201
222
|
</div>
|
|
202
223
|
<div class="form-row">
|
|
203
|
-
<div>
|
|
224
|
+
<div id="manual-description-wrap">
|
|
204
225
|
<label for="manual-description">Description</label>
|
|
205
226
|
<input type="text" id="manual-description" placeholder="What did you work on?" />
|
|
227
|
+
<div id="manual-local-hint" class="local-hint" style="display:none;">Time will be logged locally only. Configure Clockify or Jira in <a href="#" onclick="switchTab('settings');return false;">Settings</a> to sync.</div>
|
|
206
228
|
</div>
|
|
207
229
|
<div>
|
|
208
|
-
<label for="manual-jira">Jira Ticket (optional)</label>
|
|
230
|
+
<label for="manual-jira" id="manual-jira-label">Jira Ticket (optional)</label>
|
|
209
231
|
<input type="text" id="manual-jira" placeholder="e.g. PROJ-123" />
|
|
210
232
|
</div>
|
|
211
233
|
</div>
|
|
212
|
-
<
|
|
234
|
+
<div id="manual-description-preview" class="ticket-preview" style="display:none;"></div>
|
|
235
|
+
<label id="manual-billable-wrap" style="display:flex; align-items:center; gap:0.5rem; font-weight:normal; cursor:pointer;">
|
|
213
236
|
<input type="checkbox" id="manual-billable" checked style="width:auto; margin:0;" />
|
|
214
237
|
Billable
|
|
215
238
|
</label>
|
|
@@ -218,6 +241,7 @@ export function indexPage() {
|
|
|
218
241
|
</div>
|
|
219
242
|
</div>
|
|
220
243
|
|
|
244
|
+
|
|
221
245
|
<!-- Monitor Control -->
|
|
222
246
|
<div class="card">
|
|
223
247
|
<div class="card-header">
|
|
@@ -293,6 +317,13 @@ export function indexPage() {
|
|
|
293
317
|
<label for="clockify-key">API Key</label>
|
|
294
318
|
<input type="password" id="clockify-key" placeholder="Enter your Clockify API key" />
|
|
295
319
|
<button onclick="saveClockify()">Save & Validate</button>
|
|
320
|
+
<div style="display:flex; align-items:center; gap:0.6rem; margin-top:0.75rem;">
|
|
321
|
+
<label class="toggle">
|
|
322
|
+
<input type="checkbox" id="clockify-enabled-toggle" onchange="toggleClockifyEnabled()" />
|
|
323
|
+
<span class="slider"></span>
|
|
324
|
+
</label>
|
|
325
|
+
<span id="clockify-enabled-label" style="font-size:0.9rem; color:#8b949e;">Enabled</span>
|
|
326
|
+
</div>
|
|
296
327
|
<div class="msg" id="clockify-msg"></div>
|
|
297
328
|
</div>
|
|
298
329
|
|
|
@@ -304,6 +335,7 @@ export function indexPage() {
|
|
|
304
335
|
</div>
|
|
305
336
|
<p id="google-desc" style="font-size:0.85rem;color:#8b949e;margin-bottom:0.5rem;">Authorize access to your Google Calendar.</p>
|
|
306
337
|
<button class="connect" id="google-connect-btn" onclick="connectGoogle()">Connect Google Account</button>
|
|
338
|
+
<p id="google-connect-note" style="font-size:0.75rem;color:#8b949e;margin-top:0.5rem;display:none;"></p>
|
|
307
339
|
<div class="msg" id="google-msg"></div>
|
|
308
340
|
</div>
|
|
309
341
|
|
|
@@ -315,6 +347,13 @@ export function indexPage() {
|
|
|
315
347
|
</div>
|
|
316
348
|
<p id="jira-desc" style="font-size:0.85rem;color:#8b949e;margin-bottom:0.5rem;">Connect your Atlassian account to log time on Jira tickets.</p>
|
|
317
349
|
<button class="connect" id="jira-connect-btn" onclick="connectJira()">Connect Atlassian</button>
|
|
350
|
+
<div style="display:flex; align-items:center; gap:0.6rem; margin-top:0.75rem;">
|
|
351
|
+
<label class="toggle">
|
|
352
|
+
<input type="checkbox" id="jira-enabled-toggle" onchange="toggleJiraEnabled()" />
|
|
353
|
+
<span class="slider"></span>
|
|
354
|
+
</label>
|
|
355
|
+
<span id="jira-enabled-label" style="font-size:0.9rem; color:#8b949e;">Enabled</span>
|
|
356
|
+
</div>
|
|
318
357
|
<div class="msg" id="jira-msg"></div>
|
|
319
358
|
<div style="margin-top:1rem;">
|
|
320
359
|
<a href="#" id="jira-toggle" onclick="toggleJiraForm(event)" style="font-size:0.8rem;color:#8b949e;text-decoration:none;">or use API token ↓</a>
|
|
@@ -384,10 +423,26 @@ export function indexPage() {
|
|
|
384
423
|
</div>
|
|
385
424
|
|
|
386
425
|
<script>
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
426
|
+
// Suppress WebKit's Back/Forward menu. Capture-phase wins the race with
|
|
427
|
+
// the native WKWebView menu. Our custom menu offers Reload only.
|
|
428
|
+
(function wireContextMenu() {
|
|
429
|
+
const menu = document.getElementById('ctx-menu');
|
|
430
|
+
if (!menu) return;
|
|
431
|
+
const hide = () => { menu.style.display = 'none'; };
|
|
432
|
+
window.addEventListener('contextmenu', (e) => {
|
|
433
|
+
e.preventDefault();
|
|
434
|
+
const pad = 4;
|
|
435
|
+
const x = Math.min(e.clientX, window.innerWidth - menu.offsetWidth - pad);
|
|
436
|
+
const y = Math.min(e.clientY, window.innerHeight - menu.offsetHeight - pad);
|
|
437
|
+
menu.style.left = x + 'px';
|
|
438
|
+
menu.style.top = y + 'px';
|
|
439
|
+
menu.style.display = 'block';
|
|
440
|
+
}, { capture: true });
|
|
441
|
+
window.addEventListener('click', hide);
|
|
442
|
+
window.addEventListener('scroll', hide, { capture: true });
|
|
443
|
+
window.addEventListener('keydown', (e) => { if (e.key === 'Escape') hide(); });
|
|
444
|
+
window.addEventListener('blur', hide);
|
|
445
|
+
})();
|
|
391
446
|
|
|
392
447
|
let elapsedInterval = null;
|
|
393
448
|
let currentPage = 1;
|
|
@@ -395,10 +450,12 @@ export function indexPage() {
|
|
|
395
450
|
|
|
396
451
|
// --- Tab switching ---
|
|
397
452
|
function switchTab(tab) {
|
|
453
|
+
const nav = document.getElementById('nav-' + tab);
|
|
454
|
+
if (nav && nav.getAttribute('aria-disabled') === 'true') return;
|
|
398
455
|
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
|
|
399
456
|
document.querySelectorAll('.nav-btn').forEach(el => el.classList.remove('active'));
|
|
400
457
|
document.getElementById('tab-' + tab).classList.add('active');
|
|
401
|
-
|
|
458
|
+
if (nav) nav.classList.add('active');
|
|
402
459
|
}
|
|
403
460
|
|
|
404
461
|
function switchTrackMode(mode) {
|
|
@@ -475,12 +532,19 @@ export function indexPage() {
|
|
|
475
532
|
|
|
476
533
|
async function startTimer() {
|
|
477
534
|
const projectId = document.getElementById('project-select').value;
|
|
478
|
-
const description = document.getElementById('timer-description').value.trim();
|
|
479
535
|
const jiraTicket = document.getElementById('timer-jira').value.trim();
|
|
480
536
|
const billable = document.getElementById('timer-billable').checked;
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
537
|
+
const typedDescription = document.getElementById('timer-description').value.trim();
|
|
538
|
+
const description = typedDescription;
|
|
539
|
+
|
|
540
|
+
if (currentMode.clockifyOn) {
|
|
541
|
+
if (!projectId) return setMsg('timer-msg', 'Please select a project.', false);
|
|
542
|
+
if (!typedDescription && !jiraTicket) return setMsg('timer-msg', 'Please enter a description or Jira ticket.', false);
|
|
543
|
+
} else if (currentMode.jiraOn) {
|
|
544
|
+
if (!jiraTicket) return setMsg('timer-msg', 'Please enter a Jira ticket.', false);
|
|
545
|
+
} else {
|
|
546
|
+
if (!typedDescription) return setMsg('timer-msg', 'Please enter a description.', false);
|
|
547
|
+
}
|
|
484
548
|
|
|
485
549
|
const btn = document.getElementById('start-btn');
|
|
486
550
|
btn.disabled = true;
|
|
@@ -490,13 +554,20 @@ export function indexPage() {
|
|
|
490
554
|
const res = await fetch('/api/timer/start', {
|
|
491
555
|
method: 'POST',
|
|
492
556
|
headers: { 'Content-Type': 'application/json' },
|
|
493
|
-
body: JSON.stringify({
|
|
557
|
+
body: JSON.stringify({
|
|
558
|
+
projectId: projectId || undefined,
|
|
559
|
+
description: currentMode.clockifyOn ? (description || 'Working on a task...') : description,
|
|
560
|
+
jiraTicket: jiraTicket || undefined,
|
|
561
|
+
billable,
|
|
562
|
+
}),
|
|
494
563
|
});
|
|
495
564
|
const data = await res.json();
|
|
496
565
|
if (data.ok) {
|
|
497
566
|
setMsg('timer-msg', 'Timer started!', true);
|
|
498
567
|
document.getElementById('timer-description').value = '';
|
|
499
568
|
document.getElementById('timer-jira').value = '';
|
|
569
|
+
timerJiraSummary = '';
|
|
570
|
+
renderTicketPreview('timer-description-preview', '', '');
|
|
500
571
|
checkActiveTimer();
|
|
501
572
|
loadSessions();
|
|
502
573
|
} else {
|
|
@@ -548,18 +619,26 @@ export function indexPage() {
|
|
|
548
619
|
const projectId = document.getElementById('manual-project').value;
|
|
549
620
|
const startVal = document.getElementById('manual-start').value;
|
|
550
621
|
const endVal = document.getElementById('manual-end').value;
|
|
551
|
-
const
|
|
622
|
+
const typedDescription = document.getElementById('manual-description').value.trim();
|
|
552
623
|
const jiraTicket = document.getElementById('manual-jira').value.trim();
|
|
553
624
|
const billable = document.getElementById('manual-billable').checked;
|
|
625
|
+
const description = typedDescription;
|
|
554
626
|
|
|
555
|
-
if (!projectId) return setMsg('manual-msg', 'Please select a project.', false);
|
|
556
627
|
if (!startVal || !endVal) return setMsg('manual-msg', 'Please set start and end.', false);
|
|
557
628
|
|
|
558
629
|
const startMs = new Date(startVal).getTime();
|
|
559
630
|
const endMs = new Date(endVal).getTime();
|
|
560
631
|
if (isNaN(startMs) || isNaN(endMs)) return setMsg('manual-msg', 'Invalid date.', false);
|
|
561
632
|
if (endMs <= startMs) return setMsg('manual-msg', 'End must be after start.', false);
|
|
562
|
-
|
|
633
|
+
|
|
634
|
+
if (currentMode.clockifyOn) {
|
|
635
|
+
if (!projectId) return setMsg('manual-msg', 'Please select a project.', false);
|
|
636
|
+
if (!typedDescription && !jiraTicket) return setMsg('manual-msg', 'Please enter a description or Jira ticket.', false);
|
|
637
|
+
} else if (currentMode.jiraOn) {
|
|
638
|
+
if (!jiraTicket) return setMsg('manual-msg', 'Please enter a Jira ticket.', false);
|
|
639
|
+
} else {
|
|
640
|
+
if (!typedDescription) return setMsg('manual-msg', 'Please enter a description.', false);
|
|
641
|
+
}
|
|
563
642
|
|
|
564
643
|
const btn = document.getElementById('manual-log-btn');
|
|
565
644
|
btn.disabled = true;
|
|
@@ -570,7 +649,7 @@ export function indexPage() {
|
|
|
570
649
|
method: 'POST',
|
|
571
650
|
headers: { 'Content-Type': 'application/json' },
|
|
572
651
|
body: JSON.stringify({
|
|
573
|
-
projectId: projectId,
|
|
652
|
+
projectId: projectId || undefined,
|
|
574
653
|
description: description,
|
|
575
654
|
start: new Date(startMs).toISOString(),
|
|
576
655
|
end: new Date(endMs).toISOString(),
|
|
@@ -583,6 +662,8 @@ export function indexPage() {
|
|
|
583
662
|
setMsg('manual-msg', 'Time logged.', true);
|
|
584
663
|
document.getElementById('manual-description').value = '';
|
|
585
664
|
document.getElementById('manual-jira').value = '';
|
|
665
|
+
manualJiraSummary = '';
|
|
666
|
+
renderTicketPreview('manual-description-preview', '', '');
|
|
586
667
|
setManualDefaults();
|
|
587
668
|
loadSessions();
|
|
588
669
|
} else {
|
|
@@ -893,7 +974,7 @@ export function indexPage() {
|
|
|
893
974
|
: '';
|
|
894
975
|
return '<tr>' +
|
|
895
976
|
'<td>' + escapeHtml(s.description) + '</td>' +
|
|
896
|
-
'<td>' + escapeHtml(s.projectName) + '</td>' +
|
|
977
|
+
'<td>' + (s.projectName ? escapeHtml(s.projectName) : '—') + '</td>' +
|
|
897
978
|
'<td>' + started + '</td>' +
|
|
898
979
|
'<td>' + duration + '</td>' +
|
|
899
980
|
'<td>' + escapeHtml(jira) + '</td>' +
|
|
@@ -1001,6 +1082,27 @@ export function indexPage() {
|
|
|
1001
1082
|
}
|
|
1002
1083
|
}
|
|
1003
1084
|
|
|
1085
|
+
async function toggleClockifyEnabled() {
|
|
1086
|
+
const enabled = document.getElementById('clockify-enabled-toggle').checked;
|
|
1087
|
+
document.getElementById('clockify-enabled-label').textContent = enabled ? 'Enabled' : 'Disabled';
|
|
1088
|
+
try {
|
|
1089
|
+
const res = await fetch('/api/clockify/enabled', {
|
|
1090
|
+
method: 'POST',
|
|
1091
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1092
|
+
body: JSON.stringify({ enabled }),
|
|
1093
|
+
});
|
|
1094
|
+
const data = await res.json();
|
|
1095
|
+
if (data.ok) {
|
|
1096
|
+
setMsg('clockify-msg', enabled ? 'Clockify enabled.' : 'Clockify disabled.', true);
|
|
1097
|
+
fetchStatus();
|
|
1098
|
+
} else {
|
|
1099
|
+
setMsg('clockify-msg', data.error || 'Failed to update.', false);
|
|
1100
|
+
}
|
|
1101
|
+
} catch {
|
|
1102
|
+
setMsg('clockify-msg', 'Request failed.', false);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1004
1106
|
// --- Settings: Google ---
|
|
1005
1107
|
function setGoogleConnected(connected, email) {
|
|
1006
1108
|
const btn = document.getElementById('google-connect-btn');
|
|
@@ -1076,6 +1178,27 @@ export function indexPage() {
|
|
|
1076
1178
|
}
|
|
1077
1179
|
}
|
|
1078
1180
|
|
|
1181
|
+
async function toggleJiraEnabled() {
|
|
1182
|
+
const enabled = document.getElementById('jira-enabled-toggle').checked;
|
|
1183
|
+
document.getElementById('jira-enabled-label').textContent = enabled ? 'Enabled' : 'Disabled';
|
|
1184
|
+
try {
|
|
1185
|
+
const res = await fetch('/api/jira/enabled', {
|
|
1186
|
+
method: 'POST',
|
|
1187
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1188
|
+
body: JSON.stringify({ enabled }),
|
|
1189
|
+
});
|
|
1190
|
+
const data = await res.json();
|
|
1191
|
+
if (data.ok) {
|
|
1192
|
+
setMsg('jira-msg', enabled ? 'Jira enabled.' : 'Jira disabled.', true);
|
|
1193
|
+
fetchStatus();
|
|
1194
|
+
} else {
|
|
1195
|
+
setMsg('jira-msg', data.error || 'Failed to update.', false);
|
|
1196
|
+
}
|
|
1197
|
+
} catch {
|
|
1198
|
+
setMsg('jira-msg', 'Request failed.', false);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1079
1202
|
async function saveJira() {
|
|
1080
1203
|
const url = document.getElementById('jira-url').value.trim();
|
|
1081
1204
|
const email = document.getElementById('jira-email').value.trim();
|
|
@@ -1101,6 +1224,173 @@ export function indexPage() {
|
|
|
1101
1224
|
}
|
|
1102
1225
|
|
|
1103
1226
|
// --- Status ---
|
|
1227
|
+
var currentMode = { clockifyOn: true, jiraOn: false };
|
|
1228
|
+
var timerJiraSummary = '';
|
|
1229
|
+
var manualJiraSummary = '';
|
|
1230
|
+
|
|
1231
|
+
function renderTicketPreview(previewId, ticket, description) {
|
|
1232
|
+
var el = document.getElementById(previewId);
|
|
1233
|
+
if (!el) return;
|
|
1234
|
+
if (!ticket) {
|
|
1235
|
+
el.style.display = 'none';
|
|
1236
|
+
el.innerHTML = '';
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
el.style.display = '';
|
|
1240
|
+
if (description) {
|
|
1241
|
+
el.innerHTML = '<span class="ticket-id">' + escapeHtml(ticket) + '</span>' + escapeHtml(description);
|
|
1242
|
+
} else {
|
|
1243
|
+
el.innerHTML = '<span class="ticket-id">' + escapeHtml(ticket) + '</span><span class="ticket-hint">Could not fetch title from Jira. We will log with the ticket id.</span>';
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
async function fetchTicketSummary(ticket) {
|
|
1248
|
+
if (!/^[A-Z][A-Z0-9]+-\d+$/.test(ticket)) return null;
|
|
1249
|
+
try {
|
|
1250
|
+
const res = await fetch('/api/jira/ticket-summary?jira=' + encodeURIComponent(ticket));
|
|
1251
|
+
const data = await res.json();
|
|
1252
|
+
if (data.ok) return data.description || '';
|
|
1253
|
+
} catch {}
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function debounce(fn, ms) {
|
|
1258
|
+
var t;
|
|
1259
|
+
return function() {
|
|
1260
|
+
var ctx = this, args = arguments;
|
|
1261
|
+
clearTimeout(t);
|
|
1262
|
+
t = setTimeout(function(){ fn.apply(ctx, args); }, ms);
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
async function onTimerJiraInput() {
|
|
1267
|
+
var ticket = document.getElementById('timer-jira').value.trim().toUpperCase();
|
|
1268
|
+
if (!currentMode.clockifyOn) {
|
|
1269
|
+
var desc = await fetchTicketSummary(ticket);
|
|
1270
|
+
timerJiraSummary = desc || '';
|
|
1271
|
+
renderTicketPreview('timer-description-preview', ticket, timerJiraSummary);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
async function onManualJiraInput() {
|
|
1275
|
+
var ticket = document.getElementById('manual-jira').value.trim().toUpperCase();
|
|
1276
|
+
if (!currentMode.clockifyOn) {
|
|
1277
|
+
var desc = await fetchTicketSummary(ticket);
|
|
1278
|
+
manualJiraSummary = desc || '';
|
|
1279
|
+
renderTicketPreview('manual-description-preview', ticket, manualJiraSummary);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
(function wireTicketPreviewInputs() {
|
|
1284
|
+
var t = document.getElementById('timer-jira');
|
|
1285
|
+
var m = document.getElementById('manual-jira');
|
|
1286
|
+
if (t) t.addEventListener('input', debounce(onTimerJiraInput, 300));
|
|
1287
|
+
if (m) m.addEventListener('input', debounce(onManualJiraInput, 300));
|
|
1288
|
+
})();
|
|
1289
|
+
|
|
1290
|
+
function applyMode(data) {
|
|
1291
|
+
const clockifyOn = !!data.clockify;
|
|
1292
|
+
const jiraOn = !!data.jira;
|
|
1293
|
+
const localOnly = !clockifyOn && !jiraOn;
|
|
1294
|
+
currentMode.clockifyOn = clockifyOn;
|
|
1295
|
+
currentMode.jiraOn = jiraOn;
|
|
1296
|
+
|
|
1297
|
+
const trackCard = document.getElementById('track-card');
|
|
1298
|
+
const timerLocalHint = document.getElementById('timer-local-hint');
|
|
1299
|
+
const manualLocalHint = document.getElementById('manual-local-hint');
|
|
1300
|
+
const projWrap = document.getElementById('timer-project-wrap');
|
|
1301
|
+
const manualProjWrap = document.getElementById('manual-project-wrap');
|
|
1302
|
+
const descWrap = document.getElementById('timer-description-wrap');
|
|
1303
|
+
const manualDescWrap = document.getElementById('manual-description-wrap');
|
|
1304
|
+
const descPreview = document.getElementById('timer-description-preview');
|
|
1305
|
+
const manualDescPreview = document.getElementById('manual-description-preview');
|
|
1306
|
+
const jiraInput = document.getElementById('timer-jira');
|
|
1307
|
+
const manualJiraInput = document.getElementById('manual-jira');
|
|
1308
|
+
const jiraLabel = document.getElementById('timer-jira-label');
|
|
1309
|
+
const manualJiraLabel = document.getElementById('manual-jira-label');
|
|
1310
|
+
const jiraWrap = jiraInput ? jiraInput.closest('.row') || jiraInput.parentElement : null;
|
|
1311
|
+
const manualJiraWrap = manualJiraInput ? manualJiraInput.closest('.row') || manualJiraInput.parentElement : null;
|
|
1312
|
+
const billableWrap = document.getElementById('timer-billable-wrap');
|
|
1313
|
+
const manualBillableWrap = document.getElementById('manual-billable-wrap');
|
|
1314
|
+
|
|
1315
|
+
if (trackCard) trackCard.style.display = '';
|
|
1316
|
+
if (timerLocalHint) timerLocalHint.style.display = localOnly ? '' : 'none';
|
|
1317
|
+
if (manualLocalHint) manualLocalHint.style.display = localOnly ? '' : 'none';
|
|
1318
|
+
|
|
1319
|
+
if (clockifyOn) {
|
|
1320
|
+
if (projWrap) projWrap.style.display = '';
|
|
1321
|
+
if (manualProjWrap) manualProjWrap.style.display = '';
|
|
1322
|
+
if (descWrap) descWrap.style.display = '';
|
|
1323
|
+
if (manualDescWrap) manualDescWrap.style.display = '';
|
|
1324
|
+
if (descPreview) descPreview.style.display = 'none';
|
|
1325
|
+
if (manualDescPreview) manualDescPreview.style.display = 'none';
|
|
1326
|
+
if (jiraWrap) jiraWrap.style.display = jiraOn ? '' : 'none';
|
|
1327
|
+
if (manualJiraWrap) manualJiraWrap.style.display = jiraOn ? '' : 'none';
|
|
1328
|
+
if (jiraInput) jiraInput.required = false;
|
|
1329
|
+
if (manualJiraInput) manualJiraInput.required = false;
|
|
1330
|
+
if (jiraLabel) jiraLabel.textContent = 'Jira Ticket (optional)';
|
|
1331
|
+
if (manualJiraLabel) manualJiraLabel.textContent = 'Jira Ticket (optional)';
|
|
1332
|
+
if (billableWrap) billableWrap.style.display = '';
|
|
1333
|
+
if (manualBillableWrap) manualBillableWrap.style.display = '';
|
|
1334
|
+
} else if (jiraOn) {
|
|
1335
|
+
if (projWrap) projWrap.style.display = 'none';
|
|
1336
|
+
if (manualProjWrap) manualProjWrap.style.display = 'none';
|
|
1337
|
+
if (descWrap) descWrap.style.display = 'none';
|
|
1338
|
+
if (manualDescWrap) manualDescWrap.style.display = 'none';
|
|
1339
|
+
if (descPreview) { descPreview.style.display = ''; renderTicketPreview('timer-description-preview', (jiraInput ? jiraInput.value.trim().toUpperCase() : ''), timerJiraSummary); }
|
|
1340
|
+
if (manualDescPreview) { manualDescPreview.style.display = ''; renderTicketPreview('manual-description-preview', (manualJiraInput ? manualJiraInput.value.trim().toUpperCase() : ''), manualJiraSummary); }
|
|
1341
|
+
if (jiraWrap) jiraWrap.style.display = '';
|
|
1342
|
+
if (manualJiraWrap) manualJiraWrap.style.display = '';
|
|
1343
|
+
if (jiraInput) jiraInput.required = true;
|
|
1344
|
+
if (manualJiraInput) manualJiraInput.required = true;
|
|
1345
|
+
if (jiraLabel) jiraLabel.textContent = 'Jira Ticket';
|
|
1346
|
+
if (manualJiraLabel) manualJiraLabel.textContent = 'Jira Ticket';
|
|
1347
|
+
if (billableWrap) billableWrap.style.display = 'none';
|
|
1348
|
+
if (manualBillableWrap) manualBillableWrap.style.display = 'none';
|
|
1349
|
+
onTimerJiraInput();
|
|
1350
|
+
onManualJiraInput();
|
|
1351
|
+
} else {
|
|
1352
|
+
// Local-only: description only
|
|
1353
|
+
if (projWrap) projWrap.style.display = 'none';
|
|
1354
|
+
if (manualProjWrap) manualProjWrap.style.display = 'none';
|
|
1355
|
+
if (descWrap) descWrap.style.display = '';
|
|
1356
|
+
if (manualDescWrap) manualDescWrap.style.display = '';
|
|
1357
|
+
if (descPreview) descPreview.style.display = 'none';
|
|
1358
|
+
if (manualDescPreview) manualDescPreview.style.display = 'none';
|
|
1359
|
+
if (jiraWrap) jiraWrap.style.display = 'none';
|
|
1360
|
+
if (manualJiraWrap) manualJiraWrap.style.display = 'none';
|
|
1361
|
+
if (jiraInput) jiraInput.required = false;
|
|
1362
|
+
if (manualJiraInput) manualJiraInput.required = false;
|
|
1363
|
+
if (billableWrap) billableWrap.style.display = 'none';
|
|
1364
|
+
if (manualBillableWrap) manualBillableWrap.style.display = 'none';
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// Projects tab: hide "Pull from Clockify" button
|
|
1368
|
+
const pullBtn = document.getElementById('fetch-projects-btn');
|
|
1369
|
+
if (pullBtn) pullBtn.style.display = clockifyOn ? '' : 'none';
|
|
1370
|
+
|
|
1371
|
+
// Projects nav: hide entire tab when Clockify off
|
|
1372
|
+
const projectsNav = document.getElementById('nav-projects');
|
|
1373
|
+
if (projectsNav) projectsNav.style.display = clockifyOn ? '' : 'none';
|
|
1374
|
+
|
|
1375
|
+
// Calendar nav: hide entire tab when Clockify off
|
|
1376
|
+
const calNav = document.getElementById('nav-calendar');
|
|
1377
|
+
if (calNav) calNav.style.display = clockifyOn ? '' : 'none';
|
|
1378
|
+
|
|
1379
|
+
// Settings: Google Connect button — disable when Clockify off
|
|
1380
|
+
const gBtn = document.getElementById('google-connect-btn');
|
|
1381
|
+
const gNote = document.getElementById('google-connect-note');
|
|
1382
|
+
if (gBtn) gBtn.disabled = !clockifyOn;
|
|
1383
|
+
if (gNote) {
|
|
1384
|
+
if (clockifyOn) {
|
|
1385
|
+
gNote.textContent = '';
|
|
1386
|
+
gNote.style.display = 'none';
|
|
1387
|
+
} else {
|
|
1388
|
+
gNote.textContent = 'Calendar sync requires Clockify. Connect Clockify first.';
|
|
1389
|
+
gNote.style.display = '';
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1104
1394
|
async function fetchStatus() {
|
|
1105
1395
|
try {
|
|
1106
1396
|
const res = await fetch('/api/status');
|
|
@@ -1111,8 +1401,28 @@ export function indexPage() {
|
|
|
1111
1401
|
if (data.clockifyKeyHint) {
|
|
1112
1402
|
document.getElementById('clockify-key').placeholder = data.clockifyKeyHint;
|
|
1113
1403
|
}
|
|
1404
|
+
const toggle = document.getElementById('clockify-enabled-toggle');
|
|
1405
|
+
const toggleLabel = document.getElementById('clockify-enabled-label');
|
|
1406
|
+
const keyInput = document.getElementById('clockify-key');
|
|
1407
|
+
const saveBtn = document.querySelector('button[onclick="saveClockify()"]');
|
|
1408
|
+
const enabled = !data.clockifyDisabled;
|
|
1409
|
+
if (toggle) toggle.checked = enabled;
|
|
1410
|
+
if (toggleLabel) toggleLabel.textContent = enabled ? 'Enabled' : 'Disabled';
|
|
1411
|
+
if (keyInput) keyInput.disabled = !enabled;
|
|
1412
|
+
if (saveBtn) saveBtn.disabled = !enabled;
|
|
1413
|
+
|
|
1414
|
+
const jToggle = document.getElementById('jira-enabled-toggle');
|
|
1415
|
+
const jToggleLabel = document.getElementById('jira-enabled-label');
|
|
1416
|
+
const jConnectBtn = document.getElementById('jira-connect-btn');
|
|
1417
|
+
const jToggleWrap = jToggle ? jToggle.closest('label').parentElement : null;
|
|
1418
|
+
const jiraEnabled = !data.jiraDisabled;
|
|
1419
|
+
if (jToggle) jToggle.checked = jiraEnabled;
|
|
1420
|
+
if (jToggleLabel) jToggleLabel.textContent = jiraEnabled ? 'Enabled' : 'Disabled';
|
|
1421
|
+
if (jToggleWrap) jToggleWrap.style.display = data.jiraConfigured ? '' : 'none';
|
|
1422
|
+
if (jConnectBtn) jConnectBtn.disabled = data.jiraConfigured && !jiraEnabled;
|
|
1114
1423
|
setGoogleConnected(data.google, data.googleEmail);
|
|
1115
1424
|
setJiraConnected(data.jira, data.jiraOAuth, data.jiraSiteUrl);
|
|
1425
|
+
applyMode(data);
|
|
1116
1426
|
} catch {}
|
|
1117
1427
|
}
|
|
1118
1428
|
|