clocktopus 1.6.7 → 1.6.9

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/clockify.js CHANGED
@@ -196,6 +196,18 @@ export class Clockify {
196
196
  return [];
197
197
  }
198
198
  }
199
+ async deleteTimeEntry(workspaceId, entryId) {
200
+ try {
201
+ await this.httpClient.delete(`/workspaces/${workspaceId}/time-entries/${entryId}`);
202
+ return true;
203
+ }
204
+ catch (error) {
205
+ if (error instanceof Error) {
206
+ console.error('Error deleting time entry:', error.message);
207
+ }
208
+ return false;
209
+ }
210
+ }
199
211
  async logTime(workspaceId, projectId, start, end, description, billable = true) {
200
212
  if (!projectId) {
201
213
  return null;
@@ -1,8 +1,8 @@
1
1
  import { Hono } from 'hono';
2
2
  import { v4 as uuidv4 } from 'uuid';
3
3
  import { Clockify } from '../../clockify.js';
4
- import { completeLatestSession, getOpenSession, logCompletedSession, logSessionStart } from '../../lib/db.js';
5
- import { stopJiraTimer } from '../../lib/jira.js';
4
+ import { completeLatestSession, deleteSessionById, getOpenSession, getSessionById, logCompletedSession, logSessionStart, setSessionJiraWorklogId, } from '../../lib/db.js';
5
+ import { deleteJiraWorklog, stopJiraTimer } from '../../lib/jira.js';
6
6
  function extractJiraTicket(description) {
7
7
  const match = description.match(/\b([A-Z][A-Z0-9]+-\d+)\b/);
8
8
  return match?.[1];
@@ -25,7 +25,9 @@ timerRoutes.get('/timer/active', async (c) => {
25
25
  const timeSpentSeconds = Math.round((new Date(completedAt).getTime() - new Date(openSession.startedAt).getTime()) / 1000);
26
26
  if (timeSpentSeconds >= 60) {
27
27
  try {
28
- await stopJiraTimer(openSession.jiraTicket, timeSpentSeconds);
28
+ const worklog = await stopJiraTimer(openSession.jiraTicket, timeSpentSeconds);
29
+ if (worklog?.id)
30
+ setSessionJiraWorklogId(openSession.id, worklog.id);
29
31
  }
30
32
  catch (err) {
31
33
  console.error('Error stopping Jira timer on external stop:', err);
@@ -90,7 +92,9 @@ timerRoutes.post('/timer/stop', async (c) => {
90
92
  const timeSpentSeconds = Math.round((new Date(completedAt).getTime() - new Date(openSession.startedAt).getTime()) / 1000);
91
93
  if (timeSpentSeconds >= 60) {
92
94
  try {
93
- await stopJiraTimer(openSession.jiraTicket, timeSpentSeconds);
95
+ const worklog = await stopJiraTimer(openSession.jiraTicket, timeSpentSeconds);
96
+ if (worklog?.id)
97
+ setSessionJiraWorklogId(openSession.id, worklog.id);
94
98
  }
95
99
  catch (err) {
96
100
  console.error('Error stopping Jira timer:', err);
@@ -141,7 +145,9 @@ timerRoutes.post('/timer/log', async (c) => {
141
145
  const timeSpentSeconds = Math.round((endMs - startMs) / 1000);
142
146
  if (timeSpentSeconds >= 60) {
143
147
  try {
144
- await stopJiraTimer(cleanJira, timeSpentSeconds);
148
+ const worklog = await stopJiraTimer(cleanJira, timeSpentSeconds);
149
+ if (worklog?.id)
150
+ setSessionJiraWorklogId(entryId, worklog.id);
145
151
  }
146
152
  catch (err) {
147
153
  console.error('Error posting Jira worklog for manual entry:', err);
@@ -155,4 +161,36 @@ timerRoutes.post('/timer/log', async (c) => {
155
161
  return c.json({ ok: false, error: 'Failed to log time.' }, 500);
156
162
  }
157
163
  });
164
+ timerRoutes.delete('/timer/:id', async (c) => {
165
+ const id = c.req.param('id');
166
+ if (!id)
167
+ return c.json({ ok: false, error: 'Missing id.' }, 400);
168
+ const session = getSessionById(id);
169
+ if (!session)
170
+ return c.json({ ok: false, error: 'Session not found.' }, 404);
171
+ try {
172
+ const clockify = new Clockify();
173
+ const user = await clockify.getUser();
174
+ if (!user)
175
+ return c.json({ ok: false, error: 'Could not connect to Clockify.' }, 500);
176
+ const clockifyOk = await clockify.deleteTimeEntry(user.defaultWorkspace, id);
177
+ // Continue even if Clockify delete fails — the entry may already be gone remotely.
178
+ if (!clockifyOk)
179
+ console.warn(`Clockify delete returned failure for ${id}; removing local record anyway.`);
180
+ if (session.jiraTicket && session.jiraWorklogId) {
181
+ try {
182
+ await deleteJiraWorklog(session.jiraTicket, session.jiraWorklogId);
183
+ }
184
+ catch (err) {
185
+ console.error('Error deleting Jira worklog:', err);
186
+ }
187
+ }
188
+ deleteSessionById(id);
189
+ return c.json({ ok: true });
190
+ }
191
+ catch (err) {
192
+ console.error('Error deleting entry:', err);
193
+ return c.json({ ok: false, error: 'Failed to delete entry.' }, 500);
194
+ }
195
+ });
158
196
  export default timerRoutes;
@@ -78,6 +78,8 @@ export function indexPage() {
78
78
  .sessions-table td { padding: 0.5rem 0.75rem; border-bottom: 1px solid #21262d; }
79
79
  .sessions-table tr:hover { background: #161b22; }
80
80
  .sessions-table .in-progress { color: #3fb950; font-style: italic; }
81
+ .delete-btn { background: transparent; color: #8b949e; border: none; cursor: pointer; font-size: 1.1rem; line-height: 1; padding: 0 0.4rem; margin-top: 0; }
82
+ .delete-btn:hover { color: #f85149; background: transparent; }
81
83
  .empty-state { color: #8b949e; font-size: 0.9rem; padding: 2rem; text-align: center; }
82
84
 
83
85
  /* Inline form row */
@@ -230,10 +232,11 @@ export function indexPage() {
230
232
  <th>Started</th>
231
233
  <th>Duration</th>
232
234
  <th>Jira</th>
235
+ <th></th>
233
236
  </tr>
234
237
  </thead>
235
238
  <tbody id="sessions-body">
236
- <tr><td colspan="5" class="empty-state">Loading...</td></tr>
239
+ <tr><td colspan="6" class="empty-state">Loading...</td></tr>
237
240
  </tbody>
238
241
  </table>
239
242
  <div id="pagination" style="display:none; margin-top:1rem; align-items:center; justify-content:center; gap:0.75rem; flex-wrap:wrap;">
@@ -841,7 +844,7 @@ export function indexPage() {
841
844
  const pagination = document.getElementById('pagination');
842
845
 
843
846
  if (sessions.length === 0 && currentPage === 1) {
844
- tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No sessions yet. Start a timer to get going!</td></tr>';
847
+ tbody.innerHTML = '<tr><td colspan="6" class="empty-state">No sessions yet. Start a timer to get going!</td></tr>';
845
848
  pagination.style.display = 'none';
846
849
  return;
847
850
  }
@@ -856,12 +859,16 @@ export function indexPage() {
856
859
  duration = '<span class="in-progress">In progress</span>';
857
860
  }
858
861
  const jira = s.jiraTicket || '-';
862
+ const deleteBtn = s.completedAt
863
+ ? '<button class="delete-btn" title="Delete entry" onclick="deleteSession(\'' + escapeHtml(s.id) + '\')">&times;</button>'
864
+ : '';
859
865
  return '<tr>' +
860
866
  '<td>' + escapeHtml(s.description) + '</td>' +
861
867
  '<td>' + escapeHtml(s.projectName) + '</td>' +
862
868
  '<td>' + started + '</td>' +
863
869
  '<td>' + duration + '</td>' +
864
870
  '<td>' + escapeHtml(jira) + '</td>' +
871
+ '<td style="text-align:right;">' + deleteBtn + '</td>' +
865
872
  '</tr>';
866
873
  }).join('');
867
874
 
@@ -871,7 +878,7 @@ export function indexPage() {
871
878
  document.getElementById('next-btn').disabled = currentPage >= totalPages;
872
879
  document.getElementById('page-info').textContent = 'Page ' + currentPage + ' of ' + totalPages + ' (' + result.total + ' sessions)';
873
880
  } catch {
874
- document.getElementById('sessions-body').innerHTML = '<tr><td colspan="5" class="empty-state">Failed to load sessions.</td></tr>';
881
+ document.getElementById('sessions-body').innerHTML = '<tr><td colspan="6" class="empty-state">Failed to load sessions.</td></tr>';
875
882
  }
876
883
  }
877
884
 
@@ -882,6 +889,21 @@ export function indexPage() {
882
889
  loadSessions();
883
890
  }
884
891
 
892
+ async function deleteSession(id) {
893
+ if (!confirm('Delete this entry from Clockify and Jira?')) return;
894
+ try {
895
+ const res = await fetch('/api/timer/' + encodeURIComponent(id), { method: 'DELETE' });
896
+ const result = await res.json();
897
+ if (!result.ok) {
898
+ alert(result.error || 'Failed to delete entry.');
899
+ return;
900
+ }
901
+ loadSessions();
902
+ } catch {
903
+ alert('Failed to delete entry.');
904
+ }
905
+ }
906
+
885
907
  function escapeHtml(str) {
886
908
  const div = document.createElement('div');
887
909
  div.textContent = str;
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import * as path from 'path';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { createRequire } from 'module';
10
10
  import { execSync } from 'child_process';
11
- import { completeLatestSession, getLatestSession } from './lib/db.js';
11
+ import { completeLatestSession, getLatestSession, setSessionJiraWorklogId } from './lib/db.js';
12
12
  import { stopJiraTimer } from './lib/jira.js';
13
13
  import { startDashboard } from './dashboard/server.js';
14
14
  import { ensureNativeAddons } from './lib/ensure-native-addons.js';
@@ -106,7 +106,9 @@ program
106
106
  const timeSpentSeconds = Math.round((new Date(completedAt).getTime() - new Date(latestSession.startedAt).getTime()) / 1000);
107
107
  if (timeSpentSeconds >= 60) {
108
108
  try {
109
- await stopJiraTimer(latestSession.jiraTicket, timeSpentSeconds);
109
+ const worklog = await stopJiraTimer(latestSession.jiraTicket, timeSpentSeconds);
110
+ if (worklog?.id)
111
+ setSessionJiraWorklogId(latestSession.id, worklog.id);
110
112
  }
111
113
  catch (error) {
112
114
  console.error('Error stopping Jira timer:', error);
@@ -161,7 +163,9 @@ program
161
163
  const timeSpentSeconds = Math.round((new Date(completedAt).getTime() - new Date(latestSession.startedAt).getTime()) / 1000);
162
164
  if (timeSpentSeconds >= 60) {
163
165
  try {
164
- await stopJiraTimer(latestSession.jiraTicket, timeSpentSeconds);
166
+ const worklog = await stopJiraTimer(latestSession.jiraTicket, timeSpentSeconds);
167
+ if (worklog?.id)
168
+ setSessionJiraWorklogId(latestSession.id, worklog.id);
165
169
  }
166
170
  catch (err) {
167
171
  console.error('Error stopping Jira timer:', err);
package/dist/lib/db.js CHANGED
@@ -23,6 +23,7 @@ const SessionSchema = z.object({
23
23
  completedAt: z.string().nullable(),
24
24
  isAutoCompleted: z.number(),
25
25
  jiraTicket: z.string().nullable(),
26
+ jiraWorklogId: z.string().nullable().optional(),
26
27
  });
27
28
  let dbInstance = null;
28
29
  function getDb() {
@@ -37,9 +38,15 @@ function getDb() {
37
38
  startedAt TEXT NOT NULL,
38
39
  completedAt TEXT,
39
40
  isAutoCompleted INTEGER DEFAULT 0,
40
- jiraTicket TEXT
41
+ jiraTicket TEXT,
42
+ jiraWorklogId TEXT
41
43
  )
42
44
  `);
45
+ // Migration: add jiraWorklogId to pre-existing sessions tables
46
+ const sessionCols = dbInstance.prepare('PRAGMA table_info(sessions)').all();
47
+ if (!sessionCols.some((c) => c.name === 'jiraWorklogId')) {
48
+ dbInstance.exec('ALTER TABLE sessions ADD COLUMN jiraWorklogId TEXT');
49
+ }
43
50
  dbInstance.exec(`
44
51
  CREATE TABLE IF NOT EXISTS google_tokens (
45
52
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -124,6 +131,21 @@ export function completeLatestSession(completedAt, isAutoCompleted = false) {
124
131
  `);
125
132
  stmt.run(completedAt, isAutoCompleted ? 1 : 0);
126
133
  }
134
+ export function setSessionJiraWorklogId(sessionId, worklogId) {
135
+ const db = getDb();
136
+ db.prepare('UPDATE sessions SET jiraWorklogId = ? WHERE id = ?').run(worklogId, sessionId);
137
+ }
138
+ export function getSessionById(id) {
139
+ const db = getDb();
140
+ const row = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
141
+ if (!row)
142
+ return null;
143
+ return SessionSchema.parse(row);
144
+ }
145
+ export function deleteSessionById(id) {
146
+ const db = getDb();
147
+ db.prepare('DELETE FROM sessions WHERE id = ?').run(id);
148
+ }
127
149
  export function getRecentSessions(limit = 10, offset = 0) {
128
150
  const db = getDb();
129
151
  const stmt = db.prepare('SELECT * FROM sessions ORDER BY startedAt DESC LIMIT ? OFFSET ?');
package/dist/lib/jira.js CHANGED
@@ -80,8 +80,13 @@ export async function stopJiraTimer(ticketId, timeSpentSeconds) {
80
80
  ],
81
81
  },
82
82
  };
83
- console.log('Jira request body:', JSON.stringify(body, null, 2));
84
- return await jiraApiRequest(`/issue/${ticketId}/worklog`, 'POST', body);
83
+ const response = await jiraApiRequest(`/issue/${ticketId}/worklog`, 'POST', body);
84
+ const id = response?.id;
85
+ return id != null ? { id: String(id) } : null;
86
+ }
87
+ export async function deleteJiraWorklog(ticketId, worklogId) {
88
+ const result = await jiraApiRequest(`/issue/${ticketId}/worklog/${worklogId}`, 'DELETE');
89
+ return result !== null;
85
90
  }
86
91
  export async function getJiraTicket(ticketId) {
87
92
  return await jiraApiRequest(`/issue/${ticketId}`, 'GET');
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "clocktopus",
3
- "version": "1.6.7",
3
+ "version": "1.6.9",
4
+ "description": "Time-tracking automation for Clockify with idle monitoring, Jira integration, Google Calendar sync, CLI, web dashboard, and desktop app.",
4
5
  "type": "module",
5
6
  "main": "dist/index.js",
6
7
  "bin": {