clocktopus 1.6.17 → 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.
@@ -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 oncontextmenu="return false;">
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
- <label for="project-select">Project</label>
164
- <select id="project-select">
165
- <option value="">Loading projects...</option>
166
- </select>
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
- <label style="display:flex; align-items:center; gap:0.5rem; font-weight:normal; cursor:pointer;">
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
- <label for="manual-project">Project</label>
189
- <select id="manual-project">
190
- <option value="">Loading projects...</option>
191
- </select>
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
- <label style="display:flex; align-items:center; gap:0.5rem; font-weight:normal; cursor:pointer;">
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 &amp; 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 &darr;</a>
@@ -384,10 +423,26 @@ export function indexPage() {
384
423
  </div>
385
424
 
386
425
  <script>
387
- // Disable WebKit's Back/Forward/Reload context menu. Capture-phase and
388
- // attached on window so it runs before any app handler and wins the race
389
- // with the native WKWebView menu.
390
- window.addEventListener('contextmenu', e => e.preventDefault(), { capture: true });
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
- document.getElementById('nav-' + tab).classList.add('active');
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
- if (!projectId) return setMsg('timer-msg', 'Please select a project.', false);
483
- if (!description && !jiraTicket) return setMsg('timer-msg', 'Please enter a description or Jira ticket.', false);
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({ projectId, description: description || 'Working on a task...', jiraTicket: jiraTicket || undefined, billable }),
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 description = document.getElementById('manual-description').value.trim();
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
- if (!description && !jiraTicket) return setMsg('manual-msg', 'Please enter a description or Jira ticket.', false);
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