clocktopus 1.10.2 → 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.
@@ -3,11 +3,11 @@ import { execSync } from 'child_process';
3
3
  import path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { createRequire } from 'module';
6
+ import { IS_DEV } from '../../lib/constants.js';
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = path.dirname(__filename);
8
9
  const SCRIPT_PATH = path.resolve(__dirname, '../../index.js');
9
- const isDev = SCRIPT_PATH.includes('/Projects/') || SCRIPT_PATH.includes('/src/');
10
- const PM2_NAME = isDev ? 'clocktopus-monitor-dev' : 'clocktopus-monitor';
10
+ const PM2_NAME = IS_DEV ? 'clocktopus-monitor-dev' : 'clocktopus-monitor';
11
11
  const pm2Bin = path.join(path.dirname(createRequire(import.meta.url).resolve('pm2')), 'bin', 'pm2');
12
12
  const bunBin = (() => {
13
13
  try {
@@ -13,9 +13,8 @@ export function indexPage() {
13
13
  <style>
14
14
  * { box-sizing: border-box; margin: 0; padding: 0; }
15
15
  html.browser { background: #0d1117; }
16
- html, body { border-radius: 12px; }
17
- html { overflow: hidden; background: transparent; }
18
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: transparent; color: #e1e4e8; padding: 2rem; }
16
+ html { border-radius: 12px; overflow: hidden; height: 100vh; background: transparent; }
17
+ body { border-radius: 12px; height: 100vh; overflow-y: auto; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: transparent; color: #e1e4e8; padding: 2rem; }
19
18
  h1 { font-size: 1.8rem; margin-bottom: 0; color: #fff; }
20
19
  h2 { font-size: 1.1rem; color: #fff; margin-bottom: 1rem; }
21
20
 
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { execSync } from 'child_process';
9
9
  import { closeStaleOpenSessions, completeLatestSession, getLatestSession, setSessionJiraWorklogId } from './lib/db.js';
10
10
  import { isClockifyEnabled } from './lib/credentials.js';
11
11
  import { ensureNativeAddons } from './lib/ensure-native-addons.js';
12
- import { DASHBOARD_PORT, DASHBOARD_URL } from './lib/constants.js';
12
+ import { DASHBOARD_PORT, DASHBOARD_URL, IS_DEV } from './lib/constants.js';
13
13
  const __filename = fileURLToPath(import.meta.url);
14
14
  const __dirname = path.dirname(__filename);
15
15
  const program = new Command();
@@ -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');
@@ -398,9 +423,8 @@ program
398
423
  const { startDashboard } = await import('./dashboard/server.js');
399
424
  startDashboard();
400
425
  });
401
- const isDev = __dirname.includes('/Projects/') || __dirname.includes('/src/');
402
- const MONITOR_PM2_NAME = isDev ? 'clocktopus-monitor-dev' : 'clocktopus-monitor';
403
- const DASH_PM2_NAME = isDev ? 'clocktopus-dash-dev' : 'clocktopus-dash';
426
+ const MONITOR_PM2_NAME = IS_DEV ? 'clocktopus-monitor-dev' : 'clocktopus-monitor';
427
+ const DASH_PM2_NAME = IS_DEV ? 'clocktopus-dash-dev' : 'clocktopus-dash';
404
428
  const pm2Bin = path.join(path.dirname(createRequire(import.meta.url).resolve('pm2')), 'bin', 'pm2');
405
429
  const bunBin = (() => {
406
430
  try {
@@ -1,3 +1,25 @@
1
+ import * as path from 'path';
2
+ /**
3
+ * True only when running from the clocktopus source repo (e.g. `bun run`
4
+ * during local development). Bun-linked global installs live under
5
+ * `node_modules/` and must NOT be flagged as dev — otherwise PM2 names and
6
+ * data paths pick up the dev variants. Set CLOCKTOPUS_DEV=1 to force on.
7
+ */
8
+ export const IS_DEV = (() => {
9
+ const override = process.env.CLOCKTOPUS_DEV;
10
+ if (override != null && override !== '') {
11
+ return override === '1' || override.toLowerCase() === 'true';
12
+ }
13
+ try {
14
+ const scriptDir = path.dirname(new URL(import.meta.url).pathname);
15
+ if (scriptDir.includes('/node_modules/'))
16
+ return false;
17
+ return scriptDir.includes('/Projects/') || scriptDir.includes('/src/');
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ })();
1
23
  /**
2
24
  * Dashboard HTTP port. Override via CLOCKTOPUS_PORT env var if 4001 is busy.
3
25
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clocktopus",
3
- "version": "1.10.2",
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",