clocktopus 1.7.0 → 1.8.1
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 +8 -2
- package/dist/dashboard/routes/status.js +48 -37
- package/dist/dashboard/routes/timer.js +145 -47
- package/dist/dashboard/views.js +390 -101
- 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 +36 -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,31 +204,41 @@ 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
|
-
<label for="manual-start">Start</label>
|
|
195
|
-
<
|
|
215
|
+
<label for="manual-start-date">Start</label>
|
|
216
|
+
<div style="display:flex; gap:0.4rem;">
|
|
217
|
+
<input type="date" id="manual-start-date" style="flex:1;" />
|
|
218
|
+
<input type="time" id="manual-start-time" style="flex:1;" />
|
|
219
|
+
</div>
|
|
196
220
|
</div>
|
|
197
221
|
<div>
|
|
198
|
-
<label for="manual-end">End</label>
|
|
199
|
-
<
|
|
222
|
+
<label for="manual-end-date">End</label>
|
|
223
|
+
<div style="display:flex; gap:0.4rem;">
|
|
224
|
+
<input type="date" id="manual-end-date" style="flex:1;" />
|
|
225
|
+
<input type="time" id="manual-end-time" style="flex:1;" />
|
|
226
|
+
</div>
|
|
200
227
|
</div>
|
|
201
228
|
</div>
|
|
202
229
|
<div class="form-row">
|
|
203
|
-
<div>
|
|
230
|
+
<div id="manual-description-wrap">
|
|
204
231
|
<label for="manual-description">Description</label>
|
|
205
232
|
<input type="text" id="manual-description" placeholder="What did you work on?" />
|
|
233
|
+
<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
234
|
</div>
|
|
207
235
|
<div>
|
|
208
|
-
<label for="manual-jira">Jira Ticket (optional)</label>
|
|
236
|
+
<label for="manual-jira" id="manual-jira-label">Jira Ticket (optional)</label>
|
|
209
237
|
<input type="text" id="manual-jira" placeholder="e.g. PROJ-123" />
|
|
210
238
|
</div>
|
|
211
239
|
</div>
|
|
212
|
-
<
|
|
240
|
+
<div id="manual-description-preview" class="ticket-preview" style="display:none;"></div>
|
|
241
|
+
<label id="manual-billable-wrap" style="display:flex; align-items:center; gap:0.5rem; font-weight:normal; cursor:pointer;">
|
|
213
242
|
<input type="checkbox" id="manual-billable" checked style="width:auto; margin:0;" />
|
|
214
243
|
Billable
|
|
215
244
|
</label>
|
|
@@ -218,6 +247,7 @@ export function indexPage() {
|
|
|
218
247
|
</div>
|
|
219
248
|
</div>
|
|
220
249
|
|
|
250
|
+
|
|
221
251
|
<!-- Monitor Control -->
|
|
222
252
|
<div class="card">
|
|
223
253
|
<div class="card-header">
|
|
@@ -293,6 +323,13 @@ export function indexPage() {
|
|
|
293
323
|
<label for="clockify-key">API Key</label>
|
|
294
324
|
<input type="password" id="clockify-key" placeholder="Enter your Clockify API key" />
|
|
295
325
|
<button onclick="saveClockify()">Save & Validate</button>
|
|
326
|
+
<div style="display:flex; align-items:center; gap:0.6rem; margin-top:0.75rem;">
|
|
327
|
+
<label class="toggle">
|
|
328
|
+
<input type="checkbox" id="clockify-enabled-toggle" onchange="toggleClockifyEnabled()" />
|
|
329
|
+
<span class="slider"></span>
|
|
330
|
+
</label>
|
|
331
|
+
<span id="clockify-enabled-label" style="font-size:0.9rem; color:#8b949e;">Enabled</span>
|
|
332
|
+
</div>
|
|
296
333
|
<div class="msg" id="clockify-msg"></div>
|
|
297
334
|
</div>
|
|
298
335
|
|
|
@@ -304,6 +341,7 @@ export function indexPage() {
|
|
|
304
341
|
</div>
|
|
305
342
|
<p id="google-desc" style="font-size:0.85rem;color:#8b949e;margin-bottom:0.5rem;">Authorize access to your Google Calendar.</p>
|
|
306
343
|
<button class="connect" id="google-connect-btn" onclick="connectGoogle()">Connect Google Account</button>
|
|
344
|
+
<p id="google-connect-note" style="font-size:0.75rem;color:#8b949e;margin-top:0.5rem;display:none;"></p>
|
|
307
345
|
<div class="msg" id="google-msg"></div>
|
|
308
346
|
</div>
|
|
309
347
|
|
|
@@ -313,28 +351,28 @@ export function indexPage() {
|
|
|
313
351
|
<div class="dot gray" id="jira-dot"></div>
|
|
314
352
|
<h2>Jira</h2>
|
|
315
353
|
</div>
|
|
316
|
-
<p id="jira-desc" style="font-size:0.85rem;color:#8b949e;margin-bottom:0.5rem;">Connect
|
|
317
|
-
<
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
<div class="guide">
|
|
323
|
-
<ol>
|
|
324
|
-
<li>Go to <a href="https://id.atlassian.com/manage-profile/security/api-tokens" target="_blank">Atlassian API Tokens</a></li>
|
|
325
|
-
<li>Click <strong>Create API token</strong> and copy it</li>
|
|
326
|
-
<li>Your URL is <code>https://<your-org>.atlassian.net/rest/api/3</code></li>
|
|
327
|
-
</ol>
|
|
328
|
-
</div>
|
|
329
|
-
<label for="jira-url">Atlassian URL</label>
|
|
330
|
-
<input type="text" id="jira-url" placeholder="https://your-org.atlassian.net/rest/api/3" />
|
|
331
|
-
<label for="jira-email">Email</label>
|
|
332
|
-
<input type="email" id="jira-email" placeholder="you@example.com" />
|
|
333
|
-
<label for="jira-token">API Token</label>
|
|
334
|
-
<input type="password" id="jira-token" placeholder="Atlassian API token" />
|
|
335
|
-
<button onclick="saveJira()">Save & Validate</button>
|
|
336
|
-
</div>
|
|
354
|
+
<p id="jira-desc" style="font-size:0.85rem;color:#8b949e;margin-bottom:0.5rem;">Connect Jira with an API token to log time on tickets.</p>
|
|
355
|
+
<div class="guide">
|
|
356
|
+
<ol>
|
|
357
|
+
<li>Go to <a href="https://id.atlassian.com/manage-profile/security/api-tokens" target="_blank">Atlassian API Tokens</a></li>
|
|
358
|
+
<li>Click <strong>Create API token</strong> and copy it</li>
|
|
359
|
+
</ol>
|
|
337
360
|
</div>
|
|
361
|
+
<label for="jira-url">Atlassian URL</label>
|
|
362
|
+
<input type="text" id="jira-url" value="https://outsidetech.atlassian.net/rest/api/3" placeholder="https://your-org.atlassian.net/rest/api/3" />
|
|
363
|
+
<label for="jira-email">Email</label>
|
|
364
|
+
<input type="email" id="jira-email" placeholder="you@example.com" />
|
|
365
|
+
<label for="jira-token">API Token</label>
|
|
366
|
+
<input type="password" id="jira-token" placeholder="Atlassian API token" />
|
|
367
|
+
<button onclick="saveJira()">Save & Validate</button>
|
|
368
|
+
<div style="display:flex; align-items:center; gap:0.6rem; margin-top:0.75rem;">
|
|
369
|
+
<label class="toggle">
|
|
370
|
+
<input type="checkbox" id="jira-enabled-toggle" onchange="toggleJiraEnabled()" />
|
|
371
|
+
<span class="slider"></span>
|
|
372
|
+
</label>
|
|
373
|
+
<span id="jira-enabled-label" style="font-size:0.9rem; color:#8b949e;">Enabled</span>
|
|
374
|
+
</div>
|
|
375
|
+
<div class="msg" id="jira-msg"></div>
|
|
338
376
|
</div>
|
|
339
377
|
|
|
340
378
|
</div>
|
|
@@ -384,10 +422,26 @@ export function indexPage() {
|
|
|
384
422
|
</div>
|
|
385
423
|
|
|
386
424
|
<script>
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
425
|
+
// Suppress WebKit's Back/Forward menu. Capture-phase wins the race with
|
|
426
|
+
// the native WKWebView menu. Our custom menu offers Reload only.
|
|
427
|
+
(function wireContextMenu() {
|
|
428
|
+
const menu = document.getElementById('ctx-menu');
|
|
429
|
+
if (!menu) return;
|
|
430
|
+
const hide = () => { menu.style.display = 'none'; };
|
|
431
|
+
window.addEventListener('contextmenu', (e) => {
|
|
432
|
+
e.preventDefault();
|
|
433
|
+
const pad = 4;
|
|
434
|
+
const x = Math.min(e.clientX, window.innerWidth - menu.offsetWidth - pad);
|
|
435
|
+
const y = Math.min(e.clientY, window.innerHeight - menu.offsetHeight - pad);
|
|
436
|
+
menu.style.left = x + 'px';
|
|
437
|
+
menu.style.top = y + 'px';
|
|
438
|
+
menu.style.display = 'block';
|
|
439
|
+
}, { capture: true });
|
|
440
|
+
window.addEventListener('click', hide);
|
|
441
|
+
window.addEventListener('scroll', hide, { capture: true });
|
|
442
|
+
window.addEventListener('keydown', (e) => { if (e.key === 'Escape') hide(); });
|
|
443
|
+
window.addEventListener('blur', hide);
|
|
444
|
+
})();
|
|
391
445
|
|
|
392
446
|
let elapsedInterval = null;
|
|
393
447
|
let currentPage = 1;
|
|
@@ -395,10 +449,12 @@ export function indexPage() {
|
|
|
395
449
|
|
|
396
450
|
// --- Tab switching ---
|
|
397
451
|
function switchTab(tab) {
|
|
452
|
+
const nav = document.getElementById('nav-' + tab);
|
|
453
|
+
if (nav && nav.getAttribute('aria-disabled') === 'true') return;
|
|
398
454
|
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
|
|
399
455
|
document.querySelectorAll('.nav-btn').forEach(el => el.classList.remove('active'));
|
|
400
456
|
document.getElementById('tab-' + tab).classList.add('active');
|
|
401
|
-
|
|
457
|
+
if (nav) nav.classList.add('active');
|
|
402
458
|
}
|
|
403
459
|
|
|
404
460
|
function switchTrackMode(mode) {
|
|
@@ -475,12 +531,19 @@ export function indexPage() {
|
|
|
475
531
|
|
|
476
532
|
async function startTimer() {
|
|
477
533
|
const projectId = document.getElementById('project-select').value;
|
|
478
|
-
const description = document.getElementById('timer-description').value.trim();
|
|
479
534
|
const jiraTicket = document.getElementById('timer-jira').value.trim();
|
|
480
535
|
const billable = document.getElementById('timer-billable').checked;
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
536
|
+
const typedDescription = document.getElementById('timer-description').value.trim();
|
|
537
|
+
const description = typedDescription;
|
|
538
|
+
|
|
539
|
+
if (currentMode.clockifyOn) {
|
|
540
|
+
if (!projectId) return setMsg('timer-msg', 'Please select a project.', false);
|
|
541
|
+
if (!typedDescription && !jiraTicket) return setMsg('timer-msg', 'Please enter a description or Jira ticket.', false);
|
|
542
|
+
} else if (currentMode.jiraOn) {
|
|
543
|
+
if (!jiraTicket) return setMsg('timer-msg', 'Please enter a Jira ticket.', false);
|
|
544
|
+
} else {
|
|
545
|
+
if (!typedDescription) return setMsg('timer-msg', 'Please enter a description.', false);
|
|
546
|
+
}
|
|
484
547
|
|
|
485
548
|
const btn = document.getElementById('start-btn');
|
|
486
549
|
btn.disabled = true;
|
|
@@ -490,13 +553,20 @@ export function indexPage() {
|
|
|
490
553
|
const res = await fetch('/api/timer/start', {
|
|
491
554
|
method: 'POST',
|
|
492
555
|
headers: { 'Content-Type': 'application/json' },
|
|
493
|
-
body: JSON.stringify({
|
|
556
|
+
body: JSON.stringify({
|
|
557
|
+
projectId: projectId || undefined,
|
|
558
|
+
description: currentMode.clockifyOn ? (description || 'Working on a task...') : description,
|
|
559
|
+
jiraTicket: jiraTicket || undefined,
|
|
560
|
+
billable,
|
|
561
|
+
}),
|
|
494
562
|
});
|
|
495
563
|
const data = await res.json();
|
|
496
564
|
if (data.ok) {
|
|
497
565
|
setMsg('timer-msg', 'Timer started!', true);
|
|
498
566
|
document.getElementById('timer-description').value = '';
|
|
499
567
|
document.getElementById('timer-jira').value = '';
|
|
568
|
+
timerJiraSummary = '';
|
|
569
|
+
renderTicketPreview('timer-description-preview', '', '');
|
|
500
570
|
checkActiveTimer();
|
|
501
571
|
loadSessions();
|
|
502
572
|
} else {
|
|
@@ -529,37 +599,55 @@ export function indexPage() {
|
|
|
529
599
|
}
|
|
530
600
|
}
|
|
531
601
|
|
|
532
|
-
function
|
|
602
|
+
function toDateInputValue(date) {
|
|
603
|
+
const pad = function(n) { return String(n).padStart(2, '0'); };
|
|
604
|
+
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate());
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function toTimeInputValue(date) {
|
|
533
608
|
const pad = function(n) { return String(n).padStart(2, '0'); };
|
|
534
|
-
return
|
|
535
|
-
'T' + pad(date.getHours()) + ':' + pad(date.getMinutes());
|
|
609
|
+
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
|
536
610
|
}
|
|
537
611
|
|
|
538
612
|
function setManualDefaults() {
|
|
539
613
|
const now = new Date();
|
|
540
614
|
const hourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
|
541
|
-
const
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
615
|
+
const sd = document.getElementById('manual-start-date');
|
|
616
|
+
const st = document.getElementById('manual-start-time');
|
|
617
|
+
const ed = document.getElementById('manual-end-date');
|
|
618
|
+
const et = document.getElementById('manual-end-time');
|
|
619
|
+
if (sd) sd.value = toDateInputValue(hourAgo);
|
|
620
|
+
if (st) st.value = toTimeInputValue(hourAgo);
|
|
621
|
+
if (ed) ed.value = toDateInputValue(now);
|
|
622
|
+
if (et) et.value = toTimeInputValue(now);
|
|
545
623
|
}
|
|
546
624
|
|
|
547
625
|
async function logManualTime() {
|
|
548
626
|
const projectId = document.getElementById('manual-project').value;
|
|
549
|
-
const
|
|
550
|
-
const
|
|
551
|
-
const
|
|
627
|
+
const startDate = document.getElementById('manual-start-date').value;
|
|
628
|
+
const startTime = document.getElementById('manual-start-time').value;
|
|
629
|
+
const endDate = document.getElementById('manual-end-date').value;
|
|
630
|
+
const endTime = document.getElementById('manual-end-time').value;
|
|
631
|
+
const typedDescription = document.getElementById('manual-description').value.trim();
|
|
552
632
|
const jiraTicket = document.getElementById('manual-jira').value.trim();
|
|
553
633
|
const billable = document.getElementById('manual-billable').checked;
|
|
634
|
+
const description = typedDescription;
|
|
554
635
|
|
|
555
|
-
if (!
|
|
556
|
-
if (!startVal || !endVal) return setMsg('manual-msg', 'Please set start and end.', false);
|
|
636
|
+
if (!startDate || !startTime || !endDate || !endTime) return setMsg('manual-msg', 'Please set start and end.', false);
|
|
557
637
|
|
|
558
|
-
const startMs = new Date(
|
|
559
|
-
const endMs = new Date(
|
|
638
|
+
const startMs = new Date(startDate + 'T' + startTime).getTime();
|
|
639
|
+
const endMs = new Date(endDate + 'T' + endTime).getTime();
|
|
560
640
|
if (isNaN(startMs) || isNaN(endMs)) return setMsg('manual-msg', 'Invalid date.', false);
|
|
561
641
|
if (endMs <= startMs) return setMsg('manual-msg', 'End must be after start.', false);
|
|
562
|
-
|
|
642
|
+
|
|
643
|
+
if (currentMode.clockifyOn) {
|
|
644
|
+
if (!projectId) return setMsg('manual-msg', 'Please select a project.', false);
|
|
645
|
+
if (!typedDescription && !jiraTicket) return setMsg('manual-msg', 'Please enter a description or Jira ticket.', false);
|
|
646
|
+
} else if (currentMode.jiraOn) {
|
|
647
|
+
if (!jiraTicket) return setMsg('manual-msg', 'Please enter a Jira ticket.', false);
|
|
648
|
+
} else {
|
|
649
|
+
if (!typedDescription) return setMsg('manual-msg', 'Please enter a description.', false);
|
|
650
|
+
}
|
|
563
651
|
|
|
564
652
|
const btn = document.getElementById('manual-log-btn');
|
|
565
653
|
btn.disabled = true;
|
|
@@ -570,7 +658,7 @@ export function indexPage() {
|
|
|
570
658
|
method: 'POST',
|
|
571
659
|
headers: { 'Content-Type': 'application/json' },
|
|
572
660
|
body: JSON.stringify({
|
|
573
|
-
projectId: projectId,
|
|
661
|
+
projectId: projectId || undefined,
|
|
574
662
|
description: description,
|
|
575
663
|
start: new Date(startMs).toISOString(),
|
|
576
664
|
end: new Date(endMs).toISOString(),
|
|
@@ -583,6 +671,8 @@ export function indexPage() {
|
|
|
583
671
|
setMsg('manual-msg', 'Time logged.', true);
|
|
584
672
|
document.getElementById('manual-description').value = '';
|
|
585
673
|
document.getElementById('manual-jira').value = '';
|
|
674
|
+
manualJiraSummary = '';
|
|
675
|
+
renderTicketPreview('manual-description-preview', '', '');
|
|
586
676
|
setManualDefaults();
|
|
587
677
|
loadSessions();
|
|
588
678
|
} else {
|
|
@@ -893,7 +983,7 @@ export function indexPage() {
|
|
|
893
983
|
: '';
|
|
894
984
|
return '<tr>' +
|
|
895
985
|
'<td>' + escapeHtml(s.description) + '</td>' +
|
|
896
|
-
'<td>' + escapeHtml(s.projectName) + '</td>' +
|
|
986
|
+
'<td>' + (s.projectName ? escapeHtml(s.projectName) : '—') + '</td>' +
|
|
897
987
|
'<td>' + started + '</td>' +
|
|
898
988
|
'<td>' + duration + '</td>' +
|
|
899
989
|
'<td>' + escapeHtml(jira) + '</td>' +
|
|
@@ -992,6 +1082,7 @@ export function indexPage() {
|
|
|
992
1082
|
if (data.ok) {
|
|
993
1083
|
setMsg('clockify-msg', 'Saved and validated successfully.', true);
|
|
994
1084
|
setDot('clockify-dot', 'green');
|
|
1085
|
+
fetchStatus();
|
|
995
1086
|
} else {
|
|
996
1087
|
setMsg('clockify-msg', data.error || 'Validation failed.', false);
|
|
997
1088
|
setDot('clockify-dot', 'red');
|
|
@@ -1001,6 +1092,27 @@ export function indexPage() {
|
|
|
1001
1092
|
}
|
|
1002
1093
|
}
|
|
1003
1094
|
|
|
1095
|
+
async function toggleClockifyEnabled() {
|
|
1096
|
+
const enabled = document.getElementById('clockify-enabled-toggle').checked;
|
|
1097
|
+
document.getElementById('clockify-enabled-label').textContent = enabled ? 'Enabled' : 'Disabled';
|
|
1098
|
+
try {
|
|
1099
|
+
const res = await fetch('/api/clockify/enabled', {
|
|
1100
|
+
method: 'POST',
|
|
1101
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1102
|
+
body: JSON.stringify({ enabled }),
|
|
1103
|
+
});
|
|
1104
|
+
const data = await res.json();
|
|
1105
|
+
if (data.ok) {
|
|
1106
|
+
setMsg('clockify-msg', enabled ? 'Clockify enabled.' : 'Clockify disabled.', true);
|
|
1107
|
+
fetchStatus();
|
|
1108
|
+
} else {
|
|
1109
|
+
setMsg('clockify-msg', data.error || 'Failed to update.', false);
|
|
1110
|
+
}
|
|
1111
|
+
} catch {
|
|
1112
|
+
setMsg('clockify-msg', 'Request failed.', false);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1004
1116
|
// --- Settings: Google ---
|
|
1005
1117
|
function setGoogleConnected(connected, email) {
|
|
1006
1118
|
const btn = document.getElementById('google-connect-btn');
|
|
@@ -1031,48 +1143,35 @@ export function indexPage() {
|
|
|
1031
1143
|
}
|
|
1032
1144
|
|
|
1033
1145
|
// --- Settings: Jira ---
|
|
1034
|
-
function setJiraConnected(connected
|
|
1035
|
-
const btn = document.getElementById('jira-connect-btn');
|
|
1146
|
+
function setJiraConnected(connected) {
|
|
1036
1147
|
const desc = document.getElementById('jira-desc');
|
|
1037
|
-
if (connected
|
|
1038
|
-
btn.textContent = 'Reconnect';
|
|
1039
|
-
desc.textContent = 'Connected via OAuth' + (siteUrl ? ' (' + siteUrl.replace('https://', '') + ')' : '');
|
|
1040
|
-
desc.style.color = '#3fb950';
|
|
1041
|
-
} else if (connected) {
|
|
1042
|
-
btn.textContent = 'Reconnect';
|
|
1148
|
+
if (connected) {
|
|
1043
1149
|
desc.textContent = 'Connected via API token';
|
|
1044
1150
|
desc.style.color = '#3fb950';
|
|
1045
1151
|
} else {
|
|
1046
|
-
|
|
1047
|
-
desc.textContent = 'Connect your Atlassian account to log time on Jira tickets.';
|
|
1152
|
+
desc.textContent = 'Connect Jira with an API token to log time on tickets.';
|
|
1048
1153
|
desc.style.color = '#8b949e';
|
|
1049
1154
|
}
|
|
1050
1155
|
}
|
|
1051
1156
|
|
|
1052
|
-
async function
|
|
1157
|
+
async function toggleJiraEnabled() {
|
|
1158
|
+
const enabled = document.getElementById('jira-enabled-toggle').checked;
|
|
1159
|
+
document.getElementById('jira-enabled-label').textContent = enabled ? 'Enabled' : 'Disabled';
|
|
1053
1160
|
try {
|
|
1054
|
-
const res = await fetch('/api/jira/
|
|
1161
|
+
const res = await fetch('/api/jira/enabled', {
|
|
1162
|
+
method: 'POST',
|
|
1163
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1164
|
+
body: JSON.stringify({ enabled }),
|
|
1165
|
+
});
|
|
1055
1166
|
const data = await res.json();
|
|
1056
|
-
if (data.
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
}
|
|
1167
|
+
if (data.ok) {
|
|
1168
|
+
setMsg('jira-msg', enabled ? 'Jira enabled.' : 'Jira disabled.', true);
|
|
1169
|
+
fetchStatus();
|
|
1170
|
+
} else {
|
|
1171
|
+
setMsg('jira-msg', data.error || 'Failed to update.', false);
|
|
1062
1172
|
}
|
|
1063
|
-
} catch
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
function toggleJiraForm(e) {
|
|
1067
|
-
e.preventDefault();
|
|
1068
|
-
const form = document.getElementById('jira-form');
|
|
1069
|
-
const toggle = document.getElementById('jira-toggle');
|
|
1070
|
-
if (form.style.display === 'none') {
|
|
1071
|
-
form.style.display = 'block';
|
|
1072
|
-
toggle.innerHTML = 'hide API token form ↑';
|
|
1073
|
-
} else {
|
|
1074
|
-
form.style.display = 'none';
|
|
1075
|
-
toggle.innerHTML = 'or use API token ↓';
|
|
1173
|
+
} catch {
|
|
1174
|
+
setMsg('jira-msg', 'Request failed.', false);
|
|
1076
1175
|
}
|
|
1077
1176
|
}
|
|
1078
1177
|
|
|
@@ -1091,6 +1190,7 @@ export function indexPage() {
|
|
|
1091
1190
|
if (data.ok) {
|
|
1092
1191
|
setMsg('jira-msg', 'Saved and validated successfully.', true);
|
|
1093
1192
|
setDot('jira-dot', 'green');
|
|
1193
|
+
fetchStatus();
|
|
1094
1194
|
} else {
|
|
1095
1195
|
setMsg('jira-msg', data.error || 'Validation failed.', false);
|
|
1096
1196
|
setDot('jira-dot', 'red');
|
|
@@ -1101,6 +1201,169 @@ export function indexPage() {
|
|
|
1101
1201
|
}
|
|
1102
1202
|
|
|
1103
1203
|
// --- Status ---
|
|
1204
|
+
var currentMode = { clockifyOn: true, jiraOn: false };
|
|
1205
|
+
var timerJiraSummary = '';
|
|
1206
|
+
var manualJiraSummary = '';
|
|
1207
|
+
|
|
1208
|
+
function renderTicketPreview(previewId, ticket, description) {
|
|
1209
|
+
var el = document.getElementById(previewId);
|
|
1210
|
+
if (!el) return;
|
|
1211
|
+
if (!ticket || !description) {
|
|
1212
|
+
el.style.display = 'none';
|
|
1213
|
+
el.innerHTML = '';
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
el.style.display = '';
|
|
1217
|
+
el.innerHTML = '<span class="ticket-id">' + escapeHtml(ticket) + '</span>' + escapeHtml(description);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
async function fetchTicketSummary(ticket) {
|
|
1221
|
+
if (!/^[A-Z][A-Z0-9]+-\d+$/.test(ticket)) return null;
|
|
1222
|
+
try {
|
|
1223
|
+
const res = await fetch('/api/jira/ticket-summary?jira=' + encodeURIComponent(ticket));
|
|
1224
|
+
const data = await res.json();
|
|
1225
|
+
if (data.ok) return data.description || '';
|
|
1226
|
+
} catch {}
|
|
1227
|
+
return null;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function debounce(fn, ms) {
|
|
1231
|
+
var t;
|
|
1232
|
+
return function() {
|
|
1233
|
+
var ctx = this, args = arguments;
|
|
1234
|
+
clearTimeout(t);
|
|
1235
|
+
t = setTimeout(function(){ fn.apply(ctx, args); }, ms);
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
async function onTimerJiraInput() {
|
|
1240
|
+
var ticket = document.getElementById('timer-jira').value.trim().toUpperCase();
|
|
1241
|
+
if (!currentMode.clockifyOn) {
|
|
1242
|
+
var desc = await fetchTicketSummary(ticket);
|
|
1243
|
+
timerJiraSummary = desc || '';
|
|
1244
|
+
renderTicketPreview('timer-description-preview', ticket, timerJiraSummary);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
async function onManualJiraInput() {
|
|
1248
|
+
var ticket = document.getElementById('manual-jira').value.trim().toUpperCase();
|
|
1249
|
+
if (!currentMode.clockifyOn) {
|
|
1250
|
+
var desc = await fetchTicketSummary(ticket);
|
|
1251
|
+
manualJiraSummary = desc || '';
|
|
1252
|
+
renderTicketPreview('manual-description-preview', ticket, manualJiraSummary);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
(function wireTicketPreviewInputs() {
|
|
1257
|
+
var t = document.getElementById('timer-jira');
|
|
1258
|
+
var m = document.getElementById('manual-jira');
|
|
1259
|
+
if (t) t.addEventListener('input', debounce(onTimerJiraInput, 300));
|
|
1260
|
+
if (m) m.addEventListener('input', debounce(onManualJiraInput, 300));
|
|
1261
|
+
})();
|
|
1262
|
+
|
|
1263
|
+
function applyMode(data) {
|
|
1264
|
+
const clockifyOn = !!data.clockify;
|
|
1265
|
+
const jiraOn = !!data.jira;
|
|
1266
|
+
const localOnly = !clockifyOn && !jiraOn;
|
|
1267
|
+
currentMode.clockifyOn = clockifyOn;
|
|
1268
|
+
currentMode.jiraOn = jiraOn;
|
|
1269
|
+
|
|
1270
|
+
const trackCard = document.getElementById('track-card');
|
|
1271
|
+
const timerLocalHint = document.getElementById('timer-local-hint');
|
|
1272
|
+
const manualLocalHint = document.getElementById('manual-local-hint');
|
|
1273
|
+
const projWrap = document.getElementById('timer-project-wrap');
|
|
1274
|
+
const manualProjWrap = document.getElementById('manual-project-wrap');
|
|
1275
|
+
const descWrap = document.getElementById('timer-description-wrap');
|
|
1276
|
+
const manualDescWrap = document.getElementById('manual-description-wrap');
|
|
1277
|
+
const descPreview = document.getElementById('timer-description-preview');
|
|
1278
|
+
const manualDescPreview = document.getElementById('manual-description-preview');
|
|
1279
|
+
const jiraInput = document.getElementById('timer-jira');
|
|
1280
|
+
const manualJiraInput = document.getElementById('manual-jira');
|
|
1281
|
+
const jiraLabel = document.getElementById('timer-jira-label');
|
|
1282
|
+
const manualJiraLabel = document.getElementById('manual-jira-label');
|
|
1283
|
+
const jiraWrap = jiraInput ? jiraInput.closest('.row') || jiraInput.parentElement : null;
|
|
1284
|
+
const manualJiraWrap = manualJiraInput ? manualJiraInput.closest('.row') || manualJiraInput.parentElement : null;
|
|
1285
|
+
const billableWrap = document.getElementById('timer-billable-wrap');
|
|
1286
|
+
const manualBillableWrap = document.getElementById('manual-billable-wrap');
|
|
1287
|
+
|
|
1288
|
+
if (trackCard) trackCard.style.display = '';
|
|
1289
|
+
if (timerLocalHint) timerLocalHint.style.display = localOnly ? '' : 'none';
|
|
1290
|
+
if (manualLocalHint) manualLocalHint.style.display = localOnly ? '' : 'none';
|
|
1291
|
+
|
|
1292
|
+
if (clockifyOn) {
|
|
1293
|
+
if (projWrap) projWrap.style.display = '';
|
|
1294
|
+
if (manualProjWrap) manualProjWrap.style.display = '';
|
|
1295
|
+
if (descWrap) descWrap.style.display = '';
|
|
1296
|
+
if (manualDescWrap) manualDescWrap.style.display = '';
|
|
1297
|
+
if (descPreview) descPreview.style.display = 'none';
|
|
1298
|
+
if (manualDescPreview) manualDescPreview.style.display = 'none';
|
|
1299
|
+
if (jiraWrap) jiraWrap.style.display = jiraOn ? '' : 'none';
|
|
1300
|
+
if (manualJiraWrap) manualJiraWrap.style.display = jiraOn ? '' : 'none';
|
|
1301
|
+
if (jiraInput) jiraInput.required = false;
|
|
1302
|
+
if (manualJiraInput) manualJiraInput.required = false;
|
|
1303
|
+
if (jiraLabel) jiraLabel.textContent = 'Jira Ticket (optional)';
|
|
1304
|
+
if (manualJiraLabel) manualJiraLabel.textContent = 'Jira Ticket (optional)';
|
|
1305
|
+
if (billableWrap) billableWrap.style.display = '';
|
|
1306
|
+
if (manualBillableWrap) manualBillableWrap.style.display = '';
|
|
1307
|
+
} else if (jiraOn) {
|
|
1308
|
+
if (projWrap) projWrap.style.display = 'none';
|
|
1309
|
+
if (manualProjWrap) manualProjWrap.style.display = 'none';
|
|
1310
|
+
if (descWrap) descWrap.style.display = 'none';
|
|
1311
|
+
if (manualDescWrap) manualDescWrap.style.display = 'none';
|
|
1312
|
+
if (descPreview) { descPreview.style.display = ''; renderTicketPreview('timer-description-preview', (jiraInput ? jiraInput.value.trim().toUpperCase() : ''), timerJiraSummary); }
|
|
1313
|
+
if (manualDescPreview) { manualDescPreview.style.display = ''; renderTicketPreview('manual-description-preview', (manualJiraInput ? manualJiraInput.value.trim().toUpperCase() : ''), manualJiraSummary); }
|
|
1314
|
+
if (jiraWrap) jiraWrap.style.display = '';
|
|
1315
|
+
if (manualJiraWrap) manualJiraWrap.style.display = '';
|
|
1316
|
+
if (jiraInput) jiraInput.required = true;
|
|
1317
|
+
if (manualJiraInput) manualJiraInput.required = true;
|
|
1318
|
+
if (jiraLabel) jiraLabel.textContent = 'Jira Ticket';
|
|
1319
|
+
if (manualJiraLabel) manualJiraLabel.textContent = 'Jira Ticket';
|
|
1320
|
+
if (billableWrap) billableWrap.style.display = 'none';
|
|
1321
|
+
if (manualBillableWrap) manualBillableWrap.style.display = 'none';
|
|
1322
|
+
onTimerJiraInput();
|
|
1323
|
+
onManualJiraInput();
|
|
1324
|
+
} else {
|
|
1325
|
+
// Local-only: description only
|
|
1326
|
+
if (projWrap) projWrap.style.display = 'none';
|
|
1327
|
+
if (manualProjWrap) manualProjWrap.style.display = 'none';
|
|
1328
|
+
if (descWrap) descWrap.style.display = '';
|
|
1329
|
+
if (manualDescWrap) manualDescWrap.style.display = '';
|
|
1330
|
+
if (descPreview) descPreview.style.display = 'none';
|
|
1331
|
+
if (manualDescPreview) manualDescPreview.style.display = 'none';
|
|
1332
|
+
if (jiraWrap) jiraWrap.style.display = 'none';
|
|
1333
|
+
if (manualJiraWrap) manualJiraWrap.style.display = 'none';
|
|
1334
|
+
if (jiraInput) jiraInput.required = false;
|
|
1335
|
+
if (manualJiraInput) manualJiraInput.required = false;
|
|
1336
|
+
if (billableWrap) billableWrap.style.display = 'none';
|
|
1337
|
+
if (manualBillableWrap) manualBillableWrap.style.display = 'none';
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// Projects tab: hide "Pull from Clockify" button
|
|
1341
|
+
const pullBtn = document.getElementById('fetch-projects-btn');
|
|
1342
|
+
if (pullBtn) pullBtn.style.display = clockifyOn ? '' : 'none';
|
|
1343
|
+
|
|
1344
|
+
// Projects nav: hide entire tab when Clockify off
|
|
1345
|
+
const projectsNav = document.getElementById('nav-projects');
|
|
1346
|
+
if (projectsNav) projectsNav.style.display = clockifyOn ? '' : 'none';
|
|
1347
|
+
|
|
1348
|
+
// Calendar nav: hide entire tab when Clockify off
|
|
1349
|
+
const calNav = document.getElementById('nav-calendar');
|
|
1350
|
+
if (calNav) calNav.style.display = clockifyOn ? '' : 'none';
|
|
1351
|
+
|
|
1352
|
+
// Settings: Google Connect button — disable when Clockify off
|
|
1353
|
+
const gBtn = document.getElementById('google-connect-btn');
|
|
1354
|
+
const gNote = document.getElementById('google-connect-note');
|
|
1355
|
+
if (gBtn) gBtn.disabled = !clockifyOn;
|
|
1356
|
+
if (gNote) {
|
|
1357
|
+
if (clockifyOn) {
|
|
1358
|
+
gNote.textContent = '';
|
|
1359
|
+
gNote.style.display = 'none';
|
|
1360
|
+
} else {
|
|
1361
|
+
gNote.textContent = 'Calendar sync requires Clockify. Connect Clockify first.';
|
|
1362
|
+
gNote.style.display = '';
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1104
1367
|
async function fetchStatus() {
|
|
1105
1368
|
try {
|
|
1106
1369
|
const res = await fetch('/api/status');
|
|
@@ -1111,8 +1374,34 @@ export function indexPage() {
|
|
|
1111
1374
|
if (data.clockifyKeyHint) {
|
|
1112
1375
|
document.getElementById('clockify-key').placeholder = data.clockifyKeyHint;
|
|
1113
1376
|
}
|
|
1377
|
+
const toggle = document.getElementById('clockify-enabled-toggle');
|
|
1378
|
+
const toggleLabel = document.getElementById('clockify-enabled-label');
|
|
1379
|
+
const keyInput = document.getElementById('clockify-key');
|
|
1380
|
+
const saveBtn = document.querySelector('button[onclick="saveClockify()"]');
|
|
1381
|
+
const enabled = !data.clockifyDisabled;
|
|
1382
|
+
if (toggle) toggle.checked = enabled;
|
|
1383
|
+
if (toggleLabel) toggleLabel.textContent = enabled ? 'Enabled' : 'Disabled';
|
|
1384
|
+
if (keyInput) keyInput.disabled = !enabled;
|
|
1385
|
+
if (saveBtn) saveBtn.disabled = !enabled;
|
|
1386
|
+
|
|
1387
|
+
const jToggle = document.getElementById('jira-enabled-toggle');
|
|
1388
|
+
const jToggleLabel = document.getElementById('jira-enabled-label');
|
|
1389
|
+
const jToggleWrap = jToggle ? jToggle.closest('label').parentElement : null;
|
|
1390
|
+
const jUrl = document.getElementById('jira-url');
|
|
1391
|
+
const jEmail = document.getElementById('jira-email');
|
|
1392
|
+
const jTokenInput = document.getElementById('jira-token');
|
|
1393
|
+
const jSaveBtn = document.querySelector('button[onclick="saveJira()"]');
|
|
1394
|
+
const jiraEnabled = !data.jiraDisabled;
|
|
1395
|
+
if (jToggle) jToggle.checked = jiraEnabled;
|
|
1396
|
+
if (jToggleLabel) jToggleLabel.textContent = jiraEnabled ? 'Enabled' : 'Disabled';
|
|
1397
|
+
if (jToggleWrap) jToggleWrap.style.display = data.jiraConfigured ? '' : 'none';
|
|
1398
|
+
if (jUrl) jUrl.disabled = data.jiraConfigured && !jiraEnabled;
|
|
1399
|
+
if (jEmail) jEmail.disabled = data.jiraConfigured && !jiraEnabled;
|
|
1400
|
+
if (jTokenInput) jTokenInput.disabled = data.jiraConfigured && !jiraEnabled;
|
|
1401
|
+
if (jSaveBtn) jSaveBtn.disabled = data.jiraConfigured && !jiraEnabled;
|
|
1114
1402
|
setGoogleConnected(data.google, data.googleEmail);
|
|
1115
|
-
setJiraConnected(data.jira
|
|
1403
|
+
setJiraConnected(data.jira);
|
|
1404
|
+
applyMode(data);
|
|
1116
1405
|
} catch {}
|
|
1117
1406
|
}
|
|
1118
1407
|
|