clocktopus 1.2.1 → 1.4.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 +20 -3
- package/dist/dashboard/routes/timer.js +74 -3
- package/dist/dashboard/server.js +2 -0
- package/dist/dashboard/views.js +156 -28
- package/dist/lib/db.js +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,9 +21,11 @@ curl -fsSL https://bun.sh/install | bash
|
|
|
21
21
|
### Install
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
bun
|
|
24
|
+
bun install -g clocktopus --trust
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
> **Note:** `--trust` allows the postinstall script to build native addons (required for idle monitor on macOS). Without it, addon compilation is skipped and the monitor may fail to load.
|
|
28
|
+
|
|
27
29
|
### Run
|
|
28
30
|
|
|
29
31
|
```bash
|
|
@@ -150,6 +152,15 @@ Go to **System Settings > Notifications** and ensure **terminal-notifier** has n
|
|
|
150
152
|
|
|
151
153
|
Enable **Require password immediately** in System Settings > Lock Screen.
|
|
152
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
|
+
|
|
153
164
|
### Bun installs an old version
|
|
154
165
|
|
|
155
166
|
Bun caches registry data aggressively. Clear the cache and reinstall:
|
|
@@ -160,10 +171,16 @@ bun pm cache rm && bun i -g clocktopus@latest
|
|
|
160
171
|
|
|
161
172
|
### Native addons not built (untrusted postinstall)
|
|
162
173
|
|
|
163
|
-
|
|
174
|
+
Bun skips postinstall scripts for untrusted packages. Install with `--trust` to fix this:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
bun install -g clocktopus --trust
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Or if already installed, rebuild manually:
|
|
164
181
|
|
|
165
182
|
```bash
|
|
166
|
-
bun
|
|
183
|
+
cd ~/.bun/install/global/node_modules/macos-notification-state && npx node-gyp rebuild
|
|
167
184
|
```
|
|
168
185
|
|
|
169
186
|
### Linux
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { v4 as uuidv4 } from 'uuid';
|
|
3
3
|
import { Clockify } from '../../clockify.js';
|
|
4
|
-
import { completeLatestSession, getOpenSession, logSessionStart } from '../../lib/db.js';
|
|
4
|
+
import { completeLatestSession, getOpenSession, logCompletedSession, logSessionStart } from '../../lib/db.js';
|
|
5
5
|
import { stopJiraTimer } from '../../lib/jira.js';
|
|
6
6
|
function extractJiraTicket(description) {
|
|
7
7
|
const match = description.match(/\b([A-Z][A-Z0-9]+-\d+)\b/);
|
|
@@ -15,14 +15,32 @@ 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;
|
|
40
|
+
const jiraTicket = extractJiraTicket(timer.description ?? '');
|
|
22
41
|
const openSession = getOpenSession();
|
|
23
42
|
const alreadyTracked = openSession && openSession.startedAt.slice(0, 19) === timerStart.slice(0, 19);
|
|
24
43
|
if (!alreadyTracked) {
|
|
25
|
-
const jiraTicket = extractJiraTicket(timer.description ?? '');
|
|
26
44
|
logSessionStart(timer.id ?? uuidv4(), timer.projectId, timer.description ?? '', timerStart, jiraTicket);
|
|
27
45
|
}
|
|
28
46
|
return c.json({
|
|
@@ -30,6 +48,7 @@ timerRoutes.get('/timer/active', async (c) => {
|
|
|
30
48
|
description: timer.description,
|
|
31
49
|
projectId: timer.projectId,
|
|
32
50
|
start: timerStart,
|
|
51
|
+
...(jiraTicket ? { jiraTicket } : {}),
|
|
33
52
|
});
|
|
34
53
|
}
|
|
35
54
|
catch {
|
|
@@ -84,4 +103,56 @@ timerRoutes.post('/timer/stop', async (c) => {
|
|
|
84
103
|
return c.json({ ok: false, error: 'Failed to stop timer.' }, 500);
|
|
85
104
|
}
|
|
86
105
|
});
|
|
106
|
+
timerRoutes.post('/timer/log', async (c) => {
|
|
107
|
+
const { projectId, description, start, end, jiraTicket } = await c.req.json();
|
|
108
|
+
if (!projectId) {
|
|
109
|
+
return c.json({ ok: false, error: 'Project is required.' }, 400);
|
|
110
|
+
}
|
|
111
|
+
if (!start || !end) {
|
|
112
|
+
return c.json({ ok: false, error: 'Start and end are required.' }, 400);
|
|
113
|
+
}
|
|
114
|
+
const startMs = new Date(start).getTime();
|
|
115
|
+
const endMs = new Date(end).getTime();
|
|
116
|
+
if (Number.isNaN(startMs) || Number.isNaN(endMs)) {
|
|
117
|
+
return c.json({ ok: false, error: 'Invalid start or end date.' }, 400);
|
|
118
|
+
}
|
|
119
|
+
if (endMs <= startMs) {
|
|
120
|
+
return c.json({ ok: false, error: 'End must be after start.' }, 400);
|
|
121
|
+
}
|
|
122
|
+
const cleanDescription = (description ?? '').trim();
|
|
123
|
+
const cleanJira = jiraTicket?.trim() || undefined;
|
|
124
|
+
if (!cleanDescription && !cleanJira) {
|
|
125
|
+
return c.json({ ok: false, error: 'Description or Jira ticket is required.' }, 400);
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const clockify = new Clockify();
|
|
129
|
+
const user = await clockify.getUser();
|
|
130
|
+
if (!user)
|
|
131
|
+
return c.json({ ok: false, error: 'Could not connect to Clockify.' }, 500);
|
|
132
|
+
const startIso = new Date(startMs).toISOString();
|
|
133
|
+
const endIso = new Date(endMs).toISOString();
|
|
134
|
+
const finalDescription = cleanDescription || cleanJira;
|
|
135
|
+
const entry = await clockify.logTime(user.defaultWorkspace, projectId, startIso, endIso, finalDescription);
|
|
136
|
+
if (!entry)
|
|
137
|
+
return c.json({ ok: false, error: 'Failed to log time in Clockify.' }, 500);
|
|
138
|
+
const entryId = entry.id ?? uuidv4();
|
|
139
|
+
logCompletedSession(entryId, projectId, finalDescription, startIso, endIso, cleanJira);
|
|
140
|
+
if (cleanJira) {
|
|
141
|
+
const timeSpentSeconds = Math.round((endMs - startMs) / 1000);
|
|
142
|
+
if (timeSpentSeconds >= 60) {
|
|
143
|
+
try {
|
|
144
|
+
await stopJiraTimer(cleanJira, timeSpentSeconds);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
console.error('Error posting Jira worklog for manual entry:', err);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return c.json({ ok: true });
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
console.error('Error logging manual time:', err);
|
|
155
|
+
return c.json({ ok: false, error: 'Failed to log time.' }, 500);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
87
158
|
export default timerRoutes;
|
package/dist/dashboard/server.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
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';
|
|
4
5
|
import statusRoutes from './routes/status.js';
|
|
@@ -10,6 +11,7 @@ import dataRoutes from './routes/data.js';
|
|
|
10
11
|
import monitorRoutes from './routes/monitor.js';
|
|
11
12
|
import calendarRoutes from './routes/calendar.js';
|
|
12
13
|
const app = new Hono();
|
|
14
|
+
app.use('*', cors());
|
|
13
15
|
app.get('/', (c) => c.html(indexPage()));
|
|
14
16
|
app.route('/api', statusRoutes);
|
|
15
17
|
app.route('/api', clockifyRoutes);
|
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
|
|
|
@@ -22,11 +22,18 @@ export function indexPage() {
|
|
|
22
22
|
|
|
23
23
|
/* Cards */
|
|
24
24
|
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 1.5rem; }
|
|
25
|
-
.
|
|
25
|
+
.home-cards { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
|
|
26
|
+
.card { background: #1c1f26; border: 1px solid #2d3139; border-radius: 12px; padding: 1.5rem; min-width: 0; }
|
|
26
27
|
.card-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem; }
|
|
27
28
|
.card-header h2 { margin-bottom: 0; }
|
|
28
29
|
.card-full { grid-column: 1 / -1; }
|
|
29
30
|
|
|
31
|
+
/* Track tabs */
|
|
32
|
+
.track-tabs { display: inline-flex; gap: 0.25rem; background: #0d1117; border: 1px solid #30363d; border-radius: 8px; padding: 0.25rem; margin-bottom: 1rem; }
|
|
33
|
+
.track-tab-btn { margin-top: 0; padding: 0.4rem 1rem; border: none; border-radius: 6px; background: transparent; color: #8b949e; font-size: 0.85rem; cursor: pointer; }
|
|
34
|
+
.track-tab-btn:hover { color: #e1e4e8; }
|
|
35
|
+
.track-tab-btn.active { background: #30363d; color: #fff; }
|
|
36
|
+
|
|
30
37
|
/* Active timer */
|
|
31
38
|
.active-timer { border-left: 3px solid #3fb950; display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
|
|
32
39
|
.active-timer .timer-info { flex: 1; }
|
|
@@ -69,6 +76,7 @@ export function indexPage() {
|
|
|
69
76
|
|
|
70
77
|
/* Inline form row */
|
|
71
78
|
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
|
79
|
+
.form-row > div { min-width: 0; }
|
|
72
80
|
/* Calendar event cards */
|
|
73
81
|
.cal-event-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 0.75rem; margin-bottom: 0.5rem; }
|
|
74
82
|
.cal-event-card .cal-card-name { color: #e1e4e8; font-size: 0.85rem; font-weight: 500; margin-bottom: 0.5rem; }
|
|
@@ -80,6 +88,7 @@ export function indexPage() {
|
|
|
80
88
|
@media (max-width: 600px) {
|
|
81
89
|
body { padding: 0.75rem; }
|
|
82
90
|
.form-row { grid-template-columns: 1fr; }
|
|
91
|
+
.home-cards { grid-template-columns: 1fr; }
|
|
83
92
|
.header { flex-direction: column; gap: 0; align-items: stretch; margin-bottom: 1rem; }
|
|
84
93
|
.header h1 { display: none; }
|
|
85
94
|
.nav { justify-content: center; flex-wrap: wrap; }
|
|
@@ -97,7 +106,7 @@ export function indexPage() {
|
|
|
97
106
|
.toggle input:checked + .slider::before { transform: translateX(16px); background: #fff; }
|
|
98
107
|
</style>
|
|
99
108
|
</head>
|
|
100
|
-
<body>
|
|
109
|
+
<body oncontextmenu="return false;">
|
|
101
110
|
<div class="header">
|
|
102
111
|
<h1>Clocktopus</h1>
|
|
103
112
|
<div class="nav">
|
|
@@ -120,29 +129,64 @@ export function indexPage() {
|
|
|
120
129
|
<button class="stop-btn" onclick="stopTimer()">Stop Timer</button>
|
|
121
130
|
</div>
|
|
122
131
|
|
|
123
|
-
<div class="cards">
|
|
124
|
-
<!--
|
|
132
|
+
<div class="cards home-cards">
|
|
133
|
+
<!-- Track Time -->
|
|
125
134
|
<div class="card">
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
<
|
|
129
|
-
|
|
135
|
+
<div class="track-tabs">
|
|
136
|
+
<button class="track-tab-btn active" data-mode="auto" onclick="switchTrackMode('auto')">Auto Track</button>
|
|
137
|
+
<button class="track-tab-btn" data-mode="manual" onclick="switchTrackMode('manual')">Manual Log</button>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div id="track-auto">
|
|
141
|
+
<div id="start-timer-form">
|
|
142
|
+
<label for="project-select">Project</label>
|
|
143
|
+
<select id="project-select">
|
|
144
|
+
<option value="">Loading projects...</option>
|
|
145
|
+
</select>
|
|
146
|
+
<div class="form-row">
|
|
147
|
+
<div>
|
|
148
|
+
<label for="timer-description">Description</label>
|
|
149
|
+
<input type="text" id="timer-description" placeholder="What are you working on?" />
|
|
150
|
+
</div>
|
|
151
|
+
<div>
|
|
152
|
+
<label for="timer-jira">Jira Ticket (optional)</label>
|
|
153
|
+
<input type="text" id="timer-jira" placeholder="e.g. PROJ-123" />
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<button id="start-btn" onclick="startTimer()">Start Timer</button>
|
|
157
|
+
</div>
|
|
158
|
+
<div id="last-tasks" style="display:none; margin-top:0.75rem;"></div>
|
|
159
|
+
<div class="msg" id="timer-msg"></div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div id="track-manual" style="display:none;">
|
|
163
|
+
<label for="manual-project">Project</label>
|
|
164
|
+
<select id="manual-project">
|
|
130
165
|
<option value="">Loading projects...</option>
|
|
131
166
|
</select>
|
|
132
167
|
<div class="form-row">
|
|
133
168
|
<div>
|
|
134
|
-
<label for="
|
|
135
|
-
<input type="
|
|
169
|
+
<label for="manual-start">Start</label>
|
|
170
|
+
<input type="datetime-local" id="manual-start" />
|
|
171
|
+
</div>
|
|
172
|
+
<div>
|
|
173
|
+
<label for="manual-end">End</label>
|
|
174
|
+
<input type="datetime-local" id="manual-end" />
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
<div class="form-row">
|
|
178
|
+
<div>
|
|
179
|
+
<label for="manual-description">Description</label>
|
|
180
|
+
<input type="text" id="manual-description" placeholder="What did you work on?" />
|
|
136
181
|
</div>
|
|
137
182
|
<div>
|
|
138
|
-
<label for="
|
|
139
|
-
<input type="text" id="
|
|
183
|
+
<label for="manual-jira">Jira Ticket (optional)</label>
|
|
184
|
+
<input type="text" id="manual-jira" placeholder="e.g. PROJ-123" />
|
|
140
185
|
</div>
|
|
141
186
|
</div>
|
|
142
|
-
<button id="
|
|
187
|
+
<button id="manual-log-btn" onclick="logManualTime()">Log Time</button>
|
|
188
|
+
<div class="msg" id="manual-msg"></div>
|
|
143
189
|
</div>
|
|
144
|
-
<div id="last-tasks" style="display:none; margin-top:0.75rem;"></div>
|
|
145
|
-
<div class="msg" id="timer-msg"></div>
|
|
146
190
|
</div>
|
|
147
191
|
|
|
148
192
|
<!-- Monitor Control -->
|
|
@@ -299,6 +343,11 @@ export function indexPage() {
|
|
|
299
343
|
</div>
|
|
300
344
|
|
|
301
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
|
+
|
|
302
351
|
let elapsedInterval = null;
|
|
303
352
|
let currentPage = 1;
|
|
304
353
|
let totalPages = 1;
|
|
@@ -311,6 +360,14 @@ export function indexPage() {
|
|
|
311
360
|
document.getElementById('nav-' + tab).classList.add('active');
|
|
312
361
|
}
|
|
313
362
|
|
|
363
|
+
function switchTrackMode(mode) {
|
|
364
|
+
document.querySelectorAll('.track-tab-btn').forEach(function(b) {
|
|
365
|
+
b.classList.toggle('active', b.dataset.mode === mode);
|
|
366
|
+
});
|
|
367
|
+
document.getElementById('track-auto').style.display = mode === 'auto' ? 'block' : 'none';
|
|
368
|
+
document.getElementById('track-manual').style.display = mode === 'manual' ? 'block' : 'none';
|
|
369
|
+
}
|
|
370
|
+
|
|
314
371
|
// --- Utilities ---
|
|
315
372
|
function setMsg(id, text, ok) {
|
|
316
373
|
const el = document.getElementById(id);
|
|
@@ -430,6 +487,70 @@ export function indexPage() {
|
|
|
430
487
|
}
|
|
431
488
|
}
|
|
432
489
|
|
|
490
|
+
function toLocalInputValue(date) {
|
|
491
|
+
const pad = function(n) { return String(n).padStart(2, '0'); };
|
|
492
|
+
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) +
|
|
493
|
+
'T' + pad(date.getHours()) + ':' + pad(date.getMinutes());
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function setManualDefaults() {
|
|
497
|
+
const now = new Date();
|
|
498
|
+
const hourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
|
499
|
+
const startEl = document.getElementById('manual-start');
|
|
500
|
+
const endEl = document.getElementById('manual-end');
|
|
501
|
+
if (startEl) startEl.value = toLocalInputValue(hourAgo);
|
|
502
|
+
if (endEl) endEl.value = toLocalInputValue(now);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function logManualTime() {
|
|
506
|
+
const projectId = document.getElementById('manual-project').value;
|
|
507
|
+
const startVal = document.getElementById('manual-start').value;
|
|
508
|
+
const endVal = document.getElementById('manual-end').value;
|
|
509
|
+
const description = document.getElementById('manual-description').value.trim();
|
|
510
|
+
const jiraTicket = document.getElementById('manual-jira').value.trim();
|
|
511
|
+
|
|
512
|
+
if (!projectId) return setMsg('manual-msg', 'Please select a project.', false);
|
|
513
|
+
if (!startVal || !endVal) return setMsg('manual-msg', 'Please set start and end.', false);
|
|
514
|
+
|
|
515
|
+
const startMs = new Date(startVal).getTime();
|
|
516
|
+
const endMs = new Date(endVal).getTime();
|
|
517
|
+
if (isNaN(startMs) || isNaN(endMs)) return setMsg('manual-msg', 'Invalid date.', false);
|
|
518
|
+
if (endMs <= startMs) return setMsg('manual-msg', 'End must be after start.', false);
|
|
519
|
+
if (!description && !jiraTicket) return setMsg('manual-msg', 'Please enter a description or Jira ticket.', false);
|
|
520
|
+
|
|
521
|
+
const btn = document.getElementById('manual-log-btn');
|
|
522
|
+
btn.disabled = true;
|
|
523
|
+
btn.textContent = 'Logging...';
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
const res = await fetch('/api/timer/log', {
|
|
527
|
+
method: 'POST',
|
|
528
|
+
headers: { 'Content-Type': 'application/json' },
|
|
529
|
+
body: JSON.stringify({
|
|
530
|
+
projectId: projectId,
|
|
531
|
+
description: description,
|
|
532
|
+
start: new Date(startMs).toISOString(),
|
|
533
|
+
end: new Date(endMs).toISOString(),
|
|
534
|
+
jiraTicket: jiraTicket || undefined,
|
|
535
|
+
}),
|
|
536
|
+
});
|
|
537
|
+
const data = await res.json();
|
|
538
|
+
if (data.ok) {
|
|
539
|
+
setMsg('manual-msg', 'Time logged.', true);
|
|
540
|
+
document.getElementById('manual-description').value = '';
|
|
541
|
+
document.getElementById('manual-jira').value = '';
|
|
542
|
+
setManualDefaults();
|
|
543
|
+
loadSessions();
|
|
544
|
+
} else {
|
|
545
|
+
setMsg('manual-msg', data.error || 'Failed to log time.', false);
|
|
546
|
+
}
|
|
547
|
+
} catch {
|
|
548
|
+
setMsg('manual-msg', 'Request failed.', false);
|
|
549
|
+
}
|
|
550
|
+
btn.disabled = false;
|
|
551
|
+
btn.textContent = 'Log Time';
|
|
552
|
+
}
|
|
553
|
+
|
|
433
554
|
// --- Last Tasks ---
|
|
434
555
|
var lastTasks = [];
|
|
435
556
|
|
|
@@ -542,20 +663,26 @@ export function indexPage() {
|
|
|
542
663
|
try {
|
|
543
664
|
const res = await fetch('/api/projects');
|
|
544
665
|
const projects = await res.json();
|
|
545
|
-
const
|
|
546
|
-
select
|
|
547
|
-
|
|
548
|
-
select.innerHTML = '<option value="">
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
666
|
+
const selects = [document.getElementById('project-select'), document.getElementById('manual-project')];
|
|
667
|
+
selects.forEach(function(select) {
|
|
668
|
+
if (!select) return;
|
|
669
|
+
select.innerHTML = '<option value="">Select a project</option>';
|
|
670
|
+
if (projects.length === 0) {
|
|
671
|
+
select.innerHTML = '<option value="">No active projects \u2014 pull from Clockify in Settings</option>';
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
projects.forEach(function(p) {
|
|
675
|
+
const opt = document.createElement('option');
|
|
676
|
+
opt.value = p.id;
|
|
677
|
+
opt.textContent = p.name;
|
|
678
|
+
select.appendChild(opt);
|
|
679
|
+
});
|
|
556
680
|
});
|
|
557
681
|
} catch {
|
|
558
|
-
document.getElementById('project-select')
|
|
682
|
+
const select = document.getElementById('project-select');
|
|
683
|
+
if (select) select.innerHTML = '<option value="">Failed to load projects</option>';
|
|
684
|
+
const manual = document.getElementById('manual-project');
|
|
685
|
+
if (manual) manual.innerHTML = '<option value="">Failed to load projects</option>';
|
|
559
686
|
}
|
|
560
687
|
}
|
|
561
688
|
|
|
@@ -1000,6 +1127,7 @@ export function indexPage() {
|
|
|
1000
1127
|
checkActiveTimer();
|
|
1001
1128
|
checkMonitorStatus();
|
|
1002
1129
|
loadAllProjects();
|
|
1130
|
+
setManualDefaults();
|
|
1003
1131
|
|
|
1004
1132
|
</script>
|
|
1005
1133
|
</body>
|
package/dist/lib/db.js
CHANGED
|
@@ -108,6 +108,11 @@ export function logSessionStart(id, projectId, description, startedAt, jiraTicke
|
|
|
108
108
|
const stmt = db.prepare('INSERT OR IGNORE INTO sessions (id, projectId, description, startedAt, isAutoCompleted, jiraTicket) VALUES (?, ?, ?, ?, ?, ?)');
|
|
109
109
|
stmt.run(id, projectId, description, startedAt, 0, jiraTicket ?? null);
|
|
110
110
|
}
|
|
111
|
+
export function logCompletedSession(id, projectId, description, startedAt, completedAt, jiraTicket) {
|
|
112
|
+
const db = getDb();
|
|
113
|
+
const stmt = db.prepare('INSERT OR IGNORE INTO sessions (id, projectId, description, startedAt, completedAt, isAutoCompleted, jiraTicket) VALUES (?, ?, ?, ?, ?, 0, ?)');
|
|
114
|
+
stmt.run(id, projectId, description, startedAt, completedAt, jiraTicket ?? null);
|
|
115
|
+
}
|
|
111
116
|
export function completeLatestSession(completedAt, isAutoCompleted = false) {
|
|
112
117
|
const db = getDb();
|
|
113
118
|
const stmt = db.prepare(`
|