clocktopus 1.8.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.
@@ -1,7 +1,7 @@
1
1
  import { Hono } from 'hono';
2
2
  import axios from 'axios';
3
3
  import { saveCredential, setJiraDisabled } from '../../lib/credentials.js';
4
- import { storeAtlassianToken } from '../../lib/db.js';
4
+ import { clearAtlassianToken, storeAtlassianToken } from '../../lib/db.js';
5
5
  import { getAtlassianAuthUrl, exchangeCodeForTokens, getAccessibleResources } from '../../lib/atlassian.js';
6
6
  const jiraRoutes = new Hono();
7
7
  jiraRoutes.post('/jira/enabled', async (c) => {
@@ -75,6 +75,7 @@ jiraRoutes.post('/jira', async (c) => {
75
75
  saveCredential('ATLASSIAN_URL', url);
76
76
  saveCredential('ATLASSIAN_EMAIL', email);
77
77
  saveCredential('ATLASSIAN_API_TOKEN', token);
78
+ clearAtlassianToken();
78
79
  return c.json({ ok: true, user: res.data.displayName });
79
80
  }
80
81
  return c.json({ ok: false, error: 'Invalid credentials.' });
@@ -212,12 +212,18 @@ export function indexPage() {
212
212
  </div>
213
213
  <div class="form-row">
214
214
  <div>
215
- <label for="manual-start">Start</label>
216
- <input type="datetime-local" id="manual-start" />
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>
217
220
  </div>
218
221
  <div>
219
- <label for="manual-end">End</label>
220
- <input type="datetime-local" id="manual-end" />
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>
221
227
  </div>
222
228
  </div>
223
229
  <div class="form-row">
@@ -345,8 +351,20 @@ export function indexPage() {
345
351
  <div class="dot gray" id="jira-dot"></div>
346
352
  <h2>Jira</h2>
347
353
  </div>
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>
349
- <button class="connect" id="jira-connect-btn" onclick="connectJira()">Connect Atlassian</button>
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>
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 &amp; Validate</button>
350
368
  <div style="display:flex; align-items:center; gap:0.6rem; margin-top:0.75rem;">
351
369
  <label class="toggle">
352
370
  <input type="checkbox" id="jira-enabled-toggle" onchange="toggleJiraEnabled()" />
@@ -355,25 +373,6 @@ export function indexPage() {
355
373
  <span id="jira-enabled-label" style="font-size:0.9rem; color:#8b949e;">Enabled</span>
356
374
  </div>
357
375
  <div class="msg" id="jira-msg"></div>
358
- <div style="margin-top:1rem;">
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>
360
- <div id="jira-form" style="display:none;margin-top:0.5rem;">
361
- <div class="guide">
362
- <ol>
363
- <li>Go to <a href="https://id.atlassian.com/manage-profile/security/api-tokens" target="_blank">Atlassian API Tokens</a></li>
364
- <li>Click <strong>Create API token</strong> and copy it</li>
365
- <li>Your URL is <code>https://&lt;your-org&gt;.atlassian.net/rest/api/3</code></li>
366
- </ol>
367
- </div>
368
- <label for="jira-url">Atlassian URL</label>
369
- <input type="text" id="jira-url" placeholder="https://your-org.atlassian.net/rest/api/3" />
370
- <label for="jira-email">Email</label>
371
- <input type="email" id="jira-email" placeholder="you@example.com" />
372
- <label for="jira-token">API Token</label>
373
- <input type="password" id="jira-token" placeholder="Atlassian API token" />
374
- <button onclick="saveJira()">Save &amp; Validate</button>
375
- </div>
376
- </div>
377
376
  </div>
378
377
 
379
378
  </div>
@@ -600,34 +599,44 @@ export function indexPage() {
600
599
  }
601
600
  }
602
601
 
603
- function toLocalInputValue(date) {
602
+ function toDateInputValue(date) {
604
603
  const pad = function(n) { return String(n).padStart(2, '0'); };
605
- return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) +
606
- 'T' + pad(date.getHours()) + ':' + pad(date.getMinutes());
604
+ return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate());
605
+ }
606
+
607
+ function toTimeInputValue(date) {
608
+ const pad = function(n) { return String(n).padStart(2, '0'); };
609
+ return pad(date.getHours()) + ':' + pad(date.getMinutes());
607
610
  }
608
611
 
609
612
  function setManualDefaults() {
610
613
  const now = new Date();
611
614
  const hourAgo = new Date(now.getTime() - 60 * 60 * 1000);
612
- const startEl = document.getElementById('manual-start');
613
- const endEl = document.getElementById('manual-end');
614
- if (startEl) startEl.value = toLocalInputValue(hourAgo);
615
- if (endEl) endEl.value = toLocalInputValue(now);
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);
616
623
  }
617
624
 
618
625
  async function logManualTime() {
619
626
  const projectId = document.getElementById('manual-project').value;
620
- const startVal = document.getElementById('manual-start').value;
621
- const endVal = document.getElementById('manual-end').value;
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;
622
631
  const typedDescription = document.getElementById('manual-description').value.trim();
623
632
  const jiraTicket = document.getElementById('manual-jira').value.trim();
624
633
  const billable = document.getElementById('manual-billable').checked;
625
634
  const description = typedDescription;
626
635
 
627
- 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);
628
637
 
629
- const startMs = new Date(startVal).getTime();
630
- const endMs = new Date(endVal).getTime();
638
+ const startMs = new Date(startDate + 'T' + startTime).getTime();
639
+ const endMs = new Date(endDate + 'T' + endTime).getTime();
631
640
  if (isNaN(startMs) || isNaN(endMs)) return setMsg('manual-msg', 'Invalid date.', false);
632
641
  if (endMs <= startMs) return setMsg('manual-msg', 'End must be after start.', false);
633
642
 
@@ -1073,6 +1082,7 @@ export function indexPage() {
1073
1082
  if (data.ok) {
1074
1083
  setMsg('clockify-msg', 'Saved and validated successfully.', true);
1075
1084
  setDot('clockify-dot', 'green');
1085
+ fetchStatus();
1076
1086
  } else {
1077
1087
  setMsg('clockify-msg', data.error || 'Validation failed.', false);
1078
1088
  setDot('clockify-dot', 'red');
@@ -1133,51 +1143,17 @@ export function indexPage() {
1133
1143
  }
1134
1144
 
1135
1145
  // --- Settings: Jira ---
1136
- function setJiraConnected(connected, isOAuth, siteUrl) {
1137
- const btn = document.getElementById('jira-connect-btn');
1146
+ function setJiraConnected(connected) {
1138
1147
  const desc = document.getElementById('jira-desc');
1139
- if (connected && isOAuth) {
1140
- btn.textContent = 'Reconnect';
1141
- desc.textContent = 'Connected via OAuth' + (siteUrl ? ' (' + siteUrl.replace('https://', '') + ')' : '');
1142
- desc.style.color = '#3fb950';
1143
- } else if (connected) {
1144
- btn.textContent = 'Reconnect';
1148
+ if (connected) {
1145
1149
  desc.textContent = 'Connected via API token';
1146
1150
  desc.style.color = '#3fb950';
1147
1151
  } else {
1148
- btn.textContent = 'Connect Atlassian';
1149
- 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.';
1150
1153
  desc.style.color = '#8b949e';
1151
1154
  }
1152
1155
  }
1153
1156
 
1154
- async function connectJira() {
1155
- try {
1156
- const res = await fetch('/api/jira/auth-url');
1157
- const data = await res.json();
1158
- if (data.url) {
1159
- if (window.__TAURI__) {
1160
- window.__TAURI__.opener.openUrl(data.url);
1161
- } else {
1162
- window.location.href = '/api/jira/connect';
1163
- }
1164
- }
1165
- } catch (e) { console.error('Connect Jira error:', e); }
1166
- }
1167
-
1168
- function toggleJiraForm(e) {
1169
- e.preventDefault();
1170
- const form = document.getElementById('jira-form');
1171
- const toggle = document.getElementById('jira-toggle');
1172
- if (form.style.display === 'none') {
1173
- form.style.display = 'block';
1174
- toggle.innerHTML = 'hide API token form &uarr;';
1175
- } else {
1176
- form.style.display = 'none';
1177
- toggle.innerHTML = 'or use API token &darr;';
1178
- }
1179
- }
1180
-
1181
1157
  async function toggleJiraEnabled() {
1182
1158
  const enabled = document.getElementById('jira-enabled-toggle').checked;
1183
1159
  document.getElementById('jira-enabled-label').textContent = enabled ? 'Enabled' : 'Disabled';
@@ -1214,6 +1190,7 @@ export function indexPage() {
1214
1190
  if (data.ok) {
1215
1191
  setMsg('jira-msg', 'Saved and validated successfully.', true);
1216
1192
  setDot('jira-dot', 'green');
1193
+ fetchStatus();
1217
1194
  } else {
1218
1195
  setMsg('jira-msg', data.error || 'Validation failed.', false);
1219
1196
  setDot('jira-dot', 'red');
@@ -1231,17 +1208,13 @@ export function indexPage() {
1231
1208
  function renderTicketPreview(previewId, ticket, description) {
1232
1209
  var el = document.getElementById(previewId);
1233
1210
  if (!el) return;
1234
- if (!ticket) {
1211
+ if (!ticket || !description) {
1235
1212
  el.style.display = 'none';
1236
1213
  el.innerHTML = '';
1237
1214
  return;
1238
1215
  }
1239
1216
  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
- }
1217
+ el.innerHTML = '<span class="ticket-id">' + escapeHtml(ticket) + '</span>' + escapeHtml(description);
1245
1218
  }
1246
1219
 
1247
1220
  async function fetchTicketSummary(ticket) {
@@ -1413,15 +1386,21 @@ export function indexPage() {
1413
1386
 
1414
1387
  const jToggle = document.getElementById('jira-enabled-toggle');
1415
1388
  const jToggleLabel = document.getElementById('jira-enabled-label');
1416
- const jConnectBtn = document.getElementById('jira-connect-btn');
1417
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()"]');
1418
1394
  const jiraEnabled = !data.jiraDisabled;
1419
1395
  if (jToggle) jToggle.checked = jiraEnabled;
1420
1396
  if (jToggleLabel) jToggleLabel.textContent = jiraEnabled ? 'Enabled' : 'Disabled';
1421
1397
  if (jToggleWrap) jToggleWrap.style.display = data.jiraConfigured ? '' : 'none';
1422
- if (jConnectBtn) jConnectBtn.disabled = data.jiraConfigured && !jiraEnabled;
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;
1423
1402
  setGoogleConnected(data.google, data.googleEmail);
1424
- setJiraConnected(data.jira, data.jiraOAuth, data.jiraSiteUrl);
1403
+ setJiraConnected(data.jira);
1425
1404
  applyMode(data);
1426
1405
  } catch {}
1427
1406
  }
package/dist/lib/db.js CHANGED
@@ -239,6 +239,10 @@ export function updateAtlassianAccessToken(access_token, expires_at) {
239
239
  const stmt = db.prepare('UPDATE atlassian_tokens SET access_token = ?, expires_at = ? WHERE id = 1');
240
240
  stmt.run(access_token, expires_at);
241
241
  }
242
+ export function clearAtlassianToken() {
243
+ const db = getDb();
244
+ db.prepare('DELETE FROM atlassian_tokens WHERE id = 1').run();
245
+ }
242
246
  export function upsertProjects(projects) {
243
247
  const db = getDb();
244
248
  const stmt = db.prepare('INSERT INTO projects (id, name, active) VALUES (?, ?, 1) ON CONFLICT(id) DO UPDATE SET name = ?');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clocktopus",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "Time-tracking automation for Clockify with idle monitoring, Jira integration, Google Calendar sync, CLI, web dashboard, and desktop app.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",