clocktopus 1.2.0 → 1.3.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 +11 -3
- package/dist/dashboard/routes/timer.js +83 -3
- package/dist/dashboard/views.js +149 -26
- package/dist/lib/db.js +14 -1
- 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
|
|
@@ -160,10 +162,16 @@ bun pm cache rm && bun i -g clocktopus@latest
|
|
|
160
162
|
|
|
161
163
|
### Native addons not built (untrusted postinstall)
|
|
162
164
|
|
|
163
|
-
|
|
165
|
+
Bun skips postinstall scripts for untrusted packages. Install with `--trust` to fix this:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
bun install -g clocktopus --trust
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Or if already installed, rebuild manually:
|
|
164
172
|
|
|
165
173
|
```bash
|
|
166
|
-
bun
|
|
174
|
+
cd ~/.bun/install/global/node_modules/macos-notification-state && npx node-gyp rebuild
|
|
167
175
|
```
|
|
168
176
|
|
|
169
177
|
### Linux
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
3
|
import { Clockify } from '../../clockify.js';
|
|
3
|
-
import { completeLatestSession } from '../../lib/db.js';
|
|
4
|
+
import { completeLatestSession, getOpenSession, logCompletedSession, logSessionStart } from '../../lib/db.js';
|
|
5
|
+
import { stopJiraTimer } from '../../lib/jira.js';
|
|
6
|
+
function extractJiraTicket(description) {
|
|
7
|
+
const match = description.match(/\b([A-Z][A-Z0-9]+-\d+)\b/);
|
|
8
|
+
return match?.[1];
|
|
9
|
+
}
|
|
4
10
|
const timerRoutes = new Hono();
|
|
5
11
|
timerRoutes.get('/timer/active', async (c) => {
|
|
6
12
|
try {
|
|
@@ -11,11 +17,20 @@ timerRoutes.get('/timer/active', async (c) => {
|
|
|
11
17
|
const timer = await clockify.getActiveTimer(user.defaultWorkspace, user.id);
|
|
12
18
|
if (!timer)
|
|
13
19
|
return c.json({ active: false });
|
|
20
|
+
// Sync externally-started timers (e.g. from Clockify app or Jira plugin) to DB
|
|
21
|
+
const timerStart = timer.timeInterval.start;
|
|
22
|
+
const jiraTicket = extractJiraTicket(timer.description ?? '');
|
|
23
|
+
const openSession = getOpenSession();
|
|
24
|
+
const alreadyTracked = openSession && openSession.startedAt.slice(0, 19) === timerStart.slice(0, 19);
|
|
25
|
+
if (!alreadyTracked) {
|
|
26
|
+
logSessionStart(timer.id ?? uuidv4(), timer.projectId, timer.description ?? '', timerStart, jiraTicket);
|
|
27
|
+
}
|
|
14
28
|
return c.json({
|
|
15
29
|
active: true,
|
|
16
30
|
description: timer.description,
|
|
17
31
|
projectId: timer.projectId,
|
|
18
|
-
start:
|
|
32
|
+
start: timerStart,
|
|
33
|
+
...(jiraTicket ? { jiraTicket } : {}),
|
|
19
34
|
});
|
|
20
35
|
}
|
|
21
36
|
catch {
|
|
@@ -47,14 +62,79 @@ timerRoutes.post('/timer/stop', async (c) => {
|
|
|
47
62
|
const user = await clockify.getUser();
|
|
48
63
|
if (!user)
|
|
49
64
|
return c.json({ ok: false, error: 'Could not connect to Clockify.' }, 500);
|
|
65
|
+
const openSession = getOpenSession();
|
|
50
66
|
const result = await clockify.stopTimer(user.defaultWorkspace, user.id);
|
|
51
67
|
if (!result)
|
|
52
68
|
return c.json({ ok: false, error: 'Failed to stop timer.' }, 500);
|
|
53
|
-
|
|
69
|
+
const completedAt = new Date().toISOString();
|
|
70
|
+
completeLatestSession(completedAt, false);
|
|
71
|
+
if (openSession?.jiraTicket) {
|
|
72
|
+
const timeSpentSeconds = Math.round((new Date(completedAt).getTime() - new Date(openSession.startedAt).getTime()) / 1000);
|
|
73
|
+
if (timeSpentSeconds >= 60) {
|
|
74
|
+
try {
|
|
75
|
+
await stopJiraTimer(openSession.jiraTicket, timeSpentSeconds);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
console.error('Error stopping Jira timer:', err);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
54
82
|
return c.json({ ok: true });
|
|
55
83
|
}
|
|
56
84
|
catch {
|
|
57
85
|
return c.json({ ok: false, error: 'Failed to stop timer.' }, 500);
|
|
58
86
|
}
|
|
59
87
|
});
|
|
88
|
+
timerRoutes.post('/timer/log', async (c) => {
|
|
89
|
+
const { projectId, description, start, end, jiraTicket } = await c.req.json();
|
|
90
|
+
if (!projectId) {
|
|
91
|
+
return c.json({ ok: false, error: 'Project is required.' }, 400);
|
|
92
|
+
}
|
|
93
|
+
if (!start || !end) {
|
|
94
|
+
return c.json({ ok: false, error: 'Start and end are required.' }, 400);
|
|
95
|
+
}
|
|
96
|
+
const startMs = new Date(start).getTime();
|
|
97
|
+
const endMs = new Date(end).getTime();
|
|
98
|
+
if (Number.isNaN(startMs) || Number.isNaN(endMs)) {
|
|
99
|
+
return c.json({ ok: false, error: 'Invalid start or end date.' }, 400);
|
|
100
|
+
}
|
|
101
|
+
if (endMs <= startMs) {
|
|
102
|
+
return c.json({ ok: false, error: 'End must be after start.' }, 400);
|
|
103
|
+
}
|
|
104
|
+
const cleanDescription = (description ?? '').trim();
|
|
105
|
+
const cleanJira = jiraTicket?.trim() || undefined;
|
|
106
|
+
if (!cleanDescription && !cleanJira) {
|
|
107
|
+
return c.json({ ok: false, error: 'Description or Jira ticket is required.' }, 400);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const clockify = new Clockify();
|
|
111
|
+
const user = await clockify.getUser();
|
|
112
|
+
if (!user)
|
|
113
|
+
return c.json({ ok: false, error: 'Could not connect to Clockify.' }, 500);
|
|
114
|
+
const startIso = new Date(startMs).toISOString();
|
|
115
|
+
const endIso = new Date(endMs).toISOString();
|
|
116
|
+
const finalDescription = cleanDescription || cleanJira;
|
|
117
|
+
const entry = await clockify.logTime(user.defaultWorkspace, projectId, startIso, endIso, finalDescription);
|
|
118
|
+
if (!entry)
|
|
119
|
+
return c.json({ ok: false, error: 'Failed to log time in Clockify.' }, 500);
|
|
120
|
+
const entryId = entry.id ?? uuidv4();
|
|
121
|
+
logCompletedSession(entryId, projectId, finalDescription, startIso, endIso, cleanJira);
|
|
122
|
+
if (cleanJira) {
|
|
123
|
+
const timeSpentSeconds = Math.round((endMs - startMs) / 1000);
|
|
124
|
+
if (timeSpentSeconds >= 60) {
|
|
125
|
+
try {
|
|
126
|
+
await stopJiraTimer(cleanJira, timeSpentSeconds);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
console.error('Error posting Jira worklog for manual entry:', err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return c.json({ ok: true });
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
console.error('Error logging manual time:', err);
|
|
137
|
+
return c.json({ ok: false, error: 'Failed to log time.' }, 500);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
60
140
|
export default timerRoutes;
|
package/dist/dashboard/views.js
CHANGED
|
@@ -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; }
|
|
@@ -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" />
|
|
136
171
|
</div>
|
|
137
172
|
<div>
|
|
138
|
-
<label for="
|
|
139
|
-
<input type="
|
|
173
|
+
<label for="manual-end">End</label>
|
|
174
|
+
<input type="datetime-local" id="manual-end" />
|
|
140
175
|
</div>
|
|
141
176
|
</div>
|
|
142
|
-
<
|
|
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?" />
|
|
181
|
+
</div>
|
|
182
|
+
<div>
|
|
183
|
+
<label for="manual-jira">Jira Ticket (optional)</label>
|
|
184
|
+
<input type="text" id="manual-jira" placeholder="e.g. PROJ-123" />
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
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 -->
|
|
@@ -311,6 +355,14 @@ export function indexPage() {
|
|
|
311
355
|
document.getElementById('nav-' + tab).classList.add('active');
|
|
312
356
|
}
|
|
313
357
|
|
|
358
|
+
function switchTrackMode(mode) {
|
|
359
|
+
document.querySelectorAll('.track-tab-btn').forEach(function(b) {
|
|
360
|
+
b.classList.toggle('active', b.dataset.mode === mode);
|
|
361
|
+
});
|
|
362
|
+
document.getElementById('track-auto').style.display = mode === 'auto' ? 'block' : 'none';
|
|
363
|
+
document.getElementById('track-manual').style.display = mode === 'manual' ? 'block' : 'none';
|
|
364
|
+
}
|
|
365
|
+
|
|
314
366
|
// --- Utilities ---
|
|
315
367
|
function setMsg(id, text, ok) {
|
|
316
368
|
const el = document.getElementById(id);
|
|
@@ -430,6 +482,70 @@ export function indexPage() {
|
|
|
430
482
|
}
|
|
431
483
|
}
|
|
432
484
|
|
|
485
|
+
function toLocalInputValue(date) {
|
|
486
|
+
const pad = function(n) { return String(n).padStart(2, '0'); };
|
|
487
|
+
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) +
|
|
488
|
+
'T' + pad(date.getHours()) + ':' + pad(date.getMinutes());
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function setManualDefaults() {
|
|
492
|
+
const now = new Date();
|
|
493
|
+
const hourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
|
494
|
+
const startEl = document.getElementById('manual-start');
|
|
495
|
+
const endEl = document.getElementById('manual-end');
|
|
496
|
+
if (startEl) startEl.value = toLocalInputValue(hourAgo);
|
|
497
|
+
if (endEl) endEl.value = toLocalInputValue(now);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function logManualTime() {
|
|
501
|
+
const projectId = document.getElementById('manual-project').value;
|
|
502
|
+
const startVal = document.getElementById('manual-start').value;
|
|
503
|
+
const endVal = document.getElementById('manual-end').value;
|
|
504
|
+
const description = document.getElementById('manual-description').value.trim();
|
|
505
|
+
const jiraTicket = document.getElementById('manual-jira').value.trim();
|
|
506
|
+
|
|
507
|
+
if (!projectId) return setMsg('manual-msg', 'Please select a project.', false);
|
|
508
|
+
if (!startVal || !endVal) return setMsg('manual-msg', 'Please set start and end.', false);
|
|
509
|
+
|
|
510
|
+
const startMs = new Date(startVal).getTime();
|
|
511
|
+
const endMs = new Date(endVal).getTime();
|
|
512
|
+
if (isNaN(startMs) || isNaN(endMs)) return setMsg('manual-msg', 'Invalid date.', false);
|
|
513
|
+
if (endMs <= startMs) return setMsg('manual-msg', 'End must be after start.', false);
|
|
514
|
+
if (!description && !jiraTicket) return setMsg('manual-msg', 'Please enter a description or Jira ticket.', false);
|
|
515
|
+
|
|
516
|
+
const btn = document.getElementById('manual-log-btn');
|
|
517
|
+
btn.disabled = true;
|
|
518
|
+
btn.textContent = 'Logging...';
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
const res = await fetch('/api/timer/log', {
|
|
522
|
+
method: 'POST',
|
|
523
|
+
headers: { 'Content-Type': 'application/json' },
|
|
524
|
+
body: JSON.stringify({
|
|
525
|
+
projectId: projectId,
|
|
526
|
+
description: description,
|
|
527
|
+
start: new Date(startMs).toISOString(),
|
|
528
|
+
end: new Date(endMs).toISOString(),
|
|
529
|
+
jiraTicket: jiraTicket || undefined,
|
|
530
|
+
}),
|
|
531
|
+
});
|
|
532
|
+
const data = await res.json();
|
|
533
|
+
if (data.ok) {
|
|
534
|
+
setMsg('manual-msg', 'Time logged.', true);
|
|
535
|
+
document.getElementById('manual-description').value = '';
|
|
536
|
+
document.getElementById('manual-jira').value = '';
|
|
537
|
+
setManualDefaults();
|
|
538
|
+
loadSessions();
|
|
539
|
+
} else {
|
|
540
|
+
setMsg('manual-msg', data.error || 'Failed to log time.', false);
|
|
541
|
+
}
|
|
542
|
+
} catch {
|
|
543
|
+
setMsg('manual-msg', 'Request failed.', false);
|
|
544
|
+
}
|
|
545
|
+
btn.disabled = false;
|
|
546
|
+
btn.textContent = 'Log Time';
|
|
547
|
+
}
|
|
548
|
+
|
|
433
549
|
// --- Last Tasks ---
|
|
434
550
|
var lastTasks = [];
|
|
435
551
|
|
|
@@ -542,20 +658,26 @@ export function indexPage() {
|
|
|
542
658
|
try {
|
|
543
659
|
const res = await fetch('/api/projects');
|
|
544
660
|
const projects = await res.json();
|
|
545
|
-
const
|
|
546
|
-
select
|
|
547
|
-
|
|
548
|
-
select.innerHTML = '<option value="">
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
661
|
+
const selects = [document.getElementById('project-select'), document.getElementById('manual-project')];
|
|
662
|
+
selects.forEach(function(select) {
|
|
663
|
+
if (!select) return;
|
|
664
|
+
select.innerHTML = '<option value="">Select a project</option>';
|
|
665
|
+
if (projects.length === 0) {
|
|
666
|
+
select.innerHTML = '<option value="">No active projects \u2014 pull from Clockify in Settings</option>';
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
projects.forEach(function(p) {
|
|
670
|
+
const opt = document.createElement('option');
|
|
671
|
+
opt.value = p.id;
|
|
672
|
+
opt.textContent = p.name;
|
|
673
|
+
select.appendChild(opt);
|
|
674
|
+
});
|
|
556
675
|
});
|
|
557
676
|
} catch {
|
|
558
|
-
document.getElementById('project-select')
|
|
677
|
+
const select = document.getElementById('project-select');
|
|
678
|
+
if (select) select.innerHTML = '<option value="">Failed to load projects</option>';
|
|
679
|
+
const manual = document.getElementById('manual-project');
|
|
680
|
+
if (manual) manual.innerHTML = '<option value="">Failed to load projects</option>';
|
|
559
681
|
}
|
|
560
682
|
}
|
|
561
683
|
|
|
@@ -1000,6 +1122,7 @@ export function indexPage() {
|
|
|
1000
1122
|
checkActiveTimer();
|
|
1001
1123
|
checkMonitorStatus();
|
|
1002
1124
|
loadAllProjects();
|
|
1125
|
+
setManualDefaults();
|
|
1003
1126
|
|
|
1004
1127
|
</script>
|
|
1005
1128
|
</body>
|
package/dist/lib/db.js
CHANGED
|
@@ -105,9 +105,14 @@ export function getLatestToken() {
|
|
|
105
105
|
}
|
|
106
106
|
export function logSessionStart(id, projectId, description, startedAt, jiraTicket) {
|
|
107
107
|
const db = getDb();
|
|
108
|
-
const stmt = db.prepare('INSERT INTO sessions (id, projectId, description, startedAt, isAutoCompleted, jiraTicket) VALUES (?, ?, ?, ?, ?, ?)');
|
|
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(`
|
|
@@ -130,6 +135,14 @@ export function getSessionCount() {
|
|
|
130
135
|
const row = stmt.get();
|
|
131
136
|
return row.count;
|
|
132
137
|
}
|
|
138
|
+
export function getOpenSession() {
|
|
139
|
+
const db = getDb();
|
|
140
|
+
const stmt = db.prepare('SELECT * FROM sessions WHERE completedAt IS NULL ORDER BY startedAt DESC LIMIT 1');
|
|
141
|
+
const row = stmt.get();
|
|
142
|
+
if (!row)
|
|
143
|
+
return null;
|
|
144
|
+
return SessionSchema.parse(row);
|
|
145
|
+
}
|
|
133
146
|
export function getLatestSession() {
|
|
134
147
|
const db = getDb();
|
|
135
148
|
const stmt = db.prepare(`
|