clocktopus 1.10.3 → 1.10.4

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.
Files changed (2) hide show
  1. package/dist/index.js +54 -29
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -268,44 +268,70 @@ program
268
268
  }
269
269
  // Safer restart w/ cooldown; only resume a recent auto-completed session
270
270
  let lastResumeAt = 0;
271
+ let resuming = false;
271
272
  const RESUME_COOLDOWN_MS = 10000;
273
+ // Shared between lock-state and idle-time watchers so a resume triggered
274
+ // by one path disarms the other (otherwise both fire and we get two starts).
275
+ let isLocked = false;
276
+ let lastIdle = false;
272
277
  async function safeRestartTimerIfNeeded() {
273
278
  const now = Date.now();
274
- if (now - lastResumeAt < RESUME_COOLDOWN_MS)
275
- return;
276
- // Small delay lets services settle after wake/activity
277
- await sleep(800);
278
- const latestSession = getLatestSession();
279
- if (!latestSession)
280
- return;
281
- const twoHoursAgo = Date.now() - 2 * 60 * 60 * 1000;
282
- const completedMs = latestSession.completedAt ? new Date(latestSession.completedAt).getTime() : 0;
283
- if (!latestSession.isAutoCompleted || completedMs <= twoHoursAgo)
279
+ if (resuming || now - lastResumeAt < RESUME_COOLDOWN_MS)
284
280
  return;
285
- if (isClockifyEnabled()) {
286
- if (!latestSession.projectId)
281
+ resuming = true;
282
+ // Gate concurrent callers immediately; refine on success/failure below.
283
+ lastResumeAt = now;
284
+ try {
285
+ // Small delay lets services settle after wake/activity
286
+ await sleep(800);
287
+ const latestSession = getLatestSession();
288
+ if (!latestSession)
287
289
  return;
288
- const activeEntry = await (await clockify()).getActiveTimer(workspaceId, userId);
289
- if (activeEntry)
290
+ const twoHoursAgo = Date.now() - 2 * 60 * 60 * 1000;
291
+ const completedMs = latestSession.completedAt ? new Date(latestSession.completedAt).getTime() : 0;
292
+ if (!latestSession.isAutoCompleted || completedMs <= twoHoursAgo)
293
+ return;
294
+ if (isClockifyEnabled()) {
295
+ if (!latestSession.projectId)
296
+ return;
297
+ const activeEntry = await (await clockify()).getActiveTimer(workspaceId, userId);
298
+ if (activeEntry) {
299
+ isLocked = false;
300
+ lastIdle = false;
301
+ return;
302
+ }
303
+ await (await clockify()).startTimer(workspaceId, latestSession.projectId, latestSession.description, latestSession.jiraTicket ?? undefined);
304
+ console.log(chalk.green('Timer restarted for the last used project.'));
305
+ lastResumeAt = Date.now();
306
+ isLocked = false;
307
+ lastIdle = false;
290
308
  return;
291
- await (await clockify()).startTimer(workspaceId, latestSession.projectId, latestSession.description, latestSession.jiraTicket ?? undefined);
292
- console.log(chalk.green('Timer restarted for the last used project.'));
309
+ }
310
+ // Jira-only resume: new DB session with a fresh uuid, same ticket.
311
+ // Re-read latest to guard against a concurrent insert from another path.
312
+ if (!latestSession.jiraTicket)
313
+ return;
314
+ const fresh = getLatestSession();
315
+ if (fresh && !fresh.completedAt) {
316
+ isLocked = false;
317
+ lastIdle = false;
318
+ return;
319
+ }
320
+ const { v4: uuidv4 } = await import('uuid');
321
+ const { logSessionStart } = await import('./lib/db.js');
322
+ const sessionId = uuidv4();
323
+ const startedAt = new Date().toISOString();
324
+ logSessionStart(sessionId, latestSession.projectId ?? null, latestSession.description, startedAt, latestSession.jiraTicket);
325
+ console.log(chalk.green(`Resumed Jira timer for ${latestSession.jiraTicket}.`));
293
326
  lastResumeAt = Date.now();
294
- return;
327
+ isLocked = false;
328
+ lastIdle = false;
329
+ }
330
+ finally {
331
+ resuming = false;
295
332
  }
296
- // Jira-only resume: new DB session with a fresh uuid, same ticket
297
- if (!latestSession.jiraTicket)
298
- return;
299
- const { v4: uuidv4 } = await import('uuid');
300
- const { logSessionStart } = await import('./lib/db.js');
301
- const sessionId = uuidv4();
302
- const startedAt = new Date().toISOString();
303
- logSessionStart(sessionId, latestSession.projectId ?? null, latestSession.description, startedAt, latestSession.jiraTicket);
304
- console.log(chalk.green(`Resumed Jira timer for ${latestSession.jiraTicket}.`));
305
- lastResumeAt = Date.now();
306
333
  }
307
334
  console.log(chalk.blue('Monitoring display events (Unified Log) and idle time...'));
308
- let isLocked = false;
309
335
  let pollInterval = null;
310
336
  console.log(chalk.blue('Monitoring display/lock state (macos-notification-state) and idle time...'));
311
337
  try {
@@ -351,7 +377,6 @@ program
351
377
  console.error(err);
352
378
  }
353
379
  const IDLE_THRESHOLD_SECONDS = 300; // 5 minutes
354
- let lastIdle = false;
355
380
  const idleInterval = setInterval(async () => {
356
381
  try {
357
382
  const idleModule = await import('desktop-idle');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clocktopus",
3
- "version": "1.10.3",
3
+ "version": "1.10.4",
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",