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;
|
package/dist/dashboard/server.js
CHANGED
|
@@ -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 });
|
package/dist/dashboard/views.js
CHANGED
|
@@ -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 + '
|
|
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
|
-
|
|
698
|
-
|
|
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
|
-
|
|
708
|
-
desc.
|
|
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 ---
|