clocktopus 1.5.1 → 1.6.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.
package/dist/clockify.js
CHANGED
|
@@ -104,7 +104,7 @@ export class Clockify {
|
|
|
104
104
|
return null;
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
-
async startTimer(workspaceId, projectId, description = 'Working on a task...', jiraTicket) {
|
|
107
|
+
async startTimer(workspaceId, projectId, description = 'Working on a task...', jiraTicket, billable = true) {
|
|
108
108
|
try {
|
|
109
109
|
const user = await this.getUser();
|
|
110
110
|
if (!user) {
|
|
@@ -123,6 +123,7 @@ export class Clockify {
|
|
|
123
123
|
projectId: projectId,
|
|
124
124
|
description: finalDescription,
|
|
125
125
|
start: startedAt,
|
|
126
|
+
billable,
|
|
126
127
|
});
|
|
127
128
|
// Log session to SQLite
|
|
128
129
|
logSessionStart(sessionId, projectId, finalDescription, startedAt, jiraTicket);
|
|
@@ -195,7 +196,7 @@ export class Clockify {
|
|
|
195
196
|
return [];
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
|
-
async logTime(workspaceId, projectId, start, end, description) {
|
|
199
|
+
async logTime(workspaceId, projectId, start, end, description, billable = true) {
|
|
199
200
|
if (!projectId) {
|
|
200
201
|
return null;
|
|
201
202
|
}
|
|
@@ -205,6 +206,7 @@ export class Clockify {
|
|
|
205
206
|
start: start,
|
|
206
207
|
end: end,
|
|
207
208
|
description: description,
|
|
209
|
+
billable,
|
|
208
210
|
});
|
|
209
211
|
return response.data;
|
|
210
212
|
}
|
|
@@ -31,13 +31,33 @@ calendarRoutes.get('/calendar/events', async (c) => {
|
|
|
31
31
|
const endOfDay = new Date(end);
|
|
32
32
|
endOfDay.setDate(endOfDay.getDate() + 1);
|
|
33
33
|
const timeMax = endOfDay.toISOString();
|
|
34
|
-
const
|
|
34
|
+
const listEvents = () => calendar.events.list({
|
|
35
35
|
calendarId: 'primary',
|
|
36
36
|
timeMin,
|
|
37
37
|
timeMax,
|
|
38
38
|
singleEvents: true,
|
|
39
39
|
orderBy: 'startTime',
|
|
40
40
|
});
|
|
41
|
+
let res;
|
|
42
|
+
try {
|
|
43
|
+
res = await listEvents();
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const status = err?.code ?? err?.status;
|
|
47
|
+
if (status === 401 && token.refresh_token) {
|
|
48
|
+
console.log('[calendar] Access token rejected (401). Refreshing and retrying...');
|
|
49
|
+
token = await getRefreshedToken(token);
|
|
50
|
+
storeToken(token);
|
|
51
|
+
oAuth2Client.setCredentials(token);
|
|
52
|
+
res = await listEvents();
|
|
53
|
+
}
|
|
54
|
+
else if (status === 401) {
|
|
55
|
+
return c.json({ ok: false, error: 'Google auth expired. Please reconnect your Google account in Settings.' }, 401);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
41
61
|
// Fetch existing Clockify entries for the same range to detect duplicates
|
|
42
62
|
const clockify = new Clockify();
|
|
43
63
|
const user = await clockify.getUser();
|
|
@@ -86,7 +106,7 @@ calendarRoutes.post('/calendar/log', async (c) => {
|
|
|
86
106
|
continue;
|
|
87
107
|
}
|
|
88
108
|
try {
|
|
89
|
-
await clockify.logTime(user.defaultWorkspace, entry.projectId, entry.start, entry.end, entry.summary);
|
|
109
|
+
await clockify.logTime(user.defaultWorkspace, entry.projectId, entry.start, entry.end, entry.summary, entry.billable ?? true);
|
|
90
110
|
setEventProject(entry.summary, entry.projectId);
|
|
91
111
|
logged.push(entry.summary);
|
|
92
112
|
}
|
|
@@ -56,7 +56,7 @@ timerRoutes.get('/timer/active', async (c) => {
|
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
58
|
timerRoutes.post('/timer/start', async (c) => {
|
|
59
|
-
const { projectId, description, jiraTicket } = await c.req.json();
|
|
59
|
+
const { projectId, description, jiraTicket, billable } = await c.req.json();
|
|
60
60
|
if (!projectId || !description) {
|
|
61
61
|
return c.json({ ok: false, error: 'Project and description are required.' }, 400);
|
|
62
62
|
}
|
|
@@ -65,7 +65,7 @@ timerRoutes.post('/timer/start', async (c) => {
|
|
|
65
65
|
const user = await clockify.getUser();
|
|
66
66
|
if (!user)
|
|
67
67
|
return c.json({ ok: false, error: 'Could not connect to Clockify.' }, 500);
|
|
68
|
-
const result = await clockify.startTimer(user.defaultWorkspace, projectId, description, jiraTicket);
|
|
68
|
+
const result = await clockify.startTimer(user.defaultWorkspace, projectId, description, jiraTicket, billable ?? true);
|
|
69
69
|
if (!result)
|
|
70
70
|
return c.json({ ok: false, error: 'Failed to start timer.' }, 500);
|
|
71
71
|
return c.json({ ok: true });
|
|
@@ -104,7 +104,7 @@ timerRoutes.post('/timer/stop', async (c) => {
|
|
|
104
104
|
}
|
|
105
105
|
});
|
|
106
106
|
timerRoutes.post('/timer/log', async (c) => {
|
|
107
|
-
const { projectId, description, start, end, jiraTicket } = await c.req.json();
|
|
107
|
+
const { projectId, description, start, end, jiraTicket, billable } = await c.req.json();
|
|
108
108
|
if (!projectId) {
|
|
109
109
|
return c.json({ ok: false, error: 'Project is required.' }, 400);
|
|
110
110
|
}
|
|
@@ -132,7 +132,7 @@ timerRoutes.post('/timer/log', async (c) => {
|
|
|
132
132
|
const startIso = new Date(startMs).toISOString();
|
|
133
133
|
const endIso = new Date(endMs).toISOString();
|
|
134
134
|
const finalDescription = cleanDescription || cleanJira;
|
|
135
|
-
const entry = await clockify.logTime(user.defaultWorkspace, projectId, startIso, endIso, finalDescription);
|
|
135
|
+
const entry = await clockify.logTime(user.defaultWorkspace, projectId, startIso, endIso, finalDescription, billable ?? true);
|
|
136
136
|
if (!entry)
|
|
137
137
|
return c.json({ ok: false, error: 'Failed to log time in Clockify.' }, 500);
|
|
138
138
|
const entryId = entry.id ?? uuidv4();
|
package/dist/dashboard/views.js
CHANGED
|
@@ -159,6 +159,10 @@ export function indexPage() {
|
|
|
159
159
|
<input type="text" id="timer-jira" placeholder="e.g. PROJ-123" />
|
|
160
160
|
</div>
|
|
161
161
|
</div>
|
|
162
|
+
<label style="display:flex; align-items:center; gap:0.5rem; font-weight:normal; cursor:pointer;">
|
|
163
|
+
<input type="checkbox" id="timer-billable" checked style="width:auto; margin:0;" />
|
|
164
|
+
Billable
|
|
165
|
+
</label>
|
|
162
166
|
<button id="start-btn" onclick="startTimer()">Start Timer</button>
|
|
163
167
|
</div>
|
|
164
168
|
<div id="last-tasks" style="display:none; margin-top:0.75rem;"></div>
|
|
@@ -190,6 +194,10 @@ export function indexPage() {
|
|
|
190
194
|
<input type="text" id="manual-jira" placeholder="e.g. PROJ-123" />
|
|
191
195
|
</div>
|
|
192
196
|
</div>
|
|
197
|
+
<label style="display:flex; align-items:center; gap:0.5rem; font-weight:normal; cursor:pointer;">
|
|
198
|
+
<input type="checkbox" id="manual-billable" checked style="width:auto; margin:0;" />
|
|
199
|
+
Billable
|
|
200
|
+
</label>
|
|
193
201
|
<button id="manual-log-btn" onclick="logManualTime()">Log Time</button>
|
|
194
202
|
<div class="msg" id="manual-msg"></div>
|
|
195
203
|
</div>
|
|
@@ -442,6 +450,7 @@ export function indexPage() {
|
|
|
442
450
|
const projectId = document.getElementById('project-select').value;
|
|
443
451
|
const description = document.getElementById('timer-description').value.trim();
|
|
444
452
|
const jiraTicket = document.getElementById('timer-jira').value.trim();
|
|
453
|
+
const billable = document.getElementById('timer-billable').checked;
|
|
445
454
|
|
|
446
455
|
if (!projectId) return setMsg('timer-msg', 'Please select a project.', false);
|
|
447
456
|
if (!description && !jiraTicket) return setMsg('timer-msg', 'Please enter a description or Jira ticket.', false);
|
|
@@ -454,7 +463,7 @@ export function indexPage() {
|
|
|
454
463
|
const res = await fetch('/api/timer/start', {
|
|
455
464
|
method: 'POST',
|
|
456
465
|
headers: { 'Content-Type': 'application/json' },
|
|
457
|
-
body: JSON.stringify({ projectId, description: description || 'Working on a task...', jiraTicket: jiraTicket || undefined }),
|
|
466
|
+
body: JSON.stringify({ projectId, description: description || 'Working on a task...', jiraTicket: jiraTicket || undefined, billable }),
|
|
458
467
|
});
|
|
459
468
|
const data = await res.json();
|
|
460
469
|
if (data.ok) {
|
|
@@ -514,6 +523,7 @@ export function indexPage() {
|
|
|
514
523
|
const endVal = document.getElementById('manual-end').value;
|
|
515
524
|
const description = document.getElementById('manual-description').value.trim();
|
|
516
525
|
const jiraTicket = document.getElementById('manual-jira').value.trim();
|
|
526
|
+
const billable = document.getElementById('manual-billable').checked;
|
|
517
527
|
|
|
518
528
|
if (!projectId) return setMsg('manual-msg', 'Please select a project.', false);
|
|
519
529
|
if (!startVal || !endVal) return setMsg('manual-msg', 'Please set start and end.', false);
|
|
@@ -538,6 +548,7 @@ export function indexPage() {
|
|
|
538
548
|
start: new Date(startMs).toISOString(),
|
|
539
549
|
end: new Date(endMs).toISOString(),
|
|
540
550
|
jiraTicket: jiraTicket || undefined,
|
|
551
|
+
billable: billable,
|
|
541
552
|
}),
|
|
542
553
|
});
|
|
543
554
|
const data = await res.json();
|
|
@@ -599,6 +610,7 @@ export function indexPage() {
|
|
|
599
610
|
projectId: task.projectId,
|
|
600
611
|
description: task.description || 'Working on a task...',
|
|
601
612
|
jiraTicket: task.jiraTicket || undefined,
|
|
613
|
+
billable: document.getElementById('timer-billable').checked,
|
|
602
614
|
}),
|
|
603
615
|
});
|
|
604
616
|
var data = await res.json();
|
|
@@ -649,7 +661,27 @@ export function indexPage() {
|
|
|
649
661
|
}
|
|
650
662
|
|
|
651
663
|
async function monitorAction(action) {
|
|
652
|
-
|
|
664
|
+
const startBtn = document.getElementById('monitor-start-btn');
|
|
665
|
+
const stopBtn = document.getElementById('monitor-stop-btn');
|
|
666
|
+
const restartBtn = document.getElementById('monitor-restart-btn');
|
|
667
|
+
const desc = document.getElementById('monitor-desc');
|
|
668
|
+
const dot = document.getElementById('monitor-dot');
|
|
669
|
+
|
|
670
|
+
const labels = { start: 'Starting', stop: 'Stopping', restart: 'Restarting' };
|
|
671
|
+
const pending = labels[action] || 'Working';
|
|
672
|
+
|
|
673
|
+
const prevDesc = desc.textContent;
|
|
674
|
+
const prevColor = desc.style.color;
|
|
675
|
+
const prevDot = dot.className;
|
|
676
|
+
|
|
677
|
+
startBtn.disabled = true;
|
|
678
|
+
stopBtn.disabled = true;
|
|
679
|
+
restartBtn.disabled = true;
|
|
680
|
+
dot.className = 'dot gray';
|
|
681
|
+
desc.textContent = pending + ' server...';
|
|
682
|
+
desc.style.color = '#d29922';
|
|
683
|
+
setMsg('monitor-msg', pending + ' server...', true);
|
|
684
|
+
|
|
653
685
|
try {
|
|
654
686
|
const res = await fetch('/api/monitor/' + action, { method: 'POST' });
|
|
655
687
|
const data = await res.json();
|
|
@@ -657,10 +689,17 @@ export function indexPage() {
|
|
|
657
689
|
setMsg('monitor-msg', 'Monitor ' + action + (action === 'stop' ? 'ped' : 'ed') + '.', true);
|
|
658
690
|
} else {
|
|
659
691
|
setMsg('monitor-msg', data.output || 'Failed.', false);
|
|
692
|
+
desc.textContent = prevDesc;
|
|
693
|
+
desc.style.color = prevColor;
|
|
694
|
+
dot.className = prevDot;
|
|
660
695
|
}
|
|
661
696
|
setTimeout(checkMonitorStatus, 1000);
|
|
662
697
|
} catch {
|
|
663
698
|
setMsg('monitor-msg', 'Request failed.', false);
|
|
699
|
+
desc.textContent = prevDesc;
|
|
700
|
+
desc.style.color = prevColor;
|
|
701
|
+
dot.className = prevDot;
|
|
702
|
+
checkMonitorStatus();
|
|
664
703
|
}
|
|
665
704
|
}
|
|
666
705
|
|
|
@@ -1036,9 +1075,10 @@ export function indexPage() {
|
|
|
1036
1075
|
document.getElementById('cal-table-wrap').style.display = 'block';
|
|
1037
1076
|
} catch {
|
|
1038
1077
|
setMsg('cal-msg', 'Request failed.', false);
|
|
1078
|
+
} finally {
|
|
1079
|
+
fetchBtn.disabled = false;
|
|
1080
|
+
fetchBtn.textContent = 'Fetch Events';
|
|
1039
1081
|
}
|
|
1040
|
-
fetchBtn.disabled = false;
|
|
1041
|
-
fetchBtn.textContent = 'Fetch Events';
|
|
1042
1082
|
}
|
|
1043
1083
|
|
|
1044
1084
|
function renderCalendarTable() {
|
|
@@ -1060,10 +1100,15 @@ export function indexPage() {
|
|
|
1060
1100
|
return '<option value="' + p.id + '"' + selected + '>' + escapeHtml(p.name) + '</option>';
|
|
1061
1101
|
}).join('');
|
|
1062
1102
|
|
|
1103
|
+
const billableChecked = ev.billable === false ? '' : ' checked';
|
|
1063
1104
|
return '<div class="cal-event-card' + (logged ? ' logged' : '') + '">' +
|
|
1064
1105
|
'<div class="cal-card-name">' + escapeHtml(ev.summary || 'Untitled') + badge + '</div>' +
|
|
1065
1106
|
'<div class="cal-card-time">' + startDay + ' ' + startTime + ' — ' + endTime + ' (' + formatDuration(durationMs) + ')</div>' +
|
|
1066
1107
|
'<select class="cal-project-sel" data-index="' + i + '"' + selDisabled + '>' + projectOptions + '</select>' +
|
|
1108
|
+
'<label style="display:flex; align-items:center; gap:0.4rem; font-size:0.75rem; color:#8b949e; margin-top:0.5rem; cursor:pointer;">' +
|
|
1109
|
+
'<input type="checkbox" class="cal-billable-sel" data-index="' + i + '"' + billableChecked + (logged ? ' disabled' : '') + ' style="width:auto; margin:0;" />' +
|
|
1110
|
+
'Billable' +
|
|
1111
|
+
'</label>' +
|
|
1067
1112
|
'</div>';
|
|
1068
1113
|
}).join('');
|
|
1069
1114
|
|
|
@@ -1073,6 +1118,13 @@ export function indexPage() {
|
|
|
1073
1118
|
calEvents[idx].savedProjectId = sel.value || null;
|
|
1074
1119
|
});
|
|
1075
1120
|
});
|
|
1121
|
+
|
|
1122
|
+
document.querySelectorAll('.cal-billable-sel').forEach(function(cb) {
|
|
1123
|
+
cb.addEventListener('change', function() {
|
|
1124
|
+
const idx = parseInt(cb.dataset.index);
|
|
1125
|
+
calEvents[idx].billable = cb.checked;
|
|
1126
|
+
});
|
|
1127
|
+
});
|
|
1076
1128
|
}
|
|
1077
1129
|
|
|
1078
1130
|
async function logCalendarEvents() {
|
|
@@ -1082,7 +1134,9 @@ export function indexPage() {
|
|
|
1082
1134
|
if (sel.value === 'skip' || !sel.value) return;
|
|
1083
1135
|
const idx = parseInt(sel.dataset.index);
|
|
1084
1136
|
const ev = calEvents[idx];
|
|
1085
|
-
|
|
1137
|
+
const cb = document.querySelector('.cal-billable-sel[data-index="' + idx + '"]');
|
|
1138
|
+
const billable = cb ? cb.checked : true;
|
|
1139
|
+
entries.push({ projectId: sel.value, summary: ev.summary, start: ev.start, end: ev.end, billable: billable });
|
|
1086
1140
|
});
|
|
1087
1141
|
|
|
1088
1142
|
if (entries.length === 0) {
|
package/dist/index.js
CHANGED
|
@@ -56,6 +56,7 @@ program
|
|
|
56
56
|
.description('Start a new time entry. Select a project interactively.')
|
|
57
57
|
.argument('[message]', 'Description for the time entry')
|
|
58
58
|
.option('-j, --jira <ticket>', 'Jira ticket number')
|
|
59
|
+
.option('--no-billable', 'Mark the time entry as non-billable')
|
|
59
60
|
.action(async (message, options) => {
|
|
60
61
|
const { workspaceId } = await getWorkspaceAndUser();
|
|
61
62
|
let projects = await clockify.getProjects(workspaceId);
|
|
@@ -84,7 +85,7 @@ program
|
|
|
84
85
|
choices: projects.map((p) => ({ name: p.name, value: p.id })),
|
|
85
86
|
},
|
|
86
87
|
]);
|
|
87
|
-
const entry = await clockify.startTimer(workspaceId, selectedProjectId, message, options.jira);
|
|
88
|
+
const entry = await clockify.startTimer(workspaceId, selectedProjectId, message, options.jira, options.billable);
|
|
88
89
|
if (entry) {
|
|
89
90
|
const projectName = projects.find((p) => p.id === selectedProjectId)?.name;
|
|
90
91
|
console.log(chalk.green(`Timer started for project: ${chalk.bold(projectName)}`));
|
|
@@ -35,9 +35,10 @@ program
|
|
|
35
35
|
.option('-e, --end-date <endDate>', 'End date for fetching calendar events')
|
|
36
36
|
.option('-t, --today', 'Log time for today')
|
|
37
37
|
.option('-p, --project-id <projectId>', 'Clockify project ID')
|
|
38
|
+
.option('--no-billable', 'Mark logged entries as non-billable')
|
|
38
39
|
.parse(process.argv);
|
|
39
40
|
const opts = program.opts();
|
|
40
|
-
const { projectId, today } = opts;
|
|
41
|
+
const { projectId, today, billable } = opts;
|
|
41
42
|
let { startDate, endDate } = opts;
|
|
42
43
|
if (today) {
|
|
43
44
|
const todayDate = new Date();
|
|
@@ -124,7 +125,7 @@ async function main() {
|
|
|
124
125
|
}
|
|
125
126
|
if (eventProjectId) {
|
|
126
127
|
console.log(`Logging "${event.summary}" to Clockify...`);
|
|
127
|
-
await clockify.logTime(user.activeWorkspace, eventProjectId, event.start.dateTime, event.end.dateTime, event.summary);
|
|
128
|
+
await clockify.logTime(user.activeWorkspace, eventProjectId, event.start.dateTime, event.end.dateTime, event.summary, billable);
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
}
|