clocktopus 1.6.1 → 1.6.3

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 CHANGED
@@ -195,6 +195,44 @@ cd ~/.bun/install/global/node_modules/macos-notification-state && npx node-gyp r
195
195
  apt install libxss-dev pkg-config build-essential
196
196
  ```
197
197
 
198
+ ### node-gyp error: `Cannot find module './entry-index'`
199
+
200
+ During install you may see:
201
+
202
+ ```
203
+ gyp ERR! stack Error: Cannot find module './entry-index'
204
+ gyp ERR! stack Require stack:
205
+ gyp ERR! stack - .../node_modules/cacache/lib/get.js
206
+ ...
207
+ error: install script from "desktop-idle" exited with 1
208
+ ```
209
+
210
+ Caused by a broken `node-gyp@12.2.0` / `cacache` bundle fetched via `bunx` on newer Node versions (e.g. Node 25).
211
+
212
+ Fixes, in order:
213
+
214
+ 1. **Use Node 20 LTS** (most reliable):
215
+
216
+ ```bash
217
+ nvm install 20 && nvm use 20
218
+ bun i -g clocktopus@latest --trust
219
+ ```
220
+
221
+ 2. **Clear the bunx node-gyp cache** and retry:
222
+
223
+ ```bash
224
+ rm -rf /var/folders/**/bunx-*-node-gyp@latest
225
+ bun i -g clocktopus@latest --trust
226
+ ```
227
+
228
+ 3. **Install via npm** (uses its own node-gyp):
229
+
230
+ ```bash
231
+ npm i -g clocktopus@latest
232
+ ```
233
+
234
+ `desktop-idle` is a native addon and must be compiled at install time — skipping postinstall leaves idle detection disabled.
235
+
198
236
  ## License
199
237
 
200
238
  MIT
@@ -0,0 +1,42 @@
1
+ import { Hono } from 'hono';
2
+ import { execSync, spawn } from 'child_process';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { createRequire } from 'module';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const SCRIPT_PATH = path.resolve(__dirname, '../../index.js');
9
+ const isDev = SCRIPT_PATH.includes('/Projects/') || SCRIPT_PATH.includes('/src/');
10
+ const DASH_PM2_NAME = isDev ? 'clocktopus-dash-dev' : 'clocktopus-dash';
11
+ const pm2Bin = path.join(path.dirname(createRequire(import.meta.url).resolve('pm2')), 'bin', 'pm2');
12
+ const serverRoutes = new Hono();
13
+ function isUnderPm2() {
14
+ if (process.env.pm_id)
15
+ return true;
16
+ try {
17
+ const output = execSync(`${pm2Bin} jlist`, { encoding: 'utf-8', timeout: 3000 });
18
+ const processes = JSON.parse(output);
19
+ return processes.some((p) => p.name === DASH_PM2_NAME && p.pid === process.pid);
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ serverRoutes.post('/server/restart', (c) => {
26
+ const underPm2 = isUnderPm2();
27
+ setTimeout(() => {
28
+ if (underPm2) {
29
+ try {
30
+ spawn(pm2Bin, ['restart', DASH_PM2_NAME], { detached: true, stdio: 'ignore' }).unref();
31
+ }
32
+ catch {
33
+ process.exit(0);
34
+ }
35
+ }
36
+ else {
37
+ process.exit(0);
38
+ }
39
+ }, 100);
40
+ return c.json({ ok: true, managed: underPm2 });
41
+ });
42
+ export default serverRoutes;
@@ -11,6 +11,7 @@ import timerRoutes from './routes/timer.js';
11
11
  import dataRoutes from './routes/data.js';
12
12
  import monitorRoutes from './routes/monitor.js';
13
13
  import calendarRoutes from './routes/calendar.js';
14
+ import serverRoutes from './routes/server.js';
14
15
  const app = new Hono();
15
16
  app.use('*', cors());
16
17
  app.get('/', (c) => c.html(indexPage()));
@@ -22,6 +23,7 @@ app.route('/api', timerRoutes);
22
23
  app.route('/api', dataRoutes);
23
24
  app.route('/api', monitorRoutes);
24
25
  app.route('/api', calendarRoutes);
26
+ app.route('/api', serverRoutes);
25
27
  export function startDashboard() {
26
28
  console.log(`Clocktopus dashboard running at http://localhost:${DASHBOARD_PORT}`);
27
29
  serve({ fetch: app.fetch, port: DASHBOARD_PORT });
@@ -110,9 +110,24 @@ export function indexPage() {
110
110
  .toggle .slider::before { content: ''; position: absolute; width: 16px; height: 16px; left: 2px; top: 2px; background: #8b949e; border-radius: 50%; transition: transform 0.2s, background 0.2s; }
111
111
  .toggle input:checked + .slider { background: #238636; }
112
112
  .toggle input:checked + .slider::before { transform: translateX(16px); background: #fff; }
113
+
114
+ /* Server restart overlay */
115
+ .overlay { position: fixed; inset: 0; background: rgba(13, 17, 23, 0.92); display: none; align-items: center; justify-content: center; z-index: 9999; }
116
+ .overlay.active { display: flex; }
117
+ .overlay-box { background: #1c1f26; border: 1px solid #30363d; border-radius: 12px; padding: 2rem 2.5rem; display: flex; flex-direction: column; align-items: center; gap: 1rem; }
118
+ .spinner { width: 32px; height: 32px; border: 3px solid #30363d; border-top-color: #d29922; border-radius: 50%; animation: spin 0.8s linear infinite; }
119
+ .overlay-text { color: #e1e4e8; font-size: 0.95rem; }
120
+ @keyframes spin { to { transform: rotate(360deg); } }
113
121
  </style>
114
122
  </head>
115
123
  <body oncontextmenu="return false;">
124
+ <div id="server-overlay" class="overlay">
125
+ <div class="overlay-box">
126
+ <div class="spinner"></div>
127
+ <div class="overlay-text" id="server-overlay-text">Restarting Server...</div>
128
+ </div>
129
+ </div>
130
+
116
131
  <div class="header">
117
132
  <h1>Clocktopus</h1>
118
133
  <div class="nav">
@@ -215,7 +230,6 @@ export function indexPage() {
215
230
  <button id="monitor-stop-btn" onclick="monitorAction('stop')" class="stop-btn" style="margin-top:0;" disabled>Stop</button>
216
231
  <button id="monitor-restart-btn" onclick="monitorAction('restart')" style="margin-top:0;background:#30363d;" disabled>Restart</button>
217
232
  </div>
218
- <div class="msg" id="monitor-msg"></div>
219
233
  </div>
220
234
 
221
235
  <!-- Session History -->
@@ -280,6 +294,17 @@ export function indexPage() {
280
294
  <div class="msg" id="google-msg"></div>
281
295
  </div>
282
296
 
297
+ <!-- Server -->
298
+ <div class="card">
299
+ <div class="card-header">
300
+ <div class="dot green"></div>
301
+ <h2>Server</h2>
302
+ </div>
303
+ <p style="font-size:0.85rem;color:#8b949e;margin-bottom:0.5rem;">Restart the Clocktopus dashboard server.</p>
304
+ <button id="server-restart-btn" onclick="restartServer()" style="background:#30363d;">Restart Server</button>
305
+ <div class="msg" id="server-msg"></div>
306
+ </div>
307
+
283
308
  <!-- Jira -->
284
309
  <div class="card">
285
310
  <div class="card-header">
@@ -678,9 +703,8 @@ export function indexPage() {
678
703
  stopBtn.disabled = true;
679
704
  restartBtn.disabled = true;
680
705
  dot.className = 'dot gray';
681
- desc.textContent = pending + ' server...';
706
+ desc.textContent = pending + ' monitor...';
682
707
  desc.style.color = '#d29922';
683
- setMsg('monitor-msg', pending + ' server...', true);
684
708
 
685
709
  const MIN_DISPLAY_MS = 1500;
686
710
  const started = Date.now();
@@ -693,23 +717,93 @@ export function indexPage() {
693
717
  const res = await fetch('/api/monitor/' + action, { method: 'POST' });
694
718
  const data = await res.json();
695
719
  await waitMin();
696
- if (data.ok) {
697
- setMsg('monitor-msg', 'Monitor ' + action + (action === 'stop' ? 'ped' : 'ed') + '.', true);
698
- } else {
699
- setMsg('monitor-msg', data.output || 'Failed.', false);
700
- desc.textContent = prevDesc;
701
- desc.style.color = prevColor;
720
+ if (!data.ok) {
721
+ desc.textContent = data.output || 'Failed.';
722
+ desc.style.color = '#f85149';
702
723
  dot.className = prevDot;
724
+ setTimeout(checkMonitorStatus, 2500);
725
+ return;
703
726
  }
704
727
  setTimeout(checkMonitorStatus, 500);
705
728
  } catch {
706
729
  await waitMin();
707
- setMsg('monitor-msg', 'Request failed.', false);
708
- desc.textContent = prevDesc;
709
- desc.style.color = prevColor;
730
+ desc.textContent = 'Request failed.';
731
+ desc.style.color = '#f85149';
710
732
  dot.className = prevDot;
711
- checkMonitorStatus();
733
+ setTimeout(checkMonitorStatus, 2500);
734
+ }
735
+ }
736
+
737
+ // --- Server restart ---
738
+ async function restartServer() {
739
+ const btn = document.getElementById('server-restart-btn');
740
+ const overlay = document.getElementById('server-overlay');
741
+ const overlayText = document.getElementById('server-overlay-text');
742
+ btn.disabled = true;
743
+ btn.textContent = 'Restarting...';
744
+ overlayText.textContent = 'Restarting Server...';
745
+ overlay.classList.add('active');
746
+ setMsg('server-msg', '', true);
747
+
748
+ const inTauri = !!(window.__TAURI_INTERNALS__ || window.__TAURI__);
749
+
750
+ if (inTauri) {
751
+ try {
752
+ await window.__TAURI__.core.invoke('restart_server');
753
+ } catch (err) {
754
+ overlayText.textContent = 'Restart failed.';
755
+ setTimeout(function() {
756
+ overlay.classList.remove('active');
757
+ btn.disabled = false;
758
+ btn.textContent = 'Restart Server';
759
+ }, 2500);
760
+ return;
761
+ }
762
+ } else {
763
+ let managed = false;
764
+ try {
765
+ const res = await fetch('/api/server/restart', { method: 'POST' });
766
+ const data = await res.json();
767
+ managed = !!data.managed;
768
+ } catch {
769
+ // Expected — server dies mid-response
770
+ }
771
+ if (!managed) {
772
+ overlayText.textContent = 'Server stopped. Start it manually: clocktopus serve';
773
+ setMsg('server-msg', 'Not managed by PM2 — restart manually.', false);
774
+ setTimeout(function() {
775
+ overlay.classList.remove('active');
776
+ btn.disabled = false;
777
+ btn.textContent = 'Restart Server';
778
+ }, 3500);
779
+ return;
780
+ }
781
+ }
782
+
783
+ // Poll /api/status until server responds
784
+ const start = Date.now();
785
+ const MAX_WAIT_MS = 30000;
786
+ async function poll() {
787
+ try {
788
+ const r = await fetch('/api/status', { cache: 'no-store' });
789
+ if (r.ok) {
790
+ overlayText.textContent = 'Server back online. Reloading...';
791
+ setTimeout(function() { window.location.reload(); }, 400);
792
+ return;
793
+ }
794
+ } catch {}
795
+ if (Date.now() - start > MAX_WAIT_MS) {
796
+ overlayText.textContent = 'Server did not come back. Check logs.';
797
+ setTimeout(function() {
798
+ overlay.classList.remove('active');
799
+ btn.disabled = false;
800
+ btn.textContent = 'Restart Server';
801
+ }, 3000);
802
+ return;
803
+ }
804
+ setTimeout(poll, 500);
712
805
  }
806
+ setTimeout(poll, 1000);
713
807
  }
714
808
 
715
809
  // --- Projects ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clocktopus",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {