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 +12 -0
- package/dist/dashboard/routes/timer.js +43 -5
- package/dist/dashboard/views.js +25 -3
- package/dist/index.js +7 -3
- package/dist/lib/db.js +23 -1
- package/dist/lib/jira.js +7 -2
- package/package.json +2 -1
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;
|
package/dist/dashboard/views.js
CHANGED
|
@@ -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="
|
|
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="
|
|
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) + '\')">×</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="
|
|
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
|
-
|
|
84
|
-
|
|
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.
|
|
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": {
|