clocktopus 1.3.0 → 1.5.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/README.md +9 -0
- package/assets/logo.png +0 -0
- package/dist/clockify.js +21 -2
- package/dist/dashboard/routes/calendar.js +2 -0
- package/dist/dashboard/routes/google.js +2 -0
- package/dist/dashboard/routes/timer.js +19 -1
- package/dist/dashboard/server.js +5 -3
- package/dist/dashboard/views.js +7 -2
- package/dist/index.js +3 -2
- package/dist/lib/atlassian.js +2 -0
- package/dist/lib/constants.js +15 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -152,6 +152,15 @@ Go to **System Settings > Notifications** and ensure **terminal-notifier** has n
|
|
|
152
152
|
|
|
153
153
|
Enable **Require password immediately** in System Settings > Lock Screen.
|
|
154
154
|
|
|
155
|
+
### Uninstall
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
bun remove -g clocktopus
|
|
159
|
+
|
|
160
|
+
# If the above doesn't work, delete the binary directly:
|
|
161
|
+
rm ~/.bun/bin/clocktopus
|
|
162
|
+
```
|
|
163
|
+
|
|
155
164
|
### Bun installs an old version
|
|
156
165
|
|
|
157
166
|
Bun caches registry data aggressively. Clear the cache and reinstall:
|
package/assets/logo.png
ADDED
|
Binary file
|
package/dist/clockify.js
CHANGED
|
@@ -1,20 +1,39 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
1
4
|
import { HttpClient } from './lib/http-client.js';
|
|
2
5
|
import { logSessionStart } from './lib/db.js';
|
|
3
6
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
7
|
import { NotificationCenter } from 'node-notifier';
|
|
5
8
|
import { getJiraTicket } from './lib/jira.js';
|
|
9
|
+
/**
|
|
10
|
+
* Resolve `assets/logo.png` from the package root regardless of install layout.
|
|
11
|
+
* Walks up from this file until the assets dir is found.
|
|
12
|
+
*/
|
|
13
|
+
function resolveLogoPath() {
|
|
14
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
for (let dir = here, prev = ''; dir !== prev; prev = dir, dir = path.dirname(dir)) {
|
|
16
|
+
const candidate = path.join(dir, 'assets', 'logo.png');
|
|
17
|
+
if (fs.existsSync(candidate))
|
|
18
|
+
return candidate;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const LOGO_PATH = resolveLogoPath();
|
|
6
23
|
export class Clockify {
|
|
7
24
|
constructor() {
|
|
8
25
|
this.httpClient = new HttpClient().getClient();
|
|
9
26
|
this.notifier = new NotificationCenter();
|
|
10
27
|
}
|
|
11
|
-
sendNotification(
|
|
28
|
+
sendNotification(subtitle, message, actions, callback) {
|
|
12
29
|
this.notifier.notify({
|
|
13
|
-
title,
|
|
30
|
+
title: 'Clocktopus',
|
|
31
|
+
subtitle,
|
|
14
32
|
message,
|
|
15
33
|
sound: true,
|
|
16
34
|
wait: true,
|
|
17
35
|
actions,
|
|
36
|
+
contentImage: LOGO_PATH,
|
|
18
37
|
}, callback ??
|
|
19
38
|
((err) => {
|
|
20
39
|
if (err)
|
|
@@ -3,6 +3,8 @@ import { google } from 'googleapis';
|
|
|
3
3
|
import { getAuthenticatedClient, getRefreshedToken } from '../../lib/google.js';
|
|
4
4
|
import { getLatestToken, storeToken, getEventProject, setEventProject, getActiveProjects } from '../../lib/db.js';
|
|
5
5
|
import { Clockify } from '../../clockify.js';
|
|
6
|
+
// Hardcoded — registered with Google OAuth; cannot vary with CLOCKTOPUS_PORT
|
|
7
|
+
// without re-registering the redirect URI in the Google Cloud console.
|
|
6
8
|
const DASHBOARD_REDIRECT_URI = 'http://localhost:4001/api/google/callback';
|
|
7
9
|
const calendarRoutes = new Hono();
|
|
8
10
|
calendarRoutes.get('/calendar/events', async (c) => {
|
|
@@ -3,6 +3,8 @@ import { google } from 'googleapis';
|
|
|
3
3
|
import { getAuthenticatedClient, getAuthUrl, exchangeGoogleCode } from '../../lib/google.js';
|
|
4
4
|
import { storeToken } from '../../lib/db.js';
|
|
5
5
|
import { saveCredential } from '../../lib/credentials.js';
|
|
6
|
+
// Hardcoded — registered with Google OAuth; cannot vary with CLOCKTOPUS_PORT
|
|
7
|
+
// without re-registering the redirect URI in the Google Cloud console.
|
|
6
8
|
const DASHBOARD_REDIRECT_URI = 'http://localhost:4001/api/google/callback';
|
|
7
9
|
const SCOPES = ['https://www.googleapis.com/auth/calendar.readonly', 'https://www.googleapis.com/auth/userinfo.email'];
|
|
8
10
|
const googleRoutes = new Hono();
|
|
@@ -15,8 +15,26 @@ timerRoutes.get('/timer/active', async (c) => {
|
|
|
15
15
|
if (!user)
|
|
16
16
|
return c.json({ active: false });
|
|
17
17
|
const timer = await clockify.getActiveTimer(user.defaultWorkspace, user.id);
|
|
18
|
-
if (!timer)
|
|
18
|
+
if (!timer) {
|
|
19
|
+
// Timer stopped externally (e.g. in Clockify app) — close any lingering open session
|
|
20
|
+
const openSession = getOpenSession();
|
|
21
|
+
if (openSession) {
|
|
22
|
+
const completedAt = new Date().toISOString();
|
|
23
|
+
completeLatestSession(completedAt, false);
|
|
24
|
+
if (openSession.jiraTicket) {
|
|
25
|
+
const timeSpentSeconds = Math.round((new Date(completedAt).getTime() - new Date(openSession.startedAt).getTime()) / 1000);
|
|
26
|
+
if (timeSpentSeconds >= 60) {
|
|
27
|
+
try {
|
|
28
|
+
await stopJiraTimer(openSession.jiraTicket, timeSpentSeconds);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
console.error('Error stopping Jira timer on external stop:', err);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
19
36
|
return c.json({ active: false });
|
|
37
|
+
}
|
|
20
38
|
// Sync externally-started timers (e.g. from Clockify app or Jira plugin) to DB
|
|
21
39
|
const timerStart = timer.timeInterval.start;
|
|
22
40
|
const jiraTicket = extractJiraTicket(timer.description ?? '');
|
package/dist/dashboard/server.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
+
import { cors } from 'hono/cors';
|
|
2
3
|
import { serve } from '@hono/node-server';
|
|
3
4
|
import { indexPage } from './views.js';
|
|
5
|
+
import { DASHBOARD_PORT } from '../lib/constants.js';
|
|
4
6
|
import statusRoutes from './routes/status.js';
|
|
5
7
|
import clockifyRoutes from './routes/clockify.js';
|
|
6
8
|
import jiraRoutes from './routes/jira.js';
|
|
@@ -10,6 +12,7 @@ import dataRoutes from './routes/data.js';
|
|
|
10
12
|
import monitorRoutes from './routes/monitor.js';
|
|
11
13
|
import calendarRoutes from './routes/calendar.js';
|
|
12
14
|
const app = new Hono();
|
|
15
|
+
app.use('*', cors());
|
|
13
16
|
app.get('/', (c) => c.html(indexPage()));
|
|
14
17
|
app.route('/api', statusRoutes);
|
|
15
18
|
app.route('/api', clockifyRoutes);
|
|
@@ -20,7 +23,6 @@ app.route('/api', dataRoutes);
|
|
|
20
23
|
app.route('/api', monitorRoutes);
|
|
21
24
|
app.route('/api', calendarRoutes);
|
|
22
25
|
export function startDashboard() {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
serve({ fetch: app.fetch, port });
|
|
26
|
+
console.log(`Clocktopus dashboard running at http://localhost:${DASHBOARD_PORT}`);
|
|
27
|
+
serve({ fetch: app.fetch, port: DASHBOARD_PORT });
|
|
26
28
|
}
|
package/dist/dashboard/views.js
CHANGED
|
@@ -7,7 +7,7 @@ export function indexPage() {
|
|
|
7
7
|
<title>Clocktopus Dashboard</title>
|
|
8
8
|
<style>
|
|
9
9
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
10
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background:
|
|
10
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: transparent; color: #e1e4e8; padding: 2rem; }
|
|
11
11
|
h1 { font-size: 1.8rem; margin-bottom: 0; color: #fff; }
|
|
12
12
|
h2 { font-size: 1.1rem; color: #fff; margin-bottom: 1rem; }
|
|
13
13
|
|
|
@@ -106,7 +106,7 @@ export function indexPage() {
|
|
|
106
106
|
.toggle input:checked + .slider::before { transform: translateX(16px); background: #fff; }
|
|
107
107
|
</style>
|
|
108
108
|
</head>
|
|
109
|
-
<body>
|
|
109
|
+
<body oncontextmenu="return false;">
|
|
110
110
|
<div class="header">
|
|
111
111
|
<h1>Clocktopus</h1>
|
|
112
112
|
<div class="nav">
|
|
@@ -343,6 +343,11 @@ export function indexPage() {
|
|
|
343
343
|
</div>
|
|
344
344
|
|
|
345
345
|
<script>
|
|
346
|
+
// Disable WebKit's Back/Forward/Reload context menu. Capture-phase and
|
|
347
|
+
// attached on window so it runs before any app handler and wins the race
|
|
348
|
+
// with the native WKWebView menu.
|
|
349
|
+
window.addEventListener('contextmenu', e => e.preventDefault(), { capture: true });
|
|
350
|
+
|
|
346
351
|
let elapsedInterval = null;
|
|
347
352
|
let currentPage = 1;
|
|
348
353
|
let totalPages = 1;
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { completeLatestSession, getLatestSession } from './lib/db.js';
|
|
|
11
11
|
import { stopJiraTimer } from './lib/jira.js';
|
|
12
12
|
import { startDashboard } from './dashboard/server.js';
|
|
13
13
|
import { ensureNativeAddons } from './lib/ensure-native-addons.js';
|
|
14
|
+
import { DASHBOARD_PORT, DASHBOARD_URL } from './lib/constants.js';
|
|
14
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
16
|
const __dirname = path.dirname(__filename);
|
|
16
17
|
const program = new Command();
|
|
@@ -281,7 +282,7 @@ program
|
|
|
281
282
|
});
|
|
282
283
|
program
|
|
283
284
|
.command('dash')
|
|
284
|
-
.description(
|
|
285
|
+
.description(`Start the Clocktopus web dashboard on localhost:${DASHBOARD_PORT}.`)
|
|
285
286
|
.action(() => {
|
|
286
287
|
startDashboard();
|
|
287
288
|
});
|
|
@@ -352,7 +353,7 @@ program
|
|
|
352
353
|
execSync(`${pm2Bin} start ${scriptPath} --name ${DASH_PM2_NAME} --interpreter ${bunPath} -- dash`, {
|
|
353
354
|
stdio: 'inherit',
|
|
354
355
|
});
|
|
355
|
-
console.log(chalk.green(
|
|
356
|
+
console.log(chalk.green(`Dashboard running at ${DASHBOARD_URL}`));
|
|
356
357
|
console.log(chalk.gray(' Stop: clocktopus serve:stop'));
|
|
357
358
|
console.log(chalk.gray(' Logs: clocktopus serve:logs'));
|
|
358
359
|
}
|
package/dist/lib/atlassian.js
CHANGED
|
@@ -6,6 +6,8 @@ const AUTH_PROXY_URL = 'https://clocktopus-auth.clocktopus.workers.dev';
|
|
|
6
6
|
// Fallback: direct Atlassian API (when user provides their own credentials)
|
|
7
7
|
const ATLASSIAN_TOKEN_URL = 'https://auth.atlassian.com/oauth/token';
|
|
8
8
|
const ATLASSIAN_RESOURCES_URL = 'https://api.atlassian.com/oauth/token/accessible-resources';
|
|
9
|
+
// Hardcoded — registered with Atlassian; cannot vary with CLOCKTOPUS_PORT
|
|
10
|
+
// without re-registering the redirect URI on the OAuth app.
|
|
9
11
|
const REDIRECT_URI = 'http://localhost:4001/api/jira/callback';
|
|
10
12
|
function hasLocalCredentials() {
|
|
11
13
|
return !!(resolveCredential('ATLASSIAN_CLIENT_ID') && resolveCredential('ATLASSIAN_CLIENT_SECRET'));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard HTTP port. Override via CLOCKTOPUS_PORT env var if 4001 is busy.
|
|
3
|
+
*
|
|
4
|
+
* NOTE: OAuth redirect URIs (Atlassian, Google) are registered with the
|
|
5
|
+
* provider at port 4001. Changing this port breaks OAuth unless the redirect
|
|
6
|
+
* URI is re-registered with the provider as well.
|
|
7
|
+
*/
|
|
8
|
+
export const DASHBOARD_PORT = (() => {
|
|
9
|
+
const raw = process.env.CLOCKTOPUS_PORT;
|
|
10
|
+
if (!raw)
|
|
11
|
+
return 4001;
|
|
12
|
+
const n = parseInt(raw, 10);
|
|
13
|
+
return Number.isFinite(n) && n > 0 && n < 65536 ? n : 4001;
|
|
14
|
+
})();
|
|
15
|
+
export const DASHBOARD_URL = `http://localhost:${DASHBOARD_PORT}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clocktopus",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"dist",
|
|
12
12
|
"data/.gitkeep",
|
|
13
13
|
"scripts/postinstall.cjs",
|
|
14
|
+
"assets/logo.png",
|
|
14
15
|
"!dist/desktop"
|
|
15
16
|
],
|
|
16
17
|
"scripts": {
|