clocktopus 1.6.1 → 1.6.2

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.
@@ -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,78 @@ 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
+ let managed = false;
749
+ try {
750
+ const res = await fetch('/api/server/restart', { method: 'POST' });
751
+ const data = await res.json();
752
+ managed = !!data.managed;
753
+ } catch {
754
+ // Expected — server dies mid-response
755
+ }
756
+
757
+ if (!managed) {
758
+ overlayText.textContent = 'Server stopped. Start it manually: clocktopus serve';
759
+ setMsg('server-msg', 'Not managed by PM2 — restart manually.', false);
760
+ setTimeout(function() {
761
+ overlay.classList.remove('active');
762
+ btn.disabled = false;
763
+ btn.textContent = 'Restart Server';
764
+ }, 3500);
765
+ return;
766
+ }
767
+
768
+ // Poll /api/status until server responds
769
+ const start = Date.now();
770
+ const MAX_WAIT_MS = 30000;
771
+ async function poll() {
772
+ try {
773
+ const r = await fetch('/api/status', { cache: 'no-store' });
774
+ if (r.ok) {
775
+ overlayText.textContent = 'Server back online. Reloading...';
776
+ setTimeout(function() { window.location.reload(); }, 400);
777
+ return;
778
+ }
779
+ } catch {}
780
+ if (Date.now() - start > MAX_WAIT_MS) {
781
+ overlayText.textContent = 'Server did not come back. Check logs.';
782
+ setTimeout(function() {
783
+ overlay.classList.remove('active');
784
+ btn.disabled = false;
785
+ btn.textContent = 'Restart Server';
786
+ }, 3000);
787
+ return;
788
+ }
789
+ setTimeout(poll, 500);
712
790
  }
791
+ setTimeout(poll, 1000);
713
792
  }
714
793
 
715
794
  // --- Projects ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clocktopus",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {