workday-daemon 0.1.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 +95 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +710 -0
- package/dist/cli.js.map +1 -0
- package/dist/collectors/git-client.d.ts +16 -0
- package/dist/collectors/git-client.js +58 -0
- package/dist/collectors/git-client.js.map +1 -0
- package/dist/collectors/git-tracker.d.ts +43 -0
- package/dist/collectors/git-tracker.js +168 -0
- package/dist/collectors/git-tracker.js.map +1 -0
- package/dist/collectors/reflog-parser.d.ts +30 -0
- package/dist/collectors/reflog-parser.js +71 -0
- package/dist/collectors/reflog-parser.js.map +1 -0
- package/dist/collectors/snapshot-parser.d.ts +34 -0
- package/dist/collectors/snapshot-parser.js +79 -0
- package/dist/collectors/snapshot-parser.js.map +1 -0
- package/dist/core/activity-evaluator.d.ts +33 -0
- package/dist/core/activity-evaluator.js +94 -0
- package/dist/core/activity-evaluator.js.map +1 -0
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +174 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/constants.d.ts +39 -0
- package/dist/core/constants.js +51 -0
- package/dist/core/constants.js.map +1 -0
- package/dist/core/daily-log.d.ts +56 -0
- package/dist/core/daily-log.js +366 -0
- package/dist/core/daily-log.js.map +1 -0
- package/dist/core/session-tracker.d.ts +110 -0
- package/dist/core/session-tracker.js +460 -0
- package/dist/core/session-tracker.js.map +1 -0
- package/dist/core/status-renderer.d.ts +20 -0
- package/dist/core/status-renderer.js +178 -0
- package/dist/core/status-renderer.js.map +1 -0
- package/dist/core/types.d.ts +320 -0
- package/dist/core/types.js +52 -0
- package/dist/core/types.js.map +1 -0
- package/dist/daemon.d.ts +31 -0
- package/dist/daemon.js +266 -0
- package/dist/daemon.js.map +1 -0
- package/dist/http-server.d.ts +30 -0
- package/dist/http-server.js +342 -0
- package/dist/http-server.js.map +1 -0
- package/dist/push/jira-client.d.ts +5 -0
- package/dist/push/jira-client.js +84 -0
- package/dist/push/jira-client.js.map +1 -0
- package/dist/push/report-builder.d.ts +9 -0
- package/dist/push/report-builder.js +77 -0
- package/dist/push/report-builder.js.map +1 -0
- package/dist/push/tempo-client.d.ts +27 -0
- package/dist/push/tempo-client.js +92 -0
- package/dist/push/tempo-client.js.map +1 -0
- package/dist/push/tempo-pusher.d.ts +17 -0
- package/dist/push/tempo-pusher.js +317 -0
- package/dist/push/tempo-pusher.js.map +1 -0
- package/package.json +35 -0
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { loadConfig, loadSecrets, getDataDir, computeWorkingDate } from './core/config.js';
|
|
5
|
+
import { readDailyLog, writeDailyLog, getOpenPause } from './core/daily-log.js';
|
|
6
|
+
import { GitTracker } from './collectors/git-tracker.js';
|
|
7
|
+
import { SessionTracker } from './core/session-tracker.js';
|
|
8
|
+
import { ActivityEvaluator } from './core/activity-evaluator.js';
|
|
9
|
+
import { HttpServer } from './http-server.js';
|
|
10
|
+
import { StatusRenderer } from './core/status-renderer.js';
|
|
11
|
+
import { ClosedBy } from './core/types.js';
|
|
12
|
+
import { PID_FILE_NAME, CRASH_RECOVERY_LOOKBACK_DAYS } from './core/constants.js';
|
|
13
|
+
export class Daemon {
|
|
14
|
+
config;
|
|
15
|
+
secrets;
|
|
16
|
+
gitTracker;
|
|
17
|
+
sessionTracker;
|
|
18
|
+
activityEvaluator;
|
|
19
|
+
httpServer = null;
|
|
20
|
+
statusRenderer = null;
|
|
21
|
+
pollTimer = null;
|
|
22
|
+
dayBoundaryTimer = null;
|
|
23
|
+
currentDate = '';
|
|
24
|
+
running = false;
|
|
25
|
+
foreground = false;
|
|
26
|
+
startedAt = 0;
|
|
27
|
+
budgetExhaustedLogged = false;
|
|
28
|
+
async start(options) {
|
|
29
|
+
this.foreground = options?.foreground ?? false;
|
|
30
|
+
this.config = loadConfig();
|
|
31
|
+
this.secrets = loadSecrets();
|
|
32
|
+
this.ensureSingleInstance();
|
|
33
|
+
this.currentDate = computeWorkingDate(Date.now(), this.config.dayBoundaryHour, this.config.timezone);
|
|
34
|
+
this.gitTracker = new GitTracker(this.config, this.secrets);
|
|
35
|
+
// Crash recovery: close orphaned sessions from previous days
|
|
36
|
+
this.recoverOrphanedLogs();
|
|
37
|
+
// Load today's log and close any orphaned sessions
|
|
38
|
+
const existingLog = readDailyLog(this.currentDate) ?? undefined;
|
|
39
|
+
this.sessionTracker = new SessionTracker(this.config, existingLog);
|
|
40
|
+
// Set dayStartedAt if not already set (first daemon start of the day)
|
|
41
|
+
this.sessionTracker.setDayStartedAt(new Date().toISOString());
|
|
42
|
+
const crashedCount = this.sessionTracker.closeCrashedSessions();
|
|
43
|
+
if (crashedCount > 0) {
|
|
44
|
+
this.sessionTracker.flush();
|
|
45
|
+
console.log(` Crash recovery: closed ${crashedCount} orphaned session(s) from ${this.currentDate}`);
|
|
46
|
+
}
|
|
47
|
+
// Activity evaluator
|
|
48
|
+
this.activityEvaluator = new ActivityEvaluator(this.config.session.diffPollSeconds);
|
|
49
|
+
this.sessionTracker.onSessionClosed = (sessionId) => this.activityEvaluator.removeSession(sessionId);
|
|
50
|
+
this.writePidFile();
|
|
51
|
+
this.registerShutdownHandlers();
|
|
52
|
+
// HTTP API server
|
|
53
|
+
this.startedAt = Date.now();
|
|
54
|
+
const deps = {
|
|
55
|
+
sessionTracker: this.sessionTracker,
|
|
56
|
+
config: this.config,
|
|
57
|
+
stopCallback: () => this.stopAndExit(),
|
|
58
|
+
getStartedAt: () => this.startedAt,
|
|
59
|
+
getCurrentDate: () => this.currentDate,
|
|
60
|
+
onBudgetFreed: () => { this.budgetExhaustedLogged = false; },
|
|
61
|
+
};
|
|
62
|
+
this.httpServer = new HttpServer(this.config.apiPort, deps);
|
|
63
|
+
await this.httpServer.start();
|
|
64
|
+
this.running = true;
|
|
65
|
+
if (this.foreground) {
|
|
66
|
+
this.statusRenderer = new StatusRenderer({
|
|
67
|
+
sessionTracker: this.sessionTracker,
|
|
68
|
+
config: this.config,
|
|
69
|
+
currentDate: this.currentDate,
|
|
70
|
+
startedAt: this.startedAt,
|
|
71
|
+
timezone: this.config.timezone,
|
|
72
|
+
pollSeconds: this.config.session.diffPollSeconds,
|
|
73
|
+
repos: this.config.repos,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// First poll immediately, then on interval
|
|
77
|
+
await this.pollTick();
|
|
78
|
+
const pollMs = this.config.session.diffPollSeconds * 1000;
|
|
79
|
+
this.pollTimer = setInterval(() => void this.pollTick(), pollMs);
|
|
80
|
+
const boundaryMs = this.config.session.dayBoundaryCheckSeconds * 1000;
|
|
81
|
+
this.dayBoundaryTimer = setInterval(() => this.checkDayBoundary(), boundaryMs);
|
|
82
|
+
if (!this.foreground) {
|
|
83
|
+
console.log(`Daemon started (PID ${process.pid})`);
|
|
84
|
+
console.log(` API: http://127.0.0.1:${this.config.apiPort}`);
|
|
85
|
+
console.log(` Timezone: ${this.config.timezone}`);
|
|
86
|
+
console.log(` Repos: ${this.config.repos.map(r => r.split('/').pop()).join(', ')}`);
|
|
87
|
+
console.log(` Poll: ${this.config.session.diffPollSeconds}s`);
|
|
88
|
+
console.log(` Day boundary: ${this.config.dayBoundaryHour}:00`);
|
|
89
|
+
console.log(` Date: ${this.currentDate}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async stop() {
|
|
93
|
+
if (!this.running)
|
|
94
|
+
return;
|
|
95
|
+
if (this.pollTimer)
|
|
96
|
+
clearInterval(this.pollTimer);
|
|
97
|
+
if (this.dayBoundaryTimer)
|
|
98
|
+
clearInterval(this.dayBoundaryTimer);
|
|
99
|
+
// Final poll to capture last-moment activity before shutdown
|
|
100
|
+
await this.pollTick();
|
|
101
|
+
this.running = false;
|
|
102
|
+
if (this.httpServer)
|
|
103
|
+
await this.httpServer.stop();
|
|
104
|
+
this.sessionTracker.closeAllSessions(ClosedBy.DaemonStop);
|
|
105
|
+
this.sessionTracker.flush();
|
|
106
|
+
this.activityEvaluator.clear();
|
|
107
|
+
this.removePidFile();
|
|
108
|
+
console.log('Daemon stopped.');
|
|
109
|
+
}
|
|
110
|
+
async stopAndExit() {
|
|
111
|
+
await this.stop();
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
// ─── Poll loop ─────────────────────────────────────────────────────────
|
|
115
|
+
async pollTick() {
|
|
116
|
+
if (!this.running)
|
|
117
|
+
return;
|
|
118
|
+
try {
|
|
119
|
+
const results = await this.gitTracker.pollAll();
|
|
120
|
+
// 1. Session lifecycle + evidence
|
|
121
|
+
for (const result of results) {
|
|
122
|
+
this.sessionTracker.processPollResult(result);
|
|
123
|
+
}
|
|
124
|
+
// 2. Build tick inputs for evaluator
|
|
125
|
+
const tickInputs = this.sessionTracker.buildTickInputs(results);
|
|
126
|
+
// 3. Evaluate activity scores and leadership
|
|
127
|
+
const evaluatorResult = this.activityEvaluator.processAllTicks(tickInputs);
|
|
128
|
+
// 4. Apply evaluator decisions (auto-pause/resume, promotion)
|
|
129
|
+
this.sessionTracker.applyEvaluatorResult(evaluatorResult);
|
|
130
|
+
// 5. Budget check — close all sessions if budget exhausted
|
|
131
|
+
if (this.sessionTracker.isBudgetExhausted()) {
|
|
132
|
+
this.sessionTracker.closeBudgetExhausted();
|
|
133
|
+
if (!this.budgetExhaustedLogged) {
|
|
134
|
+
this.budgetExhaustedLogged = true;
|
|
135
|
+
console.warn('[budget] Day budget exhausted — all sessions closed. Use set-start to extend.');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
this.sessionTracker.flush();
|
|
139
|
+
if (this.statusRenderer) {
|
|
140
|
+
this.statusRenderer.render();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
145
|
+
if (this.statusRenderer) {
|
|
146
|
+
this.statusRenderer.renderError(message);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
console.error(`[poll] ${message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// ─── Crash recovery ────────────────────────────────────────────────────
|
|
154
|
+
/** Scan recent daily logs for orphaned sessions (cross-day crash) */
|
|
155
|
+
recoverOrphanedLogs() {
|
|
156
|
+
const lookbackDays = CRASH_RECOVERY_LOOKBACK_DAYS;
|
|
157
|
+
for (let i = 1; i <= lookbackDays; i++) {
|
|
158
|
+
const d = new Date(this.currentDate + 'T12:00:00Z');
|
|
159
|
+
d.setUTCDate(d.getUTCDate() - i);
|
|
160
|
+
const dateStr = `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`;
|
|
161
|
+
const log = readDailyLog(dateStr);
|
|
162
|
+
if (!log)
|
|
163
|
+
continue;
|
|
164
|
+
const openSessions = log.sessions.filter(s => !s.closedBy);
|
|
165
|
+
if (openSessions.length === 0)
|
|
166
|
+
continue;
|
|
167
|
+
for (const session of openSessions) {
|
|
168
|
+
const pause = getOpenPause(session);
|
|
169
|
+
if (pause)
|
|
170
|
+
pause.to = session.lastSeenAt;
|
|
171
|
+
session.closedBy = ClosedBy.DaemonCrash;
|
|
172
|
+
}
|
|
173
|
+
writeDailyLog(log);
|
|
174
|
+
console.log(` Crash recovery: closed ${openSessions.length} orphaned session(s) from ${dateStr}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// ─── Day boundary ─────────────────────────────────────────────────────
|
|
178
|
+
checkDayBoundary() {
|
|
179
|
+
const newDate = computeWorkingDate(Date.now(), this.config.dayBoundaryHour, this.config.timezone);
|
|
180
|
+
if (newDate === this.currentDate)
|
|
181
|
+
return;
|
|
182
|
+
const oldLog = this.sessionTracker.handleDayBoundary();
|
|
183
|
+
writeDailyLog(oldLog);
|
|
184
|
+
this.activityEvaluator.clear();
|
|
185
|
+
const sessionCount = oldLog.sessions.length;
|
|
186
|
+
this.currentDate = newDate;
|
|
187
|
+
if (this.statusRenderer) {
|
|
188
|
+
this.statusRenderer.updateDate(newDate);
|
|
189
|
+
this.statusRenderer.render();
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
console.log(`[day] ${oldLog.date} closed (${sessionCount} sessions) → ${newDate}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// ─── PID file ──────────────────────────────────────────────────────────
|
|
196
|
+
getPidFilePath() {
|
|
197
|
+
return join(getDataDir(), PID_FILE_NAME);
|
|
198
|
+
}
|
|
199
|
+
ensureSingleInstance() {
|
|
200
|
+
const pidPath = this.getPidFilePath();
|
|
201
|
+
if (!existsSync(pidPath))
|
|
202
|
+
return;
|
|
203
|
+
const oldPid = parseInt(readFileSync(pidPath, 'utf-8').trim(), 10);
|
|
204
|
+
if (Daemon.isProcessRunning(oldPid)) {
|
|
205
|
+
console.error(`Daemon already running (PID ${oldPid})`);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
// Stale PID file
|
|
209
|
+
unlinkSync(pidPath);
|
|
210
|
+
}
|
|
211
|
+
static isProcessRunning(pid) {
|
|
212
|
+
try {
|
|
213
|
+
// Signal 0 tests process existence without killing (POSIX idiom)
|
|
214
|
+
process.kill(pid, 0);
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
writePidFile() {
|
|
222
|
+
const dataDir = getDataDir();
|
|
223
|
+
if (!existsSync(dataDir)) {
|
|
224
|
+
mkdirSync(dataDir, { recursive: true });
|
|
225
|
+
}
|
|
226
|
+
writeFileSync(this.getPidFilePath(), String(process.pid), 'utf-8');
|
|
227
|
+
}
|
|
228
|
+
removePidFile() {
|
|
229
|
+
try {
|
|
230
|
+
const pidPath = this.getPidFilePath();
|
|
231
|
+
if (existsSync(pidPath))
|
|
232
|
+
unlinkSync(pidPath);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// ignore cleanup errors
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// ─── Shutdown ──────────────────────────────────────────────────────────
|
|
239
|
+
registerShutdownHandlers() {
|
|
240
|
+
const shutdown = async () => {
|
|
241
|
+
console.log('\nShutting down...');
|
|
242
|
+
await this.stop();
|
|
243
|
+
process.exit(0);
|
|
244
|
+
};
|
|
245
|
+
process.on('SIGINT', () => void shutdown());
|
|
246
|
+
process.on('SIGTERM', () => void shutdown());
|
|
247
|
+
// Last-resort synchronous cleanup (OS shutdown, uncaught exit)
|
|
248
|
+
// closeAllSessions + flush are synchronous (writeFileSync)
|
|
249
|
+
process.on('exit', () => {
|
|
250
|
+
if (!this.running)
|
|
251
|
+
return;
|
|
252
|
+
this.sessionTracker.closeAllSessions(ClosedBy.DaemonStop);
|
|
253
|
+
this.sessionTracker.flush();
|
|
254
|
+
this.removePidFile();
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// ─── Entry point (direct execution / background mode) ────────────────────
|
|
259
|
+
const isMain = process.argv[1] &&
|
|
260
|
+
resolve(process.argv[1]).replace(/\\/g, '/') ===
|
|
261
|
+
fileURLToPath(import.meta.url).replace(/\\/g, '/');
|
|
262
|
+
if (isMain) {
|
|
263
|
+
const daemon = new Daemon();
|
|
264
|
+
await daemon.start();
|
|
265
|
+
}
|
|
266
|
+
//# sourceMappingURL=daemon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon.js","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACzF,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3F,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAChF,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAE3D,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAElF,MAAM,OAAO,MAAM;IACT,MAAM,CAAa;IACnB,OAAO,CAAW;IAClB,UAAU,CAAc;IACxB,cAAc,CAAkB;IAChC,iBAAiB,CAAqB;IACtC,UAAU,GAAsB,IAAI,CAAC;IACrC,cAAc,GAA0B,IAAI,CAAC;IAC7C,SAAS,GAA0C,IAAI,CAAC;IACxD,gBAAgB,GAA0C,IAAI,CAAC;IAC/D,WAAW,GAAW,EAAE,CAAC;IACzB,OAAO,GAAY,KAAK,CAAC;IACzB,UAAU,GAAY,KAAK,CAAC;IAC5B,SAAS,GAAW,CAAC,CAAC;IACtB,qBAAqB,GAAY,KAAK,CAAC;IAExC,KAAK,CAAC,KAAK,CAAC,OAAkC;QACnD,IAAI,CAAC,UAAU,GAAG,OAAO,EAAE,UAAU,IAAI,KAAK,CAAC;QAC/C,IAAI,CAAC,MAAM,GAAG,UAAU,EAAE,CAAC;QAC3B,IAAI,CAAC,OAAO,GAAG,WAAW,EAAE,CAAC;QAE7B,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAE5B,IAAI,CAAC,WAAW,GAAG,kBAAkB,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACrG,IAAI,CAAC,UAAU,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAE5D,6DAA6D;QAC7D,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAE3B,mDAAmD;QACnD,MAAM,WAAW,GAAG,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,SAAS,CAAC;QAChE,IAAI,CAAC,cAAc,GAAG,IAAI,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAEnE,sEAAsE;QACtE,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;QAE9D,MAAM,YAAY,GAAG,IAAI,CAAC,cAAc,CAAC,oBAAoB,EAAE,CAAC;QAChE,IAAI,YAAY,GAAG,CAAC,EAAE,CAAC;YACrB,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC;YAC5B,OAAO,CAAC,GAAG,CAAC,4BAA4B,YAAY,6BAA6B,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;QACvG,CAAC;QAED,qBAAqB;QACrB,IAAI,CAAC,iBAAiB,GAAG,IAAI,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;QACpF,IAAI,CAAC,cAAc,CAAC,eAAe,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;QAErG,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,IAAI,CAAC,wBAAwB,EAAE,CAAC;QAEhC,kBAAkB;QAClB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAmB;YAC3B,cAAc,EAAE,IAAI,CAAC,cAAc;YACnC,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE;YACtC,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS;YAClC,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW;YACtC,aAAa,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,qBAAqB,GAAG,KAAK,CAAC,CAAC,CAAC;SAC7D,CAAC;QACF,IAAI,CAAC,UAAU,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAC5D,MAAM,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QAE9B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QAEpB,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,IAAI,CAAC,cAAc,GAAG,IAAI,cAAc,CAAC;gBACvC,cAAc,EAAE,IAAI,CAAC,cAAc;gBACnC,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ;gBAC9B,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,eAAe;gBAChD,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK;aACzB,CAAC,CAAC;QACL,CAAC;QAED,2CAA2C;QAC3C,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACtB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;QAC1D,IAAI,CAAC,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAC;QACjE,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC;QACtE,IAAI,CAAC,gBAAgB,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,UAAU,CAAC,CAAC;QAE/E,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,OAAO,CAAC,GAAG,CAAC,uBAAuB,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC;YACnD,OAAO,CAAC,GAAG,CAAC,2BAA2B,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;YAC9D,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;YACnD,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACrF,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,eAAe,GAAG,CAAC,CAAC;YAC/D,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,CAAC,MAAM,CAAC,eAAe,KAAK,CAAC,CAAC;YACjE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC;IAEM,KAAK,CAAC,IAAI;QACf,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO;QAE1B,IAAI,IAAI,CAAC,SAAS;YAAE,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAClD,IAAI,IAAI,CAAC,gBAAgB;YAAE,aAAa,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QAEhE,6DAA6D;QAC7D,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QAEtB,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;QACrB,IAAI,IAAI,CAAC,UAAU;YAAE,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;QAClD,IAAI,CAAC,cAAc,CAAC,gBAAgB,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC1D,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC;QAC5B,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,CAAC;QAC/B,IAAI,CAAC,aAAa,EAAE,CAAC;QAErB,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACjC,CAAC;IAEO,KAAK,CAAC,WAAW;QACvB,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,0EAA0E;IAElE,KAAK,CAAC,QAAQ;QACpB,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO;QAE1B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;YAEhD,kCAAkC;YAClC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;gBAC7B,IAAI,CAAC,cAAc,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC;YAChD,CAAC;YAED,qCAAqC;YACrC,MAAM,UAAU,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;YAEhE,6CAA6C;YAC7C,MAAM,eAAe,GAAG,IAAI,CAAC,iBAAiB,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;YAE3E,8DAA8D;YAC9D,IAAI,CAAC,cAAc,CAAC,oBAAoB,CAAC,eAAe,CAAC,CAAC;YAE1D,2DAA2D;YAC3D,IAAI,IAAI,CAAC,cAAc,CAAC,iBAAiB,EAAE,EAAE,CAAC;gBAC5C,IAAI,CAAC,cAAc,CAAC,oBAAoB,EAAE,CAAC;gBAC3C,IAAI,CAAC,IAAI,CAAC,qBAAqB,EAAE,CAAC;oBAChC,IAAI,CAAC,qBAAqB,GAAG,IAAI,CAAC;oBAClC,OAAO,CAAC,IAAI,CAAC,+EAA+E,CAAC,CAAC;gBAChG,CAAC;YACH,CAAC;YAED,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC;YAE5B,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;gBACxB,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC;YAC/B,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvE,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;gBACxB,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;YAC3C,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,KAAK,CAAC,UAAU,OAAO,EAAE,CAAC,CAAC;YACrC,CAAC;QACH,CAAC;IACH,CAAC;IAED,0EAA0E;IAE1E,qEAAqE;IAC7D,mBAAmB;QACzB,MAAM,YAAY,GAAG,4BAA4B,CAAC;QAElD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,YAAY,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,YAAY,CAAC,CAAC;YACpD,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC;YACjC,MAAM,OAAO,GAAG,GAAG,CAAC,CAAC,cAAc,EAAE,IAAI,MAAM,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;YAEnI,MAAM,GAAG,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;YAClC,IAAI,CAAC,GAAG;gBAAE,SAAS;YAEnB,MAAM,YAAY,GAAG,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;YAC3D,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YAExC,KAAK,MAAM,OAAO,IAAI,YAAY,EAAE,CAAC;gBACnC,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;gBACpC,IAAI,KAAK;oBAAE,KAAK,CAAC,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC;gBACzC,OAAO,CAAC,QAAQ,GAAG,QAAQ,CAAC,WAAW,CAAC;YAC1C,CAAC;YAED,aAAa,CAAC,GAAG,CAAC,CAAC;YACnB,OAAO,CAAC,GAAG,CAAC,4BAA4B,YAAY,CAAC,MAAM,6BAA6B,OAAO,EAAE,CAAC,CAAC;QACrG,CAAC;IACH,CAAC;IAED,yEAAyE;IAEjE,gBAAgB;QACtB,MAAM,OAAO,GAAG,kBAAkB,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAClG,IAAI,OAAO,KAAK,IAAI,CAAC,WAAW;YAAE,OAAO;QAEzC,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc,CAAC,iBAAiB,EAAE,CAAC;QACvD,aAAa,CAAC,MAAM,CAAC,CAAC;QACtB,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,CAAC;QAE/B,MAAM,YAAY,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;QAC5C,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC;QAE3B,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YACxC,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC;QAC/B,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,SAAS,MAAM,CAAC,IAAI,YAAY,YAAY,gBAAgB,OAAO,EAAE,CAAC,CAAC;QACrF,CAAC;IACH,CAAC;IAED,0EAA0E;IAElE,cAAc;QACpB,OAAO,IAAI,CAAC,UAAU,EAAE,EAAE,aAAa,CAAC,CAAC;IAC3C,CAAC;IAEO,oBAAoB;QAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;QACtC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,OAAO;QAEjC,MAAM,MAAM,GAAG,QAAQ,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QACnE,IAAI,MAAM,CAAC,gBAAgB,CAAC,MAAM,CAAC,EAAE,CAAC;YACpC,OAAO,CAAC,KAAK,CAAC,+BAA+B,MAAM,GAAG,CAAC,CAAC;YACxD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,iBAAiB;QACjB,UAAU,CAAC,OAAO,CAAC,CAAC;IACtB,CAAC;IAEO,MAAM,CAAC,gBAAgB,CAAC,GAAW;QACzC,IAAI,CAAC;YACH,iEAAiE;YACjE,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YACrB,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAEO,YAAY;QAClB,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;QAC7B,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YACzB,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1C,CAAC;QACD,aAAa,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC;IACrE,CAAC;IAEO,aAAa;QACnB,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;YACtC,IAAI,UAAU,CAAC,OAAO,CAAC;gBAAE,UAAU,CAAC,OAAO,CAAC,CAAC;QAC/C,CAAC;QAAC,MAAM,CAAC;YACP,wBAAwB;QAC1B,CAAC;IACH,CAAC;IAED,0EAA0E;IAElE,wBAAwB;QAC9B,MAAM,QAAQ,GAAG,KAAK,IAAmB,EAAE;YACzC,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;YAClC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;YAClB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC,CAAC;QAEF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,EAAE,CAAC,CAAC;QAC5C,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,EAAE,CAAC,CAAC;QAE7C,+DAA+D;QAC/D,2DAA2D;QAC3D,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;YACtB,IAAI,CAAC,IAAI,CAAC,OAAO;gBAAE,OAAO;YAC1B,IAAI,CAAC,cAAc,CAAC,gBAAgB,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;YAC1D,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC;YAC5B,IAAI,CAAC,aAAa,EAAE,CAAC;QACvB,CAAC,CAAC,CAAC;IACL,CAAC;CACF;AAED,4EAA4E;AAE5E,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5B,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;QAC5C,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAErD,IAAI,MAAM,EAAE,CAAC;IACX,MAAM,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;IAC5B,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;AACvB,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SessionTracker } from './core/session-tracker.js';
|
|
2
|
+
import type { AppConfig } from './core/types.js';
|
|
3
|
+
export interface HttpServerDeps {
|
|
4
|
+
readonly sessionTracker: SessionTracker;
|
|
5
|
+
readonly config: AppConfig;
|
|
6
|
+
readonly stopCallback: () => Promise<void>;
|
|
7
|
+
readonly getStartedAt: () => number;
|
|
8
|
+
readonly getCurrentDate: () => string;
|
|
9
|
+
readonly onBudgetFreed: () => void;
|
|
10
|
+
}
|
|
11
|
+
export declare class HttpServer {
|
|
12
|
+
private server;
|
|
13
|
+
private readonly port;
|
|
14
|
+
private readonly deps;
|
|
15
|
+
constructor(port: number, deps: HttpServerDeps);
|
|
16
|
+
start(): Promise<void>;
|
|
17
|
+
stop(): Promise<void>;
|
|
18
|
+
private handleRequest;
|
|
19
|
+
private handleStatus;
|
|
20
|
+
private handleToday;
|
|
21
|
+
private handlePause;
|
|
22
|
+
private handleAutoPause;
|
|
23
|
+
private handleResume;
|
|
24
|
+
private handleAdjust;
|
|
25
|
+
private handleSetStart;
|
|
26
|
+
private handleDay;
|
|
27
|
+
private toSessionSummary;
|
|
28
|
+
private sendJson;
|
|
29
|
+
private readBody;
|
|
30
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { computeEffectiveDuration, computeTotalPauseDuration, computeManualMinutes, computeBudgetMs, computeTotalClaimedMs, getRemainingBudgetMs, readDailyLog, getOpenPause, } from './core/daily-log.js';
|
|
3
|
+
import { computeWorkingDate, buildTimestamp } from './core/config.js';
|
|
4
|
+
import { MAX_BODY_BYTES } from './core/constants.js';
|
|
5
|
+
export class HttpServer {
|
|
6
|
+
server = null;
|
|
7
|
+
port;
|
|
8
|
+
deps;
|
|
9
|
+
constructor(port, deps) {
|
|
10
|
+
this.port = port;
|
|
11
|
+
this.deps = deps;
|
|
12
|
+
}
|
|
13
|
+
async start() {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
this.server = createServer((req, res) => void this.handleRequest(req, res));
|
|
16
|
+
this.server.on('error', (err) => {
|
|
17
|
+
if (err.code === 'EADDRINUSE') {
|
|
18
|
+
reject(new Error(`Port ${this.port} already in use`));
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
reject(err);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
this.server.listen(this.port, '127.0.0.1', () => resolve());
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async stop() {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
if (!this.server) {
|
|
30
|
+
resolve();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.server.close(() => resolve());
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// ─── Router ─────────────────────────────────────────────────────────
|
|
37
|
+
async handleRequest(req, res) {
|
|
38
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
|
|
39
|
+
const method = req.method ?? 'GET';
|
|
40
|
+
const path = url.pathname;
|
|
41
|
+
try {
|
|
42
|
+
if (method === 'GET' && path === '/api/status') {
|
|
43
|
+
return this.sendJson(res, 200, this.handleStatus());
|
|
44
|
+
}
|
|
45
|
+
if (method === 'GET' && path === '/api/today') {
|
|
46
|
+
return this.sendJson(res, 200, this.handleToday());
|
|
47
|
+
}
|
|
48
|
+
if (method === 'POST' && path === '/api/pause') {
|
|
49
|
+
const body = await this.readBody(req);
|
|
50
|
+
return this.sendJson(res, 200, this.handlePause(body));
|
|
51
|
+
}
|
|
52
|
+
if (method === 'POST' && path === '/api/resume') {
|
|
53
|
+
return this.sendJson(res, 200, this.handleResume());
|
|
54
|
+
}
|
|
55
|
+
if (method === 'POST' && path === '/api/autopause') {
|
|
56
|
+
const body = await this.readBody(req);
|
|
57
|
+
return this.sendJson(res, 200, this.handleAutoPause(body));
|
|
58
|
+
}
|
|
59
|
+
if (method === 'POST' && path === '/api/adjust') {
|
|
60
|
+
const body = await this.readBody(req);
|
|
61
|
+
return this.sendJson(res, 200, this.handleAdjust(body));
|
|
62
|
+
}
|
|
63
|
+
if (method === 'POST' && path === '/api/set-start') {
|
|
64
|
+
const body = await this.readBody(req);
|
|
65
|
+
return this.sendJson(res, 200, this.handleSetStart(body));
|
|
66
|
+
}
|
|
67
|
+
if (method === 'GET' && path === '/api/day') {
|
|
68
|
+
const date = url.searchParams.get('date');
|
|
69
|
+
return this.sendJson(res, 200, this.handleDay(date));
|
|
70
|
+
}
|
|
71
|
+
if (method === 'POST' && path === '/api/stop') {
|
|
72
|
+
const response = { ok: true, data: { message: 'Daemon stopping...' } };
|
|
73
|
+
this.sendJson(res, 200, response);
|
|
74
|
+
setImmediate(() => void this.deps.stopCallback());
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
this.sendJson(res, 404, { ok: false, error: 'Not found' });
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
81
|
+
this.sendJson(res, 500, { ok: false, error: message });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// ─── Handlers ─────────────────────────────────────────────────────
|
|
85
|
+
handleStatus() {
|
|
86
|
+
const tracker = this.deps.sessionTracker;
|
|
87
|
+
const openSessions = tracker.getOpenSessions();
|
|
88
|
+
const summaries = openSessions.map(s => this.toSessionSummary(s, tracker));
|
|
89
|
+
return {
|
|
90
|
+
ok: true,
|
|
91
|
+
data: {
|
|
92
|
+
running: true,
|
|
93
|
+
pid: process.pid,
|
|
94
|
+
date: this.deps.getCurrentDate(),
|
|
95
|
+
uptime: Math.floor((Date.now() - this.deps.getStartedAt()) / 1000),
|
|
96
|
+
openSessions: summaries,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
handleToday() {
|
|
101
|
+
const tracker = this.deps.sessionTracker;
|
|
102
|
+
const log = tracker.getDailyLog();
|
|
103
|
+
const config = this.deps.config;
|
|
104
|
+
const sessions = log.sessions.map(s => ({
|
|
105
|
+
...this.toSessionSummary(s, tracker),
|
|
106
|
+
closedBy: s.closedBy,
|
|
107
|
+
evidence: s.evidence,
|
|
108
|
+
pauseCount: s.pauses.length,
|
|
109
|
+
totalPauseDurationMs: computeTotalPauseDuration(s),
|
|
110
|
+
}));
|
|
111
|
+
const totalEffectiveMs = log.sessions.reduce((sum, s) => sum + computeEffectiveDuration(s), 0);
|
|
112
|
+
return {
|
|
113
|
+
ok: true,
|
|
114
|
+
data: {
|
|
115
|
+
date: log.date,
|
|
116
|
+
dayType: log.dayType,
|
|
117
|
+
status: log.status,
|
|
118
|
+
sessions,
|
|
119
|
+
totalEffectiveMs,
|
|
120
|
+
signalCount: log.signals.length,
|
|
121
|
+
budgetMs: computeBudgetMs(log, config),
|
|
122
|
+
claimedMs: computeTotalClaimedMs(log),
|
|
123
|
+
remainingBudgetMs: getRemainingBudgetMs(log, config),
|
|
124
|
+
dayStartedAt: log.dayStartedAt,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
handlePause(body) {
|
|
129
|
+
const tracker = this.deps.sessionTracker;
|
|
130
|
+
const repo = typeof body.repo === 'string' ? body.repo : null;
|
|
131
|
+
const paused = [];
|
|
132
|
+
if (repo) {
|
|
133
|
+
if (tracker.pauseRepoSession(repo)) {
|
|
134
|
+
paused.push(repo);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
const before = tracker.getOpenSessions().filter(s => !tracker.hasOpenPause(s));
|
|
139
|
+
tracker.pauseAllSessions();
|
|
140
|
+
paused.push(...before.map(s => s.repo));
|
|
141
|
+
}
|
|
142
|
+
tracker.flush();
|
|
143
|
+
return { ok: true, data: { paused } };
|
|
144
|
+
}
|
|
145
|
+
handleAutoPause(body) {
|
|
146
|
+
const tracker = this.deps.sessionTracker;
|
|
147
|
+
// { enabled: boolean, repo?: string } — enabled=true means autopause ON (not disabled)
|
|
148
|
+
const enabled = body.enabled !== false;
|
|
149
|
+
const repo = typeof body.repo === 'string' ? body.repo : undefined;
|
|
150
|
+
const disabled = !enabled;
|
|
151
|
+
const affected = tracker.setAutoPauseDisabled(disabled, repo);
|
|
152
|
+
tracker.flush();
|
|
153
|
+
return {
|
|
154
|
+
ok: true,
|
|
155
|
+
data: {
|
|
156
|
+
repo: repo ?? null,
|
|
157
|
+
autoPauseDisabled: disabled,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
handleResume() {
|
|
162
|
+
const tracker = this.deps.sessionTracker;
|
|
163
|
+
const before = tracker.getOpenSessions().filter(s => tracker.hasOpenPause(s));
|
|
164
|
+
tracker.resumeAllSessions();
|
|
165
|
+
tracker.flush();
|
|
166
|
+
return { ok: true, data: { resumed: before.map(s => s.repo) } };
|
|
167
|
+
}
|
|
168
|
+
handleAdjust(body) {
|
|
169
|
+
const target = typeof body.target === 'string' ? body.target : '';
|
|
170
|
+
const minutes = typeof body.minutes === 'number' ? body.minutes : 0;
|
|
171
|
+
const reason = typeof body.reason === 'string' ? body.reason : '';
|
|
172
|
+
if (!target)
|
|
173
|
+
return { ok: false, error: 'Missing target (session index or id)' };
|
|
174
|
+
if (!reason)
|
|
175
|
+
return { ok: false, error: 'Missing reason' };
|
|
176
|
+
const tracker = this.deps.sessionTracker;
|
|
177
|
+
const result = tracker.addAdjustment(target, minutes, reason);
|
|
178
|
+
if (!result.ok) {
|
|
179
|
+
return { ok: false, error: result.error };
|
|
180
|
+
}
|
|
181
|
+
tracker.flush();
|
|
182
|
+
const log = tracker.getDailyLog();
|
|
183
|
+
const session = log.sessions.find(s => s.id === result.sessionId);
|
|
184
|
+
return {
|
|
185
|
+
ok: true,
|
|
186
|
+
data: {
|
|
187
|
+
sessionId: session.id,
|
|
188
|
+
repo: session.repo,
|
|
189
|
+
task: session.task,
|
|
190
|
+
addedMinutes: minutes,
|
|
191
|
+
totalManualMinutes: computeManualMinutes(session),
|
|
192
|
+
remainingBudgetMs: getRemainingBudgetMs(log, this.deps.config),
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
handleSetStart(body) {
|
|
197
|
+
const time = typeof body.time === 'string' ? body.time : '';
|
|
198
|
+
if (!time)
|
|
199
|
+
return { ok: false, error: 'Missing time (HH:MM format)' };
|
|
200
|
+
// Parse HH:MM to ISO timestamp for current working date
|
|
201
|
+
const match = time.match(/^(\d{1,2}):(\d{2})$/);
|
|
202
|
+
if (!match)
|
|
203
|
+
return { ok: false, error: 'Invalid time format. Use HH:MM' };
|
|
204
|
+
const tracker = this.deps.sessionTracker;
|
|
205
|
+
const log = tracker.getDailyLog();
|
|
206
|
+
const config = this.deps.config;
|
|
207
|
+
// Build ISO timestamp from current date + provided time in config timezone
|
|
208
|
+
const isoTimestamp = buildTimestamp(log.date, parseInt(match[1]), parseInt(match[2]), config.timezone);
|
|
209
|
+
const result = tracker.setManualDayStart(isoTimestamp);
|
|
210
|
+
if (!result.ok) {
|
|
211
|
+
return { ok: false, error: result.error };
|
|
212
|
+
}
|
|
213
|
+
tracker.flush();
|
|
214
|
+
// If budget was exhausted and now freed, notify daemon
|
|
215
|
+
if (!tracker.isBudgetExhausted()) {
|
|
216
|
+
this.deps.onBudgetFreed();
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
ok: true,
|
|
220
|
+
data: {
|
|
221
|
+
dayStart: isoTimestamp,
|
|
222
|
+
budgetMs: computeBudgetMs(log, config),
|
|
223
|
+
remainingBudgetMs: getRemainingBudgetMs(log, config),
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
handleDay(date) {
|
|
228
|
+
if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
229
|
+
return { ok: false, error: 'Missing or invalid date. Use ?date=YYYY-MM-DD' };
|
|
230
|
+
}
|
|
231
|
+
const config = this.deps.config;
|
|
232
|
+
// If requesting today, delegate to handleToday
|
|
233
|
+
const today = computeWorkingDate(Date.now(), config.dayBoundaryHour, config.timezone);
|
|
234
|
+
if (date === today) {
|
|
235
|
+
return this.handleToday();
|
|
236
|
+
}
|
|
237
|
+
// Read past day from disk
|
|
238
|
+
const log = readDailyLog(date);
|
|
239
|
+
if (!log) {
|
|
240
|
+
return { ok: false, error: `No data for ${date}` };
|
|
241
|
+
}
|
|
242
|
+
const sessions = log.sessions.map(s => ({
|
|
243
|
+
id: s.id,
|
|
244
|
+
repo: s.repo,
|
|
245
|
+
task: s.task,
|
|
246
|
+
branch: s.branch,
|
|
247
|
+
state: s.state,
|
|
248
|
+
startedAt: s.startedAt,
|
|
249
|
+
activatedAt: s.activatedAt,
|
|
250
|
+
lastSeenAt: s.lastSeenAt,
|
|
251
|
+
paused: false,
|
|
252
|
+
pauseSource: null,
|
|
253
|
+
effectiveDurationMs: computeEffectiveDuration(s),
|
|
254
|
+
manualMinutes: computeManualMinutes(s),
|
|
255
|
+
score: 0,
|
|
256
|
+
normalizedScore: 0,
|
|
257
|
+
isLeader: false,
|
|
258
|
+
autoPauseDisabled: false,
|
|
259
|
+
closedBy: s.closedBy,
|
|
260
|
+
evidence: s.evidence,
|
|
261
|
+
pauseCount: s.pauses.length,
|
|
262
|
+
totalPauseDurationMs: computeTotalPauseDuration(s),
|
|
263
|
+
}));
|
|
264
|
+
const totalEffectiveMs = log.sessions.reduce((sum, s) => sum + computeEffectiveDuration(s), 0);
|
|
265
|
+
return {
|
|
266
|
+
ok: true,
|
|
267
|
+
data: {
|
|
268
|
+
date: log.date,
|
|
269
|
+
dayType: log.dayType,
|
|
270
|
+
status: log.status,
|
|
271
|
+
sessions,
|
|
272
|
+
totalEffectiveMs,
|
|
273
|
+
signalCount: log.signals.length,
|
|
274
|
+
budgetMs: computeBudgetMs(log, config),
|
|
275
|
+
claimedMs: computeTotalClaimedMs(log),
|
|
276
|
+
remainingBudgetMs: getRemainingBudgetMs(log, config),
|
|
277
|
+
dayStartedAt: log.dayStartedAt,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// ─── Helpers ──────────────────────────────────────────────────────
|
|
282
|
+
toSessionSummary(session, tracker) {
|
|
283
|
+
const evalResult = tracker.getLastEvaluatorResult();
|
|
284
|
+
const sessionScore = evalResult?.scores.get(session.id);
|
|
285
|
+
const openPause = getOpenPause(session);
|
|
286
|
+
return {
|
|
287
|
+
id: session.id,
|
|
288
|
+
repo: session.repo,
|
|
289
|
+
task: session.task,
|
|
290
|
+
branch: session.branch,
|
|
291
|
+
state: session.state,
|
|
292
|
+
startedAt: session.startedAt,
|
|
293
|
+
activatedAt: session.activatedAt ?? null,
|
|
294
|
+
lastSeenAt: session.lastSeenAt,
|
|
295
|
+
paused: tracker.hasOpenPause(session),
|
|
296
|
+
pauseSource: openPause?.source ?? null,
|
|
297
|
+
effectiveDurationMs: computeEffectiveDuration(session),
|
|
298
|
+
manualMinutes: computeManualMinutes(session),
|
|
299
|
+
score: sessionScore?.score ?? 0,
|
|
300
|
+
normalizedScore: sessionScore?.normalizedScore ?? 0,
|
|
301
|
+
isLeader: evalResult?.leaderId === session.id,
|
|
302
|
+
autoPauseDisabled: tracker.isAutoPauseDisabled(session.id),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
sendJson(res, statusCode, data) {
|
|
306
|
+
const body = JSON.stringify(data);
|
|
307
|
+
res.writeHead(statusCode, {
|
|
308
|
+
'Content-Type': 'application/json',
|
|
309
|
+
'Content-Length': Buffer.byteLength(body),
|
|
310
|
+
});
|
|
311
|
+
res.end(body);
|
|
312
|
+
}
|
|
313
|
+
readBody(req) {
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
const chunks = [];
|
|
316
|
+
let size = 0;
|
|
317
|
+
req.on('data', (chunk) => {
|
|
318
|
+
size += chunk.length;
|
|
319
|
+
if (size > MAX_BODY_BYTES) {
|
|
320
|
+
req.destroy();
|
|
321
|
+
reject(new Error('Request body too large'));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
chunks.push(chunk);
|
|
325
|
+
});
|
|
326
|
+
req.on('end', () => {
|
|
327
|
+
if (size === 0) {
|
|
328
|
+
resolve({});
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8')));
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
reject(new Error('Invalid JSON body'));
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
req.on('error', reject);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
//# sourceMappingURL=http-server.js.map
|