hotsheet 0.3.0 → 0.5.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 +45 -19
- package/dist/channel.js +97 -0
- package/dist/cli.js +857 -133
- package/dist/client/app.global.js +99 -3
- package/dist/client/styles.css +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -104,9 +104,16 @@ async function initSchema(db2) {
|
|
|
104
104
|
INSERT INTO settings (key, value) VALUES ('completed_cleanup_days', '30') ON CONFLICT DO NOTHING;
|
|
105
105
|
INSERT INTO settings (key, value) VALUES ('verified_cleanup_days', '30') ON CONFLICT DO NOTHING;
|
|
106
106
|
`);
|
|
107
|
+
await db2.exec(`
|
|
108
|
+
CREATE TABLE IF NOT EXISTS stats_snapshots (
|
|
109
|
+
date TEXT PRIMARY KEY,
|
|
110
|
+
data TEXT NOT NULL DEFAULT '{}'
|
|
111
|
+
);
|
|
112
|
+
`);
|
|
107
113
|
await db2.exec(`
|
|
108
114
|
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS notes TEXT NOT NULL DEFAULT '';
|
|
109
115
|
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS verified_at TIMESTAMP;
|
|
116
|
+
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS tags TEXT NOT NULL DEFAULT '[]';
|
|
110
117
|
`).catch(() => {
|
|
111
118
|
});
|
|
112
119
|
}
|
|
@@ -156,6 +163,201 @@ var init_file_settings = __esm({
|
|
|
156
163
|
}
|
|
157
164
|
});
|
|
158
165
|
|
|
166
|
+
// src/db/stats.ts
|
|
167
|
+
var stats_exports = {};
|
|
168
|
+
__export(stats_exports, {
|
|
169
|
+
backfillSnapshots: () => backfillSnapshots,
|
|
170
|
+
getDashboardStats: () => getDashboardStats,
|
|
171
|
+
getSnapshots: () => getSnapshots,
|
|
172
|
+
recordDailySnapshot: () => recordDailySnapshot
|
|
173
|
+
});
|
|
174
|
+
async function recordDailySnapshot() {
|
|
175
|
+
const db2 = await getDb();
|
|
176
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
177
|
+
const existing = await db2.query(`SELECT date FROM stats_snapshots WHERE date = $1`, [today]);
|
|
178
|
+
if (existing.rows.length > 0) return;
|
|
179
|
+
const result = await db2.query(
|
|
180
|
+
`SELECT status, COUNT(*) as count FROM tickets WHERE status != 'deleted' GROUP BY status`
|
|
181
|
+
);
|
|
182
|
+
const data = { not_started: 0, started: 0, completed: 0, verified: 0, backlog: 0, archive: 0 };
|
|
183
|
+
for (const row of result.rows) {
|
|
184
|
+
if (row.status in data) {
|
|
185
|
+
data[row.status] = parseInt(row.count, 10);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
await db2.query(
|
|
189
|
+
`INSERT INTO stats_snapshots (date, data) VALUES ($1, $2) ON CONFLICT (date) DO UPDATE SET data = $2`,
|
|
190
|
+
[today, JSON.stringify(data)]
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
async function backfillSnapshots() {
|
|
194
|
+
const db2 = await getDb();
|
|
195
|
+
const earliest = await db2.query(`SELECT MIN(DATE(created_at)) as min_date FROM tickets`);
|
|
196
|
+
if (!earliest.rows[0]?.min_date) return;
|
|
197
|
+
const startDate = new Date(earliest.rows[0].min_date);
|
|
198
|
+
const today = /* @__PURE__ */ new Date();
|
|
199
|
+
today.setHours(0, 0, 0, 0);
|
|
200
|
+
const existingRows = await db2.query(`SELECT date FROM stats_snapshots`);
|
|
201
|
+
const existingDates = new Set(existingRows.rows.map((r) => r.date));
|
|
202
|
+
const current = new Date(startDate);
|
|
203
|
+
while (current <= today) {
|
|
204
|
+
const dateStr = current.toISOString().slice(0, 10);
|
|
205
|
+
if (!existingDates.has(dateStr)) {
|
|
206
|
+
const dateEnd = dateStr + "T23:59:59.999Z";
|
|
207
|
+
const result = await db2.query(`
|
|
208
|
+
SELECT
|
|
209
|
+
CASE
|
|
210
|
+
WHEN verified_at IS NOT NULL AND verified_at <= $1 THEN 'verified'
|
|
211
|
+
WHEN completed_at IS NOT NULL AND completed_at <= $1 THEN 'completed'
|
|
212
|
+
WHEN deleted_at IS NOT NULL AND deleted_at <= $1 THEN 'deleted'
|
|
213
|
+
WHEN status = 'backlog' THEN 'backlog'
|
|
214
|
+
WHEN status = 'archive' THEN 'archive'
|
|
215
|
+
WHEN status = 'started' THEN 'started'
|
|
216
|
+
ELSE 'not_started'
|
|
217
|
+
END as status,
|
|
218
|
+
COUNT(*) as count
|
|
219
|
+
FROM tickets
|
|
220
|
+
WHERE created_at <= $1
|
|
221
|
+
GROUP BY 1
|
|
222
|
+
`, [dateEnd]);
|
|
223
|
+
const data = { not_started: 0, started: 0, completed: 0, verified: 0, backlog: 0, archive: 0 };
|
|
224
|
+
for (const row of result.rows) {
|
|
225
|
+
if (row.status in data) {
|
|
226
|
+
data[row.status] = parseInt(row.count, 10);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
await db2.query(
|
|
230
|
+
`INSERT INTO stats_snapshots (date, data) VALUES ($1, $2) ON CONFLICT (date) DO NOTHING`,
|
|
231
|
+
[dateStr, JSON.stringify(data)]
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
current.setDate(current.getDate() + 1);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function getSnapshots(days) {
|
|
238
|
+
const db2 = await getDb();
|
|
239
|
+
const since = /* @__PURE__ */ new Date();
|
|
240
|
+
since.setDate(since.getDate() - days);
|
|
241
|
+
const sinceStr = since.toISOString().slice(0, 10);
|
|
242
|
+
const result = await db2.query(
|
|
243
|
+
`SELECT date, data FROM stats_snapshots WHERE date >= $1 ORDER BY date ASC`,
|
|
244
|
+
[sinceStr]
|
|
245
|
+
);
|
|
246
|
+
return result.rows.map((r) => ({
|
|
247
|
+
date: r.date,
|
|
248
|
+
data: JSON.parse(r.data)
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
async function getDashboardStats(days) {
|
|
252
|
+
const db2 = await getDb();
|
|
253
|
+
const since = /* @__PURE__ */ new Date();
|
|
254
|
+
since.setDate(since.getDate() - days);
|
|
255
|
+
const sinceStr = since.toISOString();
|
|
256
|
+
const completedByDay = await db2.query(
|
|
257
|
+
`SELECT DATE(completed_at) as date, COUNT(*) as count FROM tickets
|
|
258
|
+
WHERE completed_at >= $1 AND completed_at IS NOT NULL
|
|
259
|
+
GROUP BY DATE(completed_at) ORDER BY date ASC`,
|
|
260
|
+
[sinceStr]
|
|
261
|
+
);
|
|
262
|
+
const createdByDay = await db2.query(
|
|
263
|
+
`SELECT DATE(created_at) as date, COUNT(*) as count FROM tickets
|
|
264
|
+
WHERE created_at >= $1
|
|
265
|
+
GROUP BY DATE(created_at) ORDER BY date ASC`,
|
|
266
|
+
[sinceStr]
|
|
267
|
+
);
|
|
268
|
+
const dateMap = /* @__PURE__ */ new Map();
|
|
269
|
+
const current = new Date(since);
|
|
270
|
+
const today = /* @__PURE__ */ new Date();
|
|
271
|
+
while (current <= today) {
|
|
272
|
+
const d = current.toISOString().slice(0, 10);
|
|
273
|
+
dateMap.set(d, { completed: 0, created: 0 });
|
|
274
|
+
current.setDate(current.getDate() + 1);
|
|
275
|
+
}
|
|
276
|
+
for (const r of completedByDay.rows) {
|
|
277
|
+
const d = typeof r.date === "string" ? r.date.slice(0, 10) : new Date(r.date).toISOString().slice(0, 10);
|
|
278
|
+
const entry = dateMap.get(d);
|
|
279
|
+
if (entry) entry.completed = parseInt(r.count, 10);
|
|
280
|
+
}
|
|
281
|
+
for (const r of createdByDay.rows) {
|
|
282
|
+
const d = typeof r.date === "string" ? r.date.slice(0, 10) : new Date(r.date).toISOString().slice(0, 10);
|
|
283
|
+
const entry = dateMap.get(d);
|
|
284
|
+
if (entry) entry.created = parseInt(r.count, 10);
|
|
285
|
+
}
|
|
286
|
+
const throughput = Array.from(dateMap.entries()).map(([date, counts]) => ({ date, ...counts }));
|
|
287
|
+
const cycleTimeResult = await db2.query(
|
|
288
|
+
`SELECT ticket_number, title, completed_at, created_at FROM tickets
|
|
289
|
+
WHERE completed_at >= $1 AND completed_at IS NOT NULL AND status IN ('completed', 'verified')
|
|
290
|
+
ORDER BY completed_at ASC`,
|
|
291
|
+
[sinceStr]
|
|
292
|
+
);
|
|
293
|
+
const cycleTime = cycleTimeResult.rows.map((r) => ({
|
|
294
|
+
ticket_number: r.ticket_number,
|
|
295
|
+
title: r.title,
|
|
296
|
+
completed_at: r.completed_at,
|
|
297
|
+
days: Math.max(0, Math.round((new Date(r.completed_at).getTime() - new Date(r.created_at).getTime()) / 864e5))
|
|
298
|
+
}));
|
|
299
|
+
const catResult = await db2.query(
|
|
300
|
+
`SELECT category, COUNT(*) as count FROM tickets
|
|
301
|
+
WHERE status IN ('not_started', 'started')
|
|
302
|
+
GROUP BY category ORDER BY count DESC`
|
|
303
|
+
);
|
|
304
|
+
const categoryBreakdown = catResult.rows.map((r) => ({ category: r.category, count: parseInt(r.count, 10) }));
|
|
305
|
+
const catPeriodResult = await db2.query(
|
|
306
|
+
`SELECT category, COUNT(*) as count FROM tickets
|
|
307
|
+
WHERE status != 'deleted' AND (
|
|
308
|
+
created_at >= $1 OR
|
|
309
|
+
(completed_at IS NOT NULL AND completed_at >= $1) OR
|
|
310
|
+
(verified_at IS NOT NULL AND verified_at >= $1) OR
|
|
311
|
+
updated_at >= $1
|
|
312
|
+
)
|
|
313
|
+
GROUP BY category ORDER BY count DESC`,
|
|
314
|
+
[sinceStr]
|
|
315
|
+
);
|
|
316
|
+
const categoryPeriod = catPeriodResult.rows.map((r) => ({ category: r.category, count: parseInt(r.count, 10) }));
|
|
317
|
+
const now = /* @__PURE__ */ new Date();
|
|
318
|
+
const weekStart = new Date(now);
|
|
319
|
+
weekStart.setDate(weekStart.getDate() - weekStart.getDay());
|
|
320
|
+
weekStart.setHours(0, 0, 0, 0);
|
|
321
|
+
const lastWeekStart = new Date(weekStart);
|
|
322
|
+
lastWeekStart.setDate(lastWeekStart.getDate() - 7);
|
|
323
|
+
const completedThisWeekR = await db2.query(
|
|
324
|
+
`SELECT COUNT(*) as count FROM tickets WHERE completed_at >= $1`,
|
|
325
|
+
[weekStart.toISOString()]
|
|
326
|
+
);
|
|
327
|
+
const completedLastWeekR = await db2.query(
|
|
328
|
+
`SELECT COUNT(*) as count FROM tickets WHERE completed_at >= $1 AND completed_at < $2`,
|
|
329
|
+
[lastWeekStart.toISOString(), weekStart.toISOString()]
|
|
330
|
+
);
|
|
331
|
+
const wipR = await db2.query(
|
|
332
|
+
`SELECT COUNT(*) as count FROM tickets WHERE status = 'started'`
|
|
333
|
+
);
|
|
334
|
+
const createdThisWeekR = await db2.query(
|
|
335
|
+
`SELECT COUNT(*) as count FROM tickets WHERE created_at >= $1`,
|
|
336
|
+
[weekStart.toISOString()]
|
|
337
|
+
);
|
|
338
|
+
const cycleDays = cycleTime.map((c) => c.days).sort((a, b) => a - b);
|
|
339
|
+
const medianCycleTimeDays = cycleDays.length > 0 ? cycleDays[Math.floor(cycleDays.length / 2)] : null;
|
|
340
|
+
return {
|
|
341
|
+
throughput,
|
|
342
|
+
cycleTime,
|
|
343
|
+
categoryBreakdown,
|
|
344
|
+
categoryPeriod,
|
|
345
|
+
kpi: {
|
|
346
|
+
completedThisWeek: parseInt(completedThisWeekR.rows[0].count, 10),
|
|
347
|
+
completedLastWeek: parseInt(completedLastWeekR.rows[0].count, 10),
|
|
348
|
+
wipCount: parseInt(wipR.rows[0].count, 10),
|
|
349
|
+
createdThisWeek: parseInt(createdThisWeekR.rows[0].count, 10),
|
|
350
|
+
medianCycleTimeDays
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
var init_stats = __esm({
|
|
355
|
+
"src/db/stats.ts"() {
|
|
356
|
+
"use strict";
|
|
357
|
+
init_connection();
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
159
361
|
// src/gitignore.ts
|
|
160
362
|
var gitignore_exports = {};
|
|
161
363
|
__export(gitignore_exports, {
|
|
@@ -218,10 +420,111 @@ var init_gitignore = __esm({
|
|
|
218
420
|
}
|
|
219
421
|
});
|
|
220
422
|
|
|
423
|
+
// src/channel-config.ts
|
|
424
|
+
var channel_config_exports = {};
|
|
425
|
+
__export(channel_config_exports, {
|
|
426
|
+
getChannelPort: () => getChannelPort,
|
|
427
|
+
isChannelAlive: () => isChannelAlive,
|
|
428
|
+
registerChannel: () => registerChannel,
|
|
429
|
+
triggerChannel: () => triggerChannel,
|
|
430
|
+
unregisterChannel: () => unregisterChannel
|
|
431
|
+
});
|
|
432
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
433
|
+
import { join as join8, resolve } from "path";
|
|
434
|
+
function getChannelServerPath() {
|
|
435
|
+
const cwd = process.cwd();
|
|
436
|
+
const distPath = resolve(cwd, "dist", "channel.js");
|
|
437
|
+
if (existsSync6(distPath)) {
|
|
438
|
+
return { command: "node", args: [distPath] };
|
|
439
|
+
}
|
|
440
|
+
const srcPath = resolve(cwd, "src", "channel.ts");
|
|
441
|
+
if (existsSync6(srcPath)) {
|
|
442
|
+
return { command: "npx", args: ["tsx", srcPath] };
|
|
443
|
+
}
|
|
444
|
+
return { command: "node", args: [distPath] };
|
|
445
|
+
}
|
|
446
|
+
function registerChannel(dataDir2) {
|
|
447
|
+
const cwd = process.cwd();
|
|
448
|
+
const mcpPath = join8(cwd, ".mcp.json");
|
|
449
|
+
const { command, args } = getChannelServerPath();
|
|
450
|
+
let config = {};
|
|
451
|
+
if (existsSync6(mcpPath)) {
|
|
452
|
+
try {
|
|
453
|
+
config = JSON.parse(readFileSync6(mcpPath, "utf-8"));
|
|
454
|
+
} catch {
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
458
|
+
config.mcpServers[MCP_SERVER_KEY] = {
|
|
459
|
+
command,
|
|
460
|
+
args: [...args, "--data-dir", dataDir2]
|
|
461
|
+
};
|
|
462
|
+
writeFileSync6(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
463
|
+
}
|
|
464
|
+
function unregisterChannel() {
|
|
465
|
+
const cwd = process.cwd();
|
|
466
|
+
const mcpPath = join8(cwd, ".mcp.json");
|
|
467
|
+
if (!existsSync6(mcpPath)) return;
|
|
468
|
+
try {
|
|
469
|
+
const config = JSON.parse(readFileSync6(mcpPath, "utf-8"));
|
|
470
|
+
if (config.mcpServers?.[MCP_SERVER_KEY]) {
|
|
471
|
+
delete config.mcpServers[MCP_SERVER_KEY];
|
|
472
|
+
writeFileSync6(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
473
|
+
}
|
|
474
|
+
} catch {
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function getChannelPort(dataDir2) {
|
|
478
|
+
try {
|
|
479
|
+
const portStr = readFileSync6(join8(dataDir2, "channel-port"), "utf-8").trim();
|
|
480
|
+
const port2 = parseInt(portStr, 10);
|
|
481
|
+
return isNaN(port2) ? null : port2;
|
|
482
|
+
} catch {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async function isChannelAlive(dataDir2) {
|
|
487
|
+
const port2 = getChannelPort(dataDir2);
|
|
488
|
+
if (!port2) return false;
|
|
489
|
+
try {
|
|
490
|
+
const res = await fetch(`http://127.0.0.1:${port2}/health`);
|
|
491
|
+
const data = await res.json();
|
|
492
|
+
return data.ok === true;
|
|
493
|
+
} catch {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
async function triggerChannel(dataDir2, serverPort, message) {
|
|
498
|
+
const port2 = getChannelPort(dataDir2);
|
|
499
|
+
if (!port2) return false;
|
|
500
|
+
const defaultMessage = [
|
|
501
|
+
"Process the Hot Sheet worklist. Run /hotsheet to work through the current Up Next items.",
|
|
502
|
+
"",
|
|
503
|
+
`When you are completely finished processing all items (or if the worklist was empty), signal completion by running:`,
|
|
504
|
+
`curl -s -X POST http://localhost:${serverPort}/api/channel/done`
|
|
505
|
+
].join("\n");
|
|
506
|
+
try {
|
|
507
|
+
const res = await fetch(`http://127.0.0.1:${port2}/trigger`, {
|
|
508
|
+
method: "POST",
|
|
509
|
+
body: message || defaultMessage
|
|
510
|
+
});
|
|
511
|
+
return res.ok;
|
|
512
|
+
} catch {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
var MCP_SERVER_KEY;
|
|
517
|
+
var init_channel_config = __esm({
|
|
518
|
+
"src/channel-config.ts"() {
|
|
519
|
+
"use strict";
|
|
520
|
+
MCP_SERVER_KEY = "hotsheet-channel";
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
221
524
|
// src/cli.ts
|
|
222
525
|
import { mkdirSync as mkdirSync6 } from "fs";
|
|
223
526
|
import { tmpdir } from "os";
|
|
224
|
-
import { join as
|
|
527
|
+
import { join as join12, resolve as resolve2 } from "path";
|
|
225
528
|
|
|
226
529
|
// src/backup.ts
|
|
227
530
|
init_connection();
|
|
@@ -491,14 +794,46 @@ var CATEGORY_DESCRIPTIONS = Object.fromEntries(
|
|
|
491
794
|
|
|
492
795
|
// src/db/queries.ts
|
|
493
796
|
init_connection();
|
|
797
|
+
var noteCounter = 0;
|
|
798
|
+
function generateNoteId() {
|
|
799
|
+
return `n_${Date.now().toString(36)}_${(noteCounter++).toString(36)}`;
|
|
800
|
+
}
|
|
494
801
|
function parseNotes(raw) {
|
|
495
802
|
if (!raw || raw === "") return [];
|
|
496
803
|
try {
|
|
497
804
|
const parsed = JSON.parse(raw);
|
|
498
|
-
if (Array.isArray(parsed))
|
|
805
|
+
if (Array.isArray(parsed)) {
|
|
806
|
+
return parsed.map((n) => ({
|
|
807
|
+
id: n.id || generateNoteId(),
|
|
808
|
+
text: n.text,
|
|
809
|
+
created_at: n.created_at
|
|
810
|
+
}));
|
|
811
|
+
}
|
|
499
812
|
} catch {
|
|
500
813
|
}
|
|
501
|
-
return [{ text: raw, created_at: (/* @__PURE__ */ new Date()).toISOString() }];
|
|
814
|
+
return [{ id: generateNoteId(), text: raw, created_at: (/* @__PURE__ */ new Date()).toISOString() }];
|
|
815
|
+
}
|
|
816
|
+
async function editNote(ticketId, noteId2, text) {
|
|
817
|
+
const db2 = await getDb();
|
|
818
|
+
const result = await db2.query(`SELECT notes FROM tickets WHERE id = $1`, [ticketId]);
|
|
819
|
+
if (result.rows.length === 0) return null;
|
|
820
|
+
const notes = parseNotes(result.rows[0].notes);
|
|
821
|
+
const note = notes.find((n) => n.id === noteId2);
|
|
822
|
+
if (!note) return null;
|
|
823
|
+
note.text = text;
|
|
824
|
+
await db2.query(`UPDATE tickets SET notes = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(notes), ticketId]);
|
|
825
|
+
return notes;
|
|
826
|
+
}
|
|
827
|
+
async function deleteNote(ticketId, noteId2) {
|
|
828
|
+
const db2 = await getDb();
|
|
829
|
+
const result = await db2.query(`SELECT notes FROM tickets WHERE id = $1`, [ticketId]);
|
|
830
|
+
if (result.rows.length === 0) return null;
|
|
831
|
+
const notes = parseNotes(result.rows[0].notes);
|
|
832
|
+
const idx = notes.findIndex((n) => n.id === noteId2);
|
|
833
|
+
if (idx === -1) return null;
|
|
834
|
+
notes.splice(idx, 1);
|
|
835
|
+
await db2.query(`UPDATE tickets SET notes = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(notes), ticketId]);
|
|
836
|
+
return notes;
|
|
502
837
|
}
|
|
503
838
|
async function nextTicketNumber() {
|
|
504
839
|
const db2 = await getDb();
|
|
@@ -557,7 +892,7 @@ async function updateTicket(id, updates) {
|
|
|
557
892
|
if (updates.notes !== void 0 && updates.notes !== "") {
|
|
558
893
|
const current = await db2.query(`SELECT notes FROM tickets WHERE id = $1`, [id]);
|
|
559
894
|
const existing = parseNotes(current.rows[0]?.notes || "");
|
|
560
|
-
existing.push({ text: updates.notes, created_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
895
|
+
existing.push({ id: generateNoteId(), text: updates.notes, created_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
561
896
|
sets.push(`notes = $${paramIdx}`);
|
|
562
897
|
values.push(JSON.stringify(existing));
|
|
563
898
|
paramIdx++;
|
|
@@ -574,8 +909,6 @@ async function updateTicket(id, updates) {
|
|
|
574
909
|
sets.push("deleted_at = NOW()");
|
|
575
910
|
} else if (updates.status === "backlog" || updates.status === "archive") {
|
|
576
911
|
sets.push("up_next = FALSE");
|
|
577
|
-
sets.push("completed_at = NULL");
|
|
578
|
-
sets.push("verified_at = NULL");
|
|
579
912
|
sets.push("deleted_at = NULL");
|
|
580
913
|
} else if (updates.status === "not_started" || updates.status === "started") {
|
|
581
914
|
sets.push("completed_at = NULL");
|
|
@@ -647,8 +980,8 @@ async function getTickets(filters = {}) {
|
|
|
647
980
|
break;
|
|
648
981
|
case "status":
|
|
649
982
|
orderBy = `CASE status
|
|
650
|
-
WHEN '
|
|
651
|
-
WHEN '
|
|
983
|
+
WHEN 'backlog' THEN 1 WHEN 'not_started' THEN 2 WHEN 'started' THEN 3
|
|
984
|
+
WHEN 'completed' THEN 4 WHEN 'verified' THEN 5 WHEN 'archive' THEN 6 END`;
|
|
652
985
|
break;
|
|
653
986
|
case "ticket_number":
|
|
654
987
|
orderBy = "id";
|
|
@@ -750,6 +1083,113 @@ async function getTicketsForCleanup(verifiedDays = 30, trashDays = 3) {
|
|
|
750
1083
|
`, [verifiedDays, trashDays]);
|
|
751
1084
|
return result.rows;
|
|
752
1085
|
}
|
|
1086
|
+
var QUERYABLE_FIELDS = /* @__PURE__ */ new Set(["category", "priority", "status", "title", "details", "up_next", "tags"]);
|
|
1087
|
+
var PRIORITY_ORD = `CASE priority WHEN 'highest' THEN 1 WHEN 'high' THEN 2 WHEN 'default' THEN 3 WHEN 'low' THEN 4 WHEN 'lowest' THEN 5 ELSE 3 END`;
|
|
1088
|
+
var STATUS_ORD = `CASE status WHEN 'backlog' THEN 1 WHEN 'not_started' THEN 2 WHEN 'started' THEN 3 WHEN 'completed' THEN 4 WHEN 'verified' THEN 5 WHEN 'archive' THEN 6 ELSE 2 END`;
|
|
1089
|
+
var PRIORITY_RANK = { highest: 1, high: 2, default: 3, low: 4, lowest: 5 };
|
|
1090
|
+
var STATUS_RANK = { backlog: 1, not_started: 2, started: 3, completed: 4, verified: 5, archive: 6 };
|
|
1091
|
+
function ordinalExpr(field) {
|
|
1092
|
+
if (field === "priority") return PRIORITY_ORD;
|
|
1093
|
+
if (field === "status") return STATUS_ORD;
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
function ordinalValue(field, value) {
|
|
1097
|
+
if (field === "priority") return PRIORITY_RANK[value] ?? null;
|
|
1098
|
+
if (field === "status") return STATUS_RANK[value] ?? null;
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
async function queryTickets(logic, conditions, sortBy, sortDir) {
|
|
1102
|
+
const db2 = await getDb();
|
|
1103
|
+
const where = [];
|
|
1104
|
+
const values = [];
|
|
1105
|
+
let paramIdx = 1;
|
|
1106
|
+
where.push(`status != 'deleted'`);
|
|
1107
|
+
for (const cond of conditions) {
|
|
1108
|
+
if (!QUERYABLE_FIELDS.has(cond.field)) continue;
|
|
1109
|
+
const field = cond.field;
|
|
1110
|
+
if (field === "up_next") {
|
|
1111
|
+
where.push(`up_next = $${paramIdx}`);
|
|
1112
|
+
values.push(cond.value === "true");
|
|
1113
|
+
paramIdx++;
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
const ordExpr = ordinalExpr(field);
|
|
1117
|
+
const ordVal = ordExpr ? ordinalValue(field, cond.value) : null;
|
|
1118
|
+
if (ordExpr && ordVal !== null && ["lt", "lte", "gt", "gte"].includes(cond.operator)) {
|
|
1119
|
+
const op = cond.operator === "lt" ? "<" : cond.operator === "lte" ? "<=" : cond.operator === "gt" ? ">" : ">=";
|
|
1120
|
+
where.push(`(${ordExpr}) ${op} $${paramIdx}`);
|
|
1121
|
+
values.push(ordVal);
|
|
1122
|
+
paramIdx++;
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
switch (cond.operator) {
|
|
1126
|
+
case "equals":
|
|
1127
|
+
where.push(`${field} = $${paramIdx}`);
|
|
1128
|
+
values.push(cond.value);
|
|
1129
|
+
paramIdx++;
|
|
1130
|
+
break;
|
|
1131
|
+
case "not_equals":
|
|
1132
|
+
where.push(`${field} != $${paramIdx}`);
|
|
1133
|
+
values.push(cond.value);
|
|
1134
|
+
paramIdx++;
|
|
1135
|
+
break;
|
|
1136
|
+
case "contains":
|
|
1137
|
+
where.push(`${field} ILIKE $${paramIdx}`);
|
|
1138
|
+
values.push(`%${cond.value}%`);
|
|
1139
|
+
paramIdx++;
|
|
1140
|
+
break;
|
|
1141
|
+
case "not_contains":
|
|
1142
|
+
where.push(`${field} NOT ILIKE $${paramIdx}`);
|
|
1143
|
+
values.push(`%${cond.value}%`);
|
|
1144
|
+
paramIdx++;
|
|
1145
|
+
break;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
const joiner = logic === "any" ? " OR " : " AND ";
|
|
1149
|
+
const userConditions = where.slice(1);
|
|
1150
|
+
let whereClause = where[0];
|
|
1151
|
+
if (userConditions.length > 0) {
|
|
1152
|
+
whereClause += ` AND (${userConditions.join(joiner)})`;
|
|
1153
|
+
}
|
|
1154
|
+
let orderBy;
|
|
1155
|
+
switch (sortBy) {
|
|
1156
|
+
case "priority":
|
|
1157
|
+
orderBy = `CASE priority WHEN 'highest' THEN 1 WHEN 'high' THEN 2 WHEN 'default' THEN 3 WHEN 'low' THEN 4 WHEN 'lowest' THEN 5 END`;
|
|
1158
|
+
break;
|
|
1159
|
+
case "category":
|
|
1160
|
+
orderBy = "category";
|
|
1161
|
+
break;
|
|
1162
|
+
case "status":
|
|
1163
|
+
orderBy = `CASE status WHEN 'backlog' THEN 1 WHEN 'not_started' THEN 2 WHEN 'started' THEN 3 WHEN 'completed' THEN 4 WHEN 'verified' THEN 5 WHEN 'archive' THEN 6 END`;
|
|
1164
|
+
break;
|
|
1165
|
+
default:
|
|
1166
|
+
orderBy = "created_at";
|
|
1167
|
+
break;
|
|
1168
|
+
}
|
|
1169
|
+
const dir = sortDir === "asc" ? "ASC" : "DESC";
|
|
1170
|
+
const result = await db2.query(
|
|
1171
|
+
`SELECT * FROM tickets WHERE ${whereClause} ORDER BY ${orderBy} ${dir}, id DESC`,
|
|
1172
|
+
values
|
|
1173
|
+
);
|
|
1174
|
+
return result.rows;
|
|
1175
|
+
}
|
|
1176
|
+
async function getAllTags() {
|
|
1177
|
+
const db2 = await getDb();
|
|
1178
|
+
const result = await db2.query(`SELECT DISTINCT tags FROM tickets WHERE tags != '[]' AND status != 'deleted'`);
|
|
1179
|
+
const tagSet = /* @__PURE__ */ new Set();
|
|
1180
|
+
for (const row of result.rows) {
|
|
1181
|
+
try {
|
|
1182
|
+
const parsed = JSON.parse(row.tags);
|
|
1183
|
+
if (Array.isArray(parsed)) {
|
|
1184
|
+
for (const tag of parsed) {
|
|
1185
|
+
if (typeof tag === "string" && tag.trim()) tagSet.add(tag.trim());
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
} catch {
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return Array.from(tagSet).sort();
|
|
1192
|
+
}
|
|
753
1193
|
async function getCategories() {
|
|
754
1194
|
const settings = await getSettings();
|
|
755
1195
|
if (settings.categories) {
|
|
@@ -915,20 +1355,26 @@ init_connection();
|
|
|
915
1355
|
var DEMO_SCENARIOS = [
|
|
916
1356
|
{ id: 1, label: "Main UI \u2014 all tickets with detail panel" },
|
|
917
1357
|
{ id: 2, label: "Quick entry \u2014 bullet-list ticket creation" },
|
|
918
|
-
{ id: 3, label: "Sidebar filtering \u2014
|
|
1358
|
+
{ id: 3, label: "Sidebar filtering \u2014 custom views and categories" },
|
|
919
1359
|
{ id: 4, label: "AI worklist \u2014 Up Next tickets with notes" },
|
|
920
1360
|
{ id: 5, label: "Batch operations \u2014 multi-select toolbar" },
|
|
921
|
-
{ id: 6, label: "Detail panel \u2014 bottom orientation with notes" },
|
|
922
|
-
{ id: 7, label: "Column view \u2014 kanban board by status" }
|
|
1361
|
+
{ id: 6, label: "Detail panel \u2014 bottom orientation with tags and notes" },
|
|
1362
|
+
{ id: 7, label: "Column view \u2014 kanban board by status" },
|
|
1363
|
+
{ id: 8, label: "Dashboard \u2014 stats and charts" }
|
|
923
1364
|
];
|
|
924
1365
|
function daysAgo(days) {
|
|
925
1366
|
const d = /* @__PURE__ */ new Date();
|
|
926
1367
|
d.setTime(d.getTime() - days * 24 * 60 * 60 * 1e3);
|
|
927
1368
|
return d.toISOString();
|
|
928
1369
|
}
|
|
1370
|
+
var noteId = 0;
|
|
929
1371
|
function notesJson(entries) {
|
|
930
1372
|
if (entries.length === 0) return "";
|
|
931
|
-
return JSON.stringify(entries.map((e) => ({
|
|
1373
|
+
return JSON.stringify(entries.map((e) => ({
|
|
1374
|
+
id: `n_demo_${noteId++}`,
|
|
1375
|
+
text: e.text,
|
|
1376
|
+
created_at: daysAgo(e.days_ago)
|
|
1377
|
+
})));
|
|
932
1378
|
}
|
|
933
1379
|
var SCENARIO_1 = [
|
|
934
1380
|
{
|
|
@@ -938,6 +1384,7 @@ var SCENARIO_1 = [
|
|
|
938
1384
|
priority: "highest",
|
|
939
1385
|
status: "started",
|
|
940
1386
|
up_next: true,
|
|
1387
|
+
tags: ["checkout", "shipping"],
|
|
941
1388
|
notes: notesJson([{ text: "Confirmed the issue is in ShippingCalculator.consolidate(). It uses a single rate lookup instead of per-item calculation. Working on a fix that groups items by shipping method and merges the rates.", days_ago: 0.5 }]),
|
|
942
1389
|
days_ago: 5,
|
|
943
1390
|
updated_ago: 0.5
|
|
@@ -949,6 +1396,7 @@ var SCENARIO_1 = [
|
|
|
949
1396
|
priority: "high",
|
|
950
1397
|
status: "not_started",
|
|
951
1398
|
up_next: true,
|
|
1399
|
+
tags: ["ux", "product-pages"],
|
|
952
1400
|
notes: "",
|
|
953
1401
|
days_ago: 4,
|
|
954
1402
|
updated_ago: 4
|
|
@@ -960,6 +1408,7 @@ var SCENARIO_1 = [
|
|
|
960
1408
|
priority: "high",
|
|
961
1409
|
status: "started",
|
|
962
1410
|
up_next: true,
|
|
1411
|
+
tags: ["infrastructure", "devops"],
|
|
963
1412
|
notes: notesJson([{ text: "Created the backup script and IAM role. Testing the S3 lifecycle policy for retention.", days_ago: 1 }]),
|
|
964
1413
|
days_ago: 7,
|
|
965
1414
|
updated_ago: 1
|
|
@@ -971,6 +1420,7 @@ var SCENARIO_1 = [
|
|
|
971
1420
|
priority: "high",
|
|
972
1421
|
status: "not_started",
|
|
973
1422
|
up_next: true,
|
|
1423
|
+
tags: ["payments"],
|
|
974
1424
|
notes: "",
|
|
975
1425
|
days_ago: 3,
|
|
976
1426
|
updated_ago: 3
|
|
@@ -982,6 +1432,7 @@ var SCENARIO_1 = [
|
|
|
982
1432
|
priority: "default",
|
|
983
1433
|
status: "not_started",
|
|
984
1434
|
up_next: false,
|
|
1435
|
+
tags: ["performance", "images"],
|
|
985
1436
|
notes: "",
|
|
986
1437
|
days_ago: 6,
|
|
987
1438
|
updated_ago: 6
|
|
@@ -993,6 +1444,7 @@ var SCENARIO_1 = [
|
|
|
993
1444
|
priority: "default",
|
|
994
1445
|
status: "not_started",
|
|
995
1446
|
up_next: false,
|
|
1447
|
+
tags: ["checkout", "accounts"],
|
|
996
1448
|
notes: "",
|
|
997
1449
|
days_ago: 10,
|
|
998
1450
|
updated_ago: 10
|
|
@@ -1004,6 +1456,7 @@ var SCENARIO_1 = [
|
|
|
1004
1456
|
priority: "default",
|
|
1005
1457
|
status: "started",
|
|
1006
1458
|
up_next: false,
|
|
1459
|
+
tags: ["tax", "eu", "compliance"],
|
|
1007
1460
|
notes: "",
|
|
1008
1461
|
days_ago: 8,
|
|
1009
1462
|
updated_ago: 2
|
|
@@ -1015,6 +1468,7 @@ var SCENARIO_1 = [
|
|
|
1015
1468
|
priority: "default",
|
|
1016
1469
|
status: "completed",
|
|
1017
1470
|
up_next: false,
|
|
1471
|
+
tags: ["docs", "api"],
|
|
1018
1472
|
notes: notesJson([{ text: "Documented all 12 order endpoints with examples. Published to /docs.", days_ago: 1 }]),
|
|
1019
1473
|
days_ago: 12,
|
|
1020
1474
|
updated_ago: 1,
|
|
@@ -1027,6 +1481,7 @@ var SCENARIO_1 = [
|
|
|
1027
1481
|
priority: "highest",
|
|
1028
1482
|
status: "verified",
|
|
1029
1483
|
up_next: false,
|
|
1484
|
+
tags: ["mobile", "api"],
|
|
1030
1485
|
notes: notesJson([
|
|
1031
1486
|
{ text: "Added CORS middleware with correct origins. Tested against staging with the mobile app builds.", days_ago: 3 },
|
|
1032
1487
|
{ text: "Verified fix is working in production. No more CORS errors in mobile app error logs.", days_ago: 2 }
|
|
@@ -1043,6 +1498,7 @@ var SCENARIO_1 = [
|
|
|
1043
1498
|
priority: "low",
|
|
1044
1499
|
status: "not_started",
|
|
1045
1500
|
up_next: false,
|
|
1501
|
+
tags: ["ux", "theming"],
|
|
1046
1502
|
notes: "",
|
|
1047
1503
|
days_ago: 15,
|
|
1048
1504
|
updated_ago: 15
|
|
@@ -1054,6 +1510,7 @@ var SCENARIO_1 = [
|
|
|
1054
1510
|
priority: "default",
|
|
1055
1511
|
status: "completed",
|
|
1056
1512
|
up_next: false,
|
|
1513
|
+
tags: ["infrastructure", "database"],
|
|
1057
1514
|
notes: notesJson([{ text: "Migrated to pg pool with max 20 connections. Load tested successfully at 500 concurrent requests.", days_ago: 4 }]),
|
|
1058
1515
|
days_ago: 18,
|
|
1059
1516
|
updated_ago: 4,
|
|
@@ -1066,6 +1523,7 @@ var SCENARIO_1 = [
|
|
|
1066
1523
|
priority: "lowest",
|
|
1067
1524
|
status: "not_started",
|
|
1068
1525
|
up_next: false,
|
|
1526
|
+
tags: ["frontend", "seo"],
|
|
1069
1527
|
notes: "",
|
|
1070
1528
|
days_ago: 20,
|
|
1071
1529
|
updated_ago: 20
|
|
@@ -1079,6 +1537,7 @@ var SCENARIO_2 = [
|
|
|
1079
1537
|
priority: "high",
|
|
1080
1538
|
status: "not_started",
|
|
1081
1539
|
up_next: true,
|
|
1540
|
+
tags: ["auth"],
|
|
1082
1541
|
notes: "",
|
|
1083
1542
|
days_ago: 2,
|
|
1084
1543
|
updated_ago: 2
|
|
@@ -1090,6 +1549,7 @@ var SCENARIO_2 = [
|
|
|
1090
1549
|
priority: "default",
|
|
1091
1550
|
status: "not_started",
|
|
1092
1551
|
up_next: false,
|
|
1552
|
+
tags: ["admin"],
|
|
1093
1553
|
notes: "",
|
|
1094
1554
|
days_ago: 3,
|
|
1095
1555
|
updated_ago: 3
|
|
@@ -1101,6 +1561,7 @@ var SCENARIO_2 = [
|
|
|
1101
1561
|
priority: "default",
|
|
1102
1562
|
status: "started",
|
|
1103
1563
|
up_next: false,
|
|
1564
|
+
tags: ["maintenance"],
|
|
1104
1565
|
notes: "",
|
|
1105
1566
|
days_ago: 1,
|
|
1106
1567
|
updated_ago: 0.5
|
|
@@ -1114,17 +1575,19 @@ var SCENARIO_3 = [
|
|
|
1114
1575
|
priority: "highest",
|
|
1115
1576
|
status: "started",
|
|
1116
1577
|
up_next: true,
|
|
1578
|
+
tags: ["checkout", "pricing"],
|
|
1117
1579
|
notes: "",
|
|
1118
1580
|
days_ago: 3,
|
|
1119
1581
|
updated_ago: 1
|
|
1120
1582
|
},
|
|
1121
1583
|
{
|
|
1122
1584
|
title: "Search returns stale results after product update",
|
|
1123
|
-
details: "The search index isn't being refreshed when product details change.
|
|
1585
|
+
details: "The search index isn't being refreshed when product details change.",
|
|
1124
1586
|
category: "bug",
|
|
1125
1587
|
priority: "high",
|
|
1126
1588
|
status: "not_started",
|
|
1127
1589
|
up_next: true,
|
|
1590
|
+
tags: ["search"],
|
|
1128
1591
|
notes: "",
|
|
1129
1592
|
days_ago: 5,
|
|
1130
1593
|
updated_ago: 5
|
|
@@ -1136,17 +1599,19 @@ var SCENARIO_3 = [
|
|
|
1136
1599
|
priority: "default",
|
|
1137
1600
|
status: "not_started",
|
|
1138
1601
|
up_next: false,
|
|
1602
|
+
tags: ["notifications"],
|
|
1139
1603
|
notes: "",
|
|
1140
1604
|
days_ago: 7,
|
|
1141
1605
|
updated_ago: 7
|
|
1142
1606
|
},
|
|
1143
1607
|
{
|
|
1144
1608
|
title: "Implement real-time inventory tracking",
|
|
1145
|
-
details:
|
|
1609
|
+
details: "Use WebSocket connections to push stock level changes to the product page.",
|
|
1146
1610
|
category: "feature",
|
|
1147
1611
|
priority: "high",
|
|
1148
1612
|
status: "started",
|
|
1149
1613
|
up_next: true,
|
|
1614
|
+
tags: ["real-time", "inventory"],
|
|
1150
1615
|
notes: notesJson([{ text: "WebSocket server is set up. Working on the client-side stock badge component.", days_ago: 0.5 }]),
|
|
1151
1616
|
days_ago: 6,
|
|
1152
1617
|
updated_ago: 0.5
|
|
@@ -1158,84 +1623,67 @@ var SCENARIO_3 = [
|
|
|
1158
1623
|
priority: "default",
|
|
1159
1624
|
status: "not_started",
|
|
1160
1625
|
up_next: false,
|
|
1626
|
+
tags: ["social"],
|
|
1161
1627
|
notes: "",
|
|
1162
1628
|
days_ago: 9,
|
|
1163
1629
|
updated_ago: 9
|
|
1164
1630
|
},
|
|
1165
1631
|
{
|
|
1166
1632
|
title: "Product video support on detail pages",
|
|
1167
|
-
details: "Allow merchants to upload product videos alongside photos.
|
|
1633
|
+
details: "Allow merchants to upload product videos alongside photos.",
|
|
1168
1634
|
category: "feature",
|
|
1169
1635
|
priority: "low",
|
|
1170
1636
|
status: "not_started",
|
|
1171
1637
|
up_next: false,
|
|
1638
|
+
tags: ["media"],
|
|
1172
1639
|
notes: "",
|
|
1173
1640
|
days_ago: 12,
|
|
1174
1641
|
updated_ago: 12
|
|
1175
1642
|
},
|
|
1176
1643
|
{
|
|
1177
1644
|
title: "Migrate image storage to CDN",
|
|
1178
|
-
details: "Move product images from local disk to CloudFront.
|
|
1645
|
+
details: "Move product images from local disk to CloudFront.",
|
|
1179
1646
|
category: "task",
|
|
1180
1647
|
priority: "high",
|
|
1181
1648
|
status: "started",
|
|
1182
1649
|
up_next: false,
|
|
1650
|
+
tags: ["infrastructure", "images"],
|
|
1183
1651
|
notes: "",
|
|
1184
1652
|
days_ago: 4,
|
|
1185
1653
|
updated_ago: 2
|
|
1186
1654
|
},
|
|
1187
|
-
{
|
|
1188
|
-
title: "Set up error monitoring with Sentry",
|
|
1189
|
-
details: "Configure Sentry for both server and client-side error tracking. Set up alert rules for critical errors.",
|
|
1190
|
-
category: "task",
|
|
1191
|
-
priority: "default",
|
|
1192
|
-
status: "completed",
|
|
1193
|
-
up_next: false,
|
|
1194
|
-
notes: notesJson([{ text: "Sentry configured for Node.js backend and React frontend. Alert rules set for 5xx errors.", days_ago: 3 }]),
|
|
1195
|
-
days_ago: 10,
|
|
1196
|
-
updated_ago: 3,
|
|
1197
|
-
completed_ago: 3
|
|
1198
|
-
},
|
|
1199
1655
|
{
|
|
1200
1656
|
title: "Support guest checkout without account creation",
|
|
1201
|
-
details: "
|
|
1657
|
+
details: "Many users abandon at the registration step. Allow checkout with just email.",
|
|
1202
1658
|
category: "requirement_change",
|
|
1203
1659
|
priority: "high",
|
|
1204
1660
|
status: "not_started",
|
|
1205
1661
|
up_next: true,
|
|
1662
|
+
tags: ["checkout", "conversion"],
|
|
1206
1663
|
notes: "",
|
|
1207
1664
|
days_ago: 2,
|
|
1208
1665
|
updated_ago: 2
|
|
1209
1666
|
},
|
|
1210
|
-
{
|
|
1211
|
-
title: "Update return policy to 60-day window",
|
|
1212
|
-
details: "Legal team requires extending the return window from 30 to 60 days. Update all customer-facing copy and the returns API logic.",
|
|
1213
|
-
category: "requirement_change",
|
|
1214
|
-
priority: "default",
|
|
1215
|
-
status: "started",
|
|
1216
|
-
up_next: false,
|
|
1217
|
-
notes: "",
|
|
1218
|
-
days_ago: 8,
|
|
1219
|
-
updated_ago: 3
|
|
1220
|
-
},
|
|
1221
1667
|
{
|
|
1222
1668
|
title: "Compare Redis vs Memcached for session storage",
|
|
1223
|
-
details: "
|
|
1669
|
+
details: "Evaluate Redis and Memcached for persistence, speed, and ops complexity.",
|
|
1224
1670
|
category: "investigation",
|
|
1225
1671
|
priority: "high",
|
|
1226
1672
|
status: "not_started",
|
|
1227
1673
|
up_next: false,
|
|
1674
|
+
tags: ["infrastructure"],
|
|
1228
1675
|
notes: "",
|
|
1229
1676
|
days_ago: 6,
|
|
1230
1677
|
updated_ago: 6
|
|
1231
1678
|
},
|
|
1232
1679
|
{
|
|
1233
1680
|
title: "Analyze mobile conversion drop-off funnel",
|
|
1234
|
-
details: "Mobile users convert at 1.2% vs 3.8% desktop. Investigate where
|
|
1681
|
+
details: "Mobile users convert at 1.2% vs 3.8% desktop. Investigate where users drop off.",
|
|
1235
1682
|
category: "investigation",
|
|
1236
1683
|
priority: "default",
|
|
1237
1684
|
status: "not_started",
|
|
1238
1685
|
up_next: false,
|
|
1686
|
+
tags: ["analytics", "mobile"],
|
|
1239
1687
|
notes: "",
|
|
1240
1688
|
days_ago: 11,
|
|
1241
1689
|
updated_ago: 11
|
|
@@ -1249,6 +1697,7 @@ var SCENARIO_4 = [
|
|
|
1249
1697
|
priority: "highest",
|
|
1250
1698
|
status: "started",
|
|
1251
1699
|
up_next: true,
|
|
1700
|
+
tags: ["concurrency", "orders"],
|
|
1252
1701
|
notes: notesJson([
|
|
1253
1702
|
{ text: "Reproduced the issue with a concurrent request test. The problem is in OrderService.place() \u2014 it reads inventory, then decrements in a separate query without locking.", days_ago: 1 },
|
|
1254
1703
|
{ text: "Implemented SELECT ... FOR UPDATE on the inventory row. Running stress tests to confirm the fix holds under load.", days_ago: 0.3 }
|
|
@@ -1263,6 +1712,7 @@ var SCENARIO_4 = [
|
|
|
1263
1712
|
priority: "high",
|
|
1264
1713
|
status: "not_started",
|
|
1265
1714
|
up_next: true,
|
|
1715
|
+
tags: ["webhooks", "api"],
|
|
1266
1716
|
notes: "",
|
|
1267
1717
|
days_ago: 3,
|
|
1268
1718
|
updated_ago: 3
|
|
@@ -1274,6 +1724,7 @@ var SCENARIO_4 = [
|
|
|
1274
1724
|
priority: "high",
|
|
1275
1725
|
status: "not_started",
|
|
1276
1726
|
up_next: true,
|
|
1727
|
+
tags: ["security", "api"],
|
|
1277
1728
|
notes: "",
|
|
1278
1729
|
days_ago: 5,
|
|
1279
1730
|
updated_ago: 5
|
|
@@ -1285,31 +1736,34 @@ var SCENARIO_4 = [
|
|
|
1285
1736
|
priority: "default",
|
|
1286
1737
|
status: "not_started",
|
|
1287
1738
|
up_next: true,
|
|
1739
|
+
tags: ["pricing"],
|
|
1288
1740
|
notes: "",
|
|
1289
1741
|
days_ago: 6,
|
|
1290
1742
|
updated_ago: 6
|
|
1291
1743
|
},
|
|
1292
1744
|
{
|
|
1293
1745
|
title: "Evaluate caching strategies for product catalog",
|
|
1294
|
-
details: "Product pages are slow under load. Investigate Redis caching, CDN edge caching, and stale-while-revalidate patterns.
|
|
1746
|
+
details: "Product pages are slow under load. Investigate Redis caching, CDN edge caching, and stale-while-revalidate patterns.",
|
|
1295
1747
|
category: "investigation",
|
|
1296
1748
|
priority: "default",
|
|
1297
1749
|
status: "not_started",
|
|
1298
1750
|
up_next: true,
|
|
1751
|
+
tags: ["performance", "caching"],
|
|
1299
1752
|
notes: "",
|
|
1300
1753
|
days_ago: 7,
|
|
1301
1754
|
updated_ago: 7
|
|
1302
1755
|
},
|
|
1303
1756
|
{
|
|
1304
1757
|
title: "Add bulk product import from CSV",
|
|
1305
|
-
details: "Merchants need to upload a CSV of products to create/update inventory in batch.
|
|
1758
|
+
details: "Merchants need to upload a CSV of products to create/update inventory in batch.",
|
|
1306
1759
|
category: "feature",
|
|
1307
1760
|
priority: "low",
|
|
1308
1761
|
status: "completed",
|
|
1309
1762
|
up_next: false,
|
|
1763
|
+
tags: ["admin", "import"],
|
|
1310
1764
|
notes: notesJson([
|
|
1311
1765
|
{ text: "Implemented CSV parser using papaparse. Supports create and update modes with duplicate detection by SKU.", days_ago: 3 },
|
|
1312
|
-
{ text: "Added validation for required fields (name, price, SKU) and friendly error messages with row numbers
|
|
1766
|
+
{ text: "Added validation for required fields (name, price, SKU) and friendly error messages with row numbers.", days_ago: 2 }
|
|
1313
1767
|
]),
|
|
1314
1768
|
days_ago: 10,
|
|
1315
1769
|
updated_ago: 2,
|
|
@@ -1317,14 +1771,15 @@ var SCENARIO_4 = [
|
|
|
1317
1771
|
},
|
|
1318
1772
|
{
|
|
1319
1773
|
title: "Normalize database schema for customer addresses",
|
|
1320
|
-
details: "Addresses are currently embedded as JSON in the customers table. Extract to a separate addresses table
|
|
1774
|
+
details: "Addresses are currently embedded as JSON in the customers table. Extract to a separate addresses table.",
|
|
1321
1775
|
category: "task",
|
|
1322
1776
|
priority: "default",
|
|
1323
1777
|
status: "verified",
|
|
1324
1778
|
up_next: false,
|
|
1779
|
+
tags: ["database", "schema"],
|
|
1325
1780
|
notes: notesJson([
|
|
1326
1781
|
{ text: "Created migration to extract addresses into a new table. Backfilled 12,400 existing address records.", days_ago: 5 },
|
|
1327
|
-
{ text: "Verified the migration ran correctly. All address lookups use the new table.
|
|
1782
|
+
{ text: "Verified the migration ran correctly. All address lookups use the new table.", days_ago: 3 }
|
|
1328
1783
|
]),
|
|
1329
1784
|
days_ago: 14,
|
|
1330
1785
|
updated_ago: 3,
|
|
@@ -1335,55 +1790,60 @@ var SCENARIO_4 = [
|
|
|
1335
1790
|
var SCENARIO_5 = [
|
|
1336
1791
|
{
|
|
1337
1792
|
title: "Fix email template rendering in Outlook",
|
|
1338
|
-
details: "Order confirmation emails break in Outlook due to unsupported CSS flexbox.
|
|
1793
|
+
details: "Order confirmation emails break in Outlook due to unsupported CSS flexbox.",
|
|
1339
1794
|
category: "bug",
|
|
1340
1795
|
priority: "default",
|
|
1341
1796
|
status: "not_started",
|
|
1342
1797
|
up_next: false,
|
|
1798
|
+
tags: ["email"],
|
|
1343
1799
|
notes: "",
|
|
1344
1800
|
days_ago: 3,
|
|
1345
1801
|
updated_ago: 3
|
|
1346
1802
|
},
|
|
1347
1803
|
{
|
|
1348
1804
|
title: "Handle timeout on third-party shipping rate API",
|
|
1349
|
-
details: "When the shipping provider API times out,
|
|
1805
|
+
details: "When the shipping provider API times out, show a retry prompt instead of a 500 error.",
|
|
1350
1806
|
category: "bug",
|
|
1351
1807
|
priority: "default",
|
|
1352
1808
|
status: "not_started",
|
|
1353
1809
|
up_next: false,
|
|
1810
|
+
tags: ["shipping", "error-handling"],
|
|
1354
1811
|
notes: "",
|
|
1355
1812
|
days_ago: 4,
|
|
1356
1813
|
updated_ago: 4
|
|
1357
1814
|
},
|
|
1358
1815
|
{
|
|
1359
1816
|
title: "Fix pagination on search results page",
|
|
1360
|
-
details: "Page 2+ of search results shows duplicate items.
|
|
1817
|
+
details: "Page 2+ of search results shows duplicate items.",
|
|
1361
1818
|
category: "bug",
|
|
1362
1819
|
priority: "default",
|
|
1363
1820
|
status: "not_started",
|
|
1364
1821
|
up_next: false,
|
|
1822
|
+
tags: ["search"],
|
|
1365
1823
|
notes: "",
|
|
1366
1824
|
days_ago: 5,
|
|
1367
1825
|
updated_ago: 5
|
|
1368
1826
|
},
|
|
1369
1827
|
{
|
|
1370
1828
|
title: "Cart badge count not updating after item removal",
|
|
1371
|
-
details: "The header cart icon shows the old count until a full page refresh.
|
|
1829
|
+
details: "The header cart icon shows the old count until a full page refresh.",
|
|
1372
1830
|
category: "bug",
|
|
1373
1831
|
priority: "high",
|
|
1374
1832
|
status: "not_started",
|
|
1375
1833
|
up_next: false,
|
|
1834
|
+
tags: ["cart", "ui"],
|
|
1376
1835
|
notes: "",
|
|
1377
1836
|
days_ago: 2,
|
|
1378
1837
|
updated_ago: 2
|
|
1379
1838
|
},
|
|
1380
1839
|
{
|
|
1381
1840
|
title: "Add order tracking page for customers",
|
|
1382
|
-
details: "Customers need a page showing shipment status, tracking number, and estimated delivery.
|
|
1841
|
+
details: "Customers need a page showing shipment status, tracking number, and estimated delivery.",
|
|
1383
1842
|
category: "feature",
|
|
1384
1843
|
priority: "default",
|
|
1385
1844
|
status: "not_started",
|
|
1386
1845
|
up_next: false,
|
|
1846
|
+
tags: ["orders", "ux"],
|
|
1387
1847
|
notes: "",
|
|
1388
1848
|
days_ago: 6,
|
|
1389
1849
|
updated_ago: 6
|
|
@@ -1395,17 +1855,19 @@ var SCENARIO_5 = [
|
|
|
1395
1855
|
priority: "default",
|
|
1396
1856
|
status: "not_started",
|
|
1397
1857
|
up_next: false,
|
|
1858
|
+
tags: ["admin", "reviews"],
|
|
1398
1859
|
notes: "",
|
|
1399
1860
|
days_ago: 7,
|
|
1400
1861
|
updated_ago: 7
|
|
1401
1862
|
},
|
|
1402
1863
|
{
|
|
1403
1864
|
title: "Add rate limiting to public API endpoints",
|
|
1404
|
-
details: "Protect against abuse with per-IP rate limiting.
|
|
1865
|
+
details: "Protect against abuse with per-IP rate limiting. Target: 100 req/min anonymous, 500 authenticated.",
|
|
1405
1866
|
category: "task",
|
|
1406
1867
|
priority: "default",
|
|
1407
1868
|
status: "not_started",
|
|
1408
1869
|
up_next: false,
|
|
1870
|
+
tags: ["security", "api"],
|
|
1409
1871
|
notes: "",
|
|
1410
1872
|
days_ago: 8,
|
|
1411
1873
|
updated_ago: 8
|
|
@@ -1417,6 +1879,7 @@ var SCENARIO_5 = [
|
|
|
1417
1879
|
priority: "default",
|
|
1418
1880
|
status: "not_started",
|
|
1419
1881
|
up_next: false,
|
|
1882
|
+
tags: ["infrastructure", "devops"],
|
|
1420
1883
|
notes: "",
|
|
1421
1884
|
days_ago: 9,
|
|
1422
1885
|
updated_ago: 9
|
|
@@ -1428,6 +1891,7 @@ var SCENARIO_5 = [
|
|
|
1428
1891
|
priority: "low",
|
|
1429
1892
|
status: "not_started",
|
|
1430
1893
|
up_next: false,
|
|
1894
|
+
tags: ["cleanup"],
|
|
1431
1895
|
notes: "",
|
|
1432
1896
|
days_ago: 12,
|
|
1433
1897
|
updated_ago: 12
|
|
@@ -1439,6 +1903,7 @@ var SCENARIO_5 = [
|
|
|
1439
1903
|
priority: "low",
|
|
1440
1904
|
status: "not_started",
|
|
1441
1905
|
up_next: false,
|
|
1906
|
+
tags: ["cleanup", "database"],
|
|
1442
1907
|
notes: "",
|
|
1443
1908
|
days_ago: 14,
|
|
1444
1909
|
updated_ago: 14
|
|
@@ -1452,6 +1917,7 @@ var SCENARIO_6 = [
|
|
|
1452
1917
|
priority: "highest",
|
|
1453
1918
|
status: "started",
|
|
1454
1919
|
up_next: true,
|
|
1920
|
+
tags: ["real-time", "orders", "websocket"],
|
|
1455
1921
|
notes: notesJson([
|
|
1456
1922
|
{ text: "Set up the WebSocket server using ws library. Basic connection lifecycle working \u2014 connect, heartbeat, disconnect with cleanup.", days_ago: 3 },
|
|
1457
1923
|
{ text: "Implemented the event broadcast system. When an order status changes in the API, all connected clients for that order receive a push event. Added Redis pub/sub for multi-server support.", days_ago: 2 },
|
|
@@ -1467,6 +1933,7 @@ var SCENARIO_6 = [
|
|
|
1467
1933
|
priority: "high",
|
|
1468
1934
|
status: "started",
|
|
1469
1935
|
up_next: true,
|
|
1936
|
+
tags: ["performance", "memory", "search"],
|
|
1470
1937
|
notes: notesJson([{ text: "Heap snapshot shows the BatchProcessor holding references to completed batches. The onComplete callbacks are never cleaned up.", days_ago: 1 }]),
|
|
1471
1938
|
days_ago: 5,
|
|
1472
1939
|
updated_ago: 1
|
|
@@ -1478,17 +1945,19 @@ var SCENARIO_6 = [
|
|
|
1478
1945
|
priority: "high",
|
|
1479
1946
|
status: "not_started",
|
|
1480
1947
|
up_next: true,
|
|
1948
|
+
tags: ["testing", "payments"],
|
|
1481
1949
|
notes: "",
|
|
1482
1950
|
days_ago: 4,
|
|
1483
1951
|
updated_ago: 4
|
|
1484
1952
|
},
|
|
1485
1953
|
{
|
|
1486
1954
|
title: "Add product recommendations based on purchase history",
|
|
1487
|
-
details: 'Show "Customers also bought" recommendations on product pages using collaborative filtering
|
|
1955
|
+
details: 'Show "Customers also bought" recommendations on product pages using collaborative filtering.',
|
|
1488
1956
|
category: "feature",
|
|
1489
1957
|
priority: "default",
|
|
1490
1958
|
status: "completed",
|
|
1491
1959
|
up_next: false,
|
|
1960
|
+
tags: ["ml", "recommendations", "product-pages"],
|
|
1492
1961
|
notes: notesJson([
|
|
1493
1962
|
{ text: "Implemented a simple collaborative filtering algorithm. Computes item-item similarity from co-purchase frequency in the last 90 days.", days_ago: 5 },
|
|
1494
1963
|
{ text: "Added the recommendations API endpoint and the product page widget. Limited to 4 recommendations. Recalculation runs nightly via cron.", days_ago: 3 }
|
|
@@ -1499,14 +1968,15 @@ var SCENARIO_6 = [
|
|
|
1499
1968
|
},
|
|
1500
1969
|
{
|
|
1501
1970
|
title: "Migrate static assets to CDN",
|
|
1502
|
-
details: "Product images, CSS, and JS bundles should be served from CloudFront.
|
|
1971
|
+
details: "Product images, CSS, and JS bundles should be served from CloudFront.",
|
|
1503
1972
|
category: "task",
|
|
1504
1973
|
priority: "default",
|
|
1505
1974
|
status: "verified",
|
|
1506
1975
|
up_next: false,
|
|
1976
|
+
tags: ["infrastructure", "cdn", "performance"],
|
|
1507
1977
|
notes: notesJson([
|
|
1508
|
-
{ text: "Configured CloudFront distribution with S3 origin. Migrated all product images (42GB)
|
|
1509
|
-
{ text: "Updated asset URLs
|
|
1978
|
+
{ text: "Configured CloudFront distribution with S3 origin. Migrated all product images (42GB).", days_ago: 7 },
|
|
1979
|
+
{ text: "Updated asset URLs. Cache hit rate is at 94% after 48 hours. TTFB improved from 240ms to 35ms.", days_ago: 5 }
|
|
1510
1980
|
]),
|
|
1511
1981
|
days_ago: 16,
|
|
1512
1982
|
updated_ago: 5,
|
|
@@ -1520,6 +1990,7 @@ var SCENARIO_6 = [
|
|
|
1520
1990
|
priority: "default",
|
|
1521
1991
|
status: "not_started",
|
|
1522
1992
|
up_next: false,
|
|
1993
|
+
tags: ["navigation", "ui"],
|
|
1523
1994
|
notes: "",
|
|
1524
1995
|
days_ago: 8,
|
|
1525
1996
|
updated_ago: 8
|
|
@@ -1528,88 +1999,96 @@ var SCENARIO_6 = [
|
|
|
1528
1999
|
var SCENARIO_7 = [
|
|
1529
2000
|
{
|
|
1530
2001
|
title: "Implement product search autocomplete",
|
|
1531
|
-
details: "Add typeahead suggestions to the search bar using the product name index.
|
|
2002
|
+
details: "Add typeahead suggestions to the search bar using the product name index.",
|
|
1532
2003
|
category: "feature",
|
|
1533
2004
|
priority: "highest",
|
|
1534
2005
|
status: "not_started",
|
|
1535
2006
|
up_next: true,
|
|
2007
|
+
tags: ["search", "ux"],
|
|
1536
2008
|
notes: "",
|
|
1537
2009
|
days_ago: 2,
|
|
1538
2010
|
updated_ago: 2
|
|
1539
2011
|
},
|
|
1540
2012
|
{
|
|
1541
2013
|
title: "Fix broken password reset flow for SSO users",
|
|
1542
|
-
details: "SSO users who try to reset their password get a generic error.
|
|
2014
|
+
details: "SSO users who try to reset their password get a generic error.",
|
|
1543
2015
|
category: "bug",
|
|
1544
2016
|
priority: "high",
|
|
1545
2017
|
status: "not_started",
|
|
1546
2018
|
up_next: true,
|
|
2019
|
+
tags: ["auth", "sso"],
|
|
1547
2020
|
notes: "",
|
|
1548
2021
|
days_ago: 3,
|
|
1549
2022
|
updated_ago: 3
|
|
1550
2023
|
},
|
|
1551
2024
|
{
|
|
1552
2025
|
title: "Add support for gift cards at checkout",
|
|
1553
|
-
details: "
|
|
2026
|
+
details: "Support gift card codes during checkout with partial redemption and balance tracking.",
|
|
1554
2027
|
category: "feature",
|
|
1555
2028
|
priority: "default",
|
|
1556
2029
|
status: "not_started",
|
|
1557
2030
|
up_next: false,
|
|
2031
|
+
tags: ["checkout", "payments"],
|
|
1558
2032
|
notes: "",
|
|
1559
2033
|
days_ago: 5,
|
|
1560
2034
|
updated_ago: 5
|
|
1561
2035
|
},
|
|
1562
2036
|
{
|
|
1563
2037
|
title: "Investigate slow query on order history page",
|
|
1564
|
-
details: "The order history page takes 4+ seconds for users with 200+ orders.
|
|
2038
|
+
details: "The order history page takes 4+ seconds for users with 200+ orders.",
|
|
1565
2039
|
category: "investigation",
|
|
1566
2040
|
priority: "high",
|
|
1567
2041
|
status: "not_started",
|
|
1568
2042
|
up_next: false,
|
|
2043
|
+
tags: ["performance", "database"],
|
|
1569
2044
|
notes: "",
|
|
1570
2045
|
days_ago: 4,
|
|
1571
2046
|
updated_ago: 4
|
|
1572
2047
|
},
|
|
1573
2048
|
{
|
|
1574
2049
|
title: "Refactor authentication middleware to support API keys",
|
|
1575
|
-
details: "Third-party integrations need API key auth in addition to session cookies.
|
|
2050
|
+
details: "Third-party integrations need API key auth in addition to session cookies.",
|
|
1576
2051
|
category: "task",
|
|
1577
2052
|
priority: "high",
|
|
1578
2053
|
status: "started",
|
|
1579
2054
|
up_next: true,
|
|
1580
|
-
|
|
2055
|
+
tags: ["auth", "api"],
|
|
2056
|
+
notes: notesJson([{ text: "Created the AuthStrategy interface and migrated session auth. Working on the API key strategy next.", days_ago: 0.5 }]),
|
|
1581
2057
|
days_ago: 4,
|
|
1582
2058
|
updated_ago: 0.5
|
|
1583
2059
|
},
|
|
1584
2060
|
{
|
|
1585
2061
|
title: "Fix cart not clearing after successful checkout",
|
|
1586
|
-
details: "
|
|
2062
|
+
details: "The clearCart() call is inside a catch block by mistake.",
|
|
1587
2063
|
category: "bug",
|
|
1588
2064
|
priority: "highest",
|
|
1589
2065
|
status: "started",
|
|
1590
2066
|
up_next: true,
|
|
2067
|
+
tags: ["checkout", "cart"],
|
|
1591
2068
|
notes: notesJson([{ text: "Found the issue \u2014 clearCart() was moved into the catch block during a refactor. Fixing and adding a test.", days_ago: 0.3 }]),
|
|
1592
2069
|
days_ago: 1,
|
|
1593
2070
|
updated_ago: 0.3
|
|
1594
2071
|
},
|
|
1595
2072
|
{
|
|
1596
2073
|
title: "Update shipping rate calculation for oversized items",
|
|
1597
|
-
details: "Dimensional weight pricing is required for packages over 1 cubic foot.
|
|
2074
|
+
details: "Dimensional weight pricing is required for packages over 1 cubic foot.",
|
|
1598
2075
|
category: "requirement_change",
|
|
1599
2076
|
priority: "default",
|
|
1600
2077
|
status: "started",
|
|
1601
2078
|
up_next: false,
|
|
1602
|
-
|
|
2079
|
+
tags: ["shipping", "pricing"],
|
|
2080
|
+
notes: notesJson([{ text: "Implemented dim weight formula. Comparing rates against the carrier API.", days_ago: 1 }]),
|
|
1603
2081
|
days_ago: 6,
|
|
1604
2082
|
updated_ago: 1
|
|
1605
2083
|
},
|
|
1606
2084
|
{
|
|
1607
2085
|
title: "Add end-to-end tests for the checkout flow",
|
|
1608
|
-
details: "Write Playwright tests covering: add to cart, apply coupon, enter shipping, pay, and confirm.
|
|
2086
|
+
details: "Write Playwright tests covering: add to cart, apply coupon, enter shipping, pay, and confirm.",
|
|
1609
2087
|
category: "task",
|
|
1610
2088
|
priority: "default",
|
|
1611
2089
|
status: "completed",
|
|
1612
2090
|
up_next: false,
|
|
2091
|
+
tags: ["testing", "e2e"],
|
|
1613
2092
|
notes: notesJson([{ text: "Wrote 8 E2E tests covering the full checkout flow including coupon application and payment decline handling.", days_ago: 1 }]),
|
|
1614
2093
|
days_ago: 8,
|
|
1615
2094
|
updated_ago: 1,
|
|
@@ -1617,29 +2096,54 @@ var SCENARIO_7 = [
|
|
|
1617
2096
|
},
|
|
1618
2097
|
{
|
|
1619
2098
|
title: "Fix product image carousel swipe on mobile",
|
|
1620
|
-
details: "Swipe gestures
|
|
2099
|
+
details: "Swipe gestures conflict with the browser back gesture.",
|
|
1621
2100
|
category: "bug",
|
|
1622
2101
|
priority: "default",
|
|
1623
2102
|
status: "completed",
|
|
1624
2103
|
up_next: false,
|
|
1625
|
-
|
|
2104
|
+
tags: ["mobile", "ui"],
|
|
2105
|
+
notes: notesJson([{ text: "Added a 30px horizontal threshold. Tested on iOS Safari and Chrome Android.", days_ago: 2 }]),
|
|
1626
2106
|
days_ago: 7,
|
|
1627
2107
|
updated_ago: 2,
|
|
1628
2108
|
completed_ago: 2
|
|
1629
2109
|
},
|
|
1630
2110
|
{
|
|
1631
2111
|
title: "Set up log aggregation with structured JSON logging",
|
|
1632
|
-
details: "Replace console.log calls with
|
|
2112
|
+
details: "Replace console.log calls with pino. Send logs to a central aggregation service.",
|
|
1633
2113
|
category: "task",
|
|
1634
2114
|
priority: "low",
|
|
1635
2115
|
status: "completed",
|
|
1636
2116
|
up_next: false,
|
|
1637
|
-
|
|
2117
|
+
tags: ["observability", "logging"],
|
|
2118
|
+
notes: notesJson([{ text: "Replaced all console.log with pino. Configured log shipping. Alert rules set for error-level logs.", days_ago: 3 }]),
|
|
1638
2119
|
days_ago: 10,
|
|
1639
2120
|
updated_ago: 3,
|
|
1640
2121
|
completed_ago: 3
|
|
1641
2122
|
}
|
|
1642
2123
|
];
|
|
2124
|
+
var SCENARIO_8 = [];
|
|
2125
|
+
for (let i = 0; i < 30; i++) {
|
|
2126
|
+
const cats = ["bug", "feature", "task", "investigation", "requirement_change", "issue"];
|
|
2127
|
+
const pris = ["highest", "high", "default", "low", "lowest"];
|
|
2128
|
+
const statuses = ["not_started", "started", "completed", "verified"];
|
|
2129
|
+
const status = statuses[i < 8 ? 0 : i < 14 ? 1 : i < 24 ? 2 : 3];
|
|
2130
|
+
const completed = status === "completed" || status === "verified" ? 30 - i + Math.floor(Math.random() * 5) : void 0;
|
|
2131
|
+
const verified = status === "verified" ? completed - 2 : void 0;
|
|
2132
|
+
SCENARIO_8.push({
|
|
2133
|
+
title: `Dashboard ticket ${i + 1} \u2014 ${cats[i % cats.length]} work item`,
|
|
2134
|
+
details: "",
|
|
2135
|
+
category: cats[i % cats.length],
|
|
2136
|
+
priority: pris[i % pris.length],
|
|
2137
|
+
status,
|
|
2138
|
+
up_next: i < 3,
|
|
2139
|
+
tags: [],
|
|
2140
|
+
notes: status === "completed" || status === "verified" ? notesJson([{ text: "Completed work.", days_ago: completed }]) : "",
|
|
2141
|
+
days_ago: 30 - i + Math.floor(Math.random() * 10),
|
|
2142
|
+
updated_ago: completed || Math.floor(Math.random() * 10),
|
|
2143
|
+
completed_ago: completed,
|
|
2144
|
+
verified_ago: verified
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
1643
2147
|
var SCENARIO_DATA = {
|
|
1644
2148
|
1: SCENARIO_1,
|
|
1645
2149
|
2: SCENARIO_2,
|
|
@@ -1647,8 +2151,29 @@ var SCENARIO_DATA = {
|
|
|
1647
2151
|
4: SCENARIO_4,
|
|
1648
2152
|
5: SCENARIO_5,
|
|
1649
2153
|
6: SCENARIO_6,
|
|
1650
|
-
7: SCENARIO_7
|
|
2154
|
+
7: SCENARIO_7,
|
|
2155
|
+
8: SCENARIO_8
|
|
1651
2156
|
};
|
|
2157
|
+
var SCENARIO_3_VIEWS = [
|
|
2158
|
+
{
|
|
2159
|
+
id: "high-priority-bugs",
|
|
2160
|
+
name: "High Priority Bugs",
|
|
2161
|
+
logic: "all",
|
|
2162
|
+
conditions: [
|
|
2163
|
+
{ field: "category", operator: "equals", value: "bug" },
|
|
2164
|
+
{ field: "priority", operator: "lte", value: "high" }
|
|
2165
|
+
]
|
|
2166
|
+
},
|
|
2167
|
+
{
|
|
2168
|
+
id: "active-features",
|
|
2169
|
+
name: "Active Features",
|
|
2170
|
+
logic: "all",
|
|
2171
|
+
conditions: [
|
|
2172
|
+
{ field: "category", operator: "equals", value: "feature" },
|
|
2173
|
+
{ field: "status", operator: "lte", value: "started" }
|
|
2174
|
+
]
|
|
2175
|
+
}
|
|
2176
|
+
];
|
|
1652
2177
|
async function seedDemoData(scenario) {
|
|
1653
2178
|
const db2 = await getDb();
|
|
1654
2179
|
const tickets = SCENARIO_DATA[scenario];
|
|
@@ -1660,12 +2185,19 @@ async function seedDemoData(scenario) {
|
|
|
1660
2185
|
const updatedAt = daysAgo(t.updated_ago);
|
|
1661
2186
|
const completedAt = t.completed_ago !== void 0 ? daysAgo(t.completed_ago) : null;
|
|
1662
2187
|
const verifiedAt = t.verified_ago !== void 0 ? daysAgo(t.verified_ago) : null;
|
|
2188
|
+
const tags = JSON.stringify(t.tags);
|
|
1663
2189
|
await db2.query(`
|
|
1664
|
-
INSERT INTO tickets (ticket_number, title, details, category, priority, status, up_next, notes, created_at, updated_at, completed_at, verified_at)
|
|
1665
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9
|
|
1666
|
-
`, [ticketNumber, t.title, t.details, t.category, t.priority, t.status, t.up_next, t.notes, createdAt, updatedAt, completedAt, verifiedAt]);
|
|
2190
|
+
INSERT INTO tickets (ticket_number, title, details, category, priority, status, up_next, notes, tags, created_at, updated_at, completed_at, verified_at)
|
|
2191
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::timestamp, $11::timestamp, $12::timestamp, $13::timestamp)
|
|
2192
|
+
`, [ticketNumber, t.title, t.details, t.category, t.priority, t.status, t.up_next, t.notes, tags, createdAt, updatedAt, completedAt, verifiedAt]);
|
|
1667
2193
|
}
|
|
1668
2194
|
await db2.query(`SELECT setval('ticket_seq', $1)`, [tickets.length]);
|
|
2195
|
+
if (scenario === 3) {
|
|
2196
|
+
await db2.query(
|
|
2197
|
+
`INSERT INTO settings (key, value) VALUES ('custom_views', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,
|
|
2198
|
+
[JSON.stringify(SCENARIO_3_VIEWS)]
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
1669
2201
|
if (scenario === 6) {
|
|
1670
2202
|
await db2.query(`UPDATE settings SET value = 'bottom' WHERE key = 'detail_position'`);
|
|
1671
2203
|
await db2.query(`UPDATE settings SET value = '280' WHERE key = 'detail_height'`);
|
|
@@ -1673,6 +2205,11 @@ async function seedDemoData(scenario) {
|
|
|
1673
2205
|
if (scenario === 7) {
|
|
1674
2206
|
await db2.query(`INSERT INTO settings (key, value) VALUES ('layout', 'columns') ON CONFLICT (key) DO UPDATE SET value = 'columns'`);
|
|
1675
2207
|
}
|
|
2208
|
+
if (scenario === 8) {
|
|
2209
|
+
const { backfillSnapshots: backfillSnapshots2, recordDailySnapshot: recordDailySnapshot2 } = await Promise.resolve().then(() => (init_stats(), stats_exports));
|
|
2210
|
+
await backfillSnapshots2();
|
|
2211
|
+
await recordDailySnapshot2();
|
|
2212
|
+
}
|
|
1676
2213
|
}
|
|
1677
2214
|
|
|
1678
2215
|
// src/cli.ts
|
|
@@ -1681,15 +2218,15 @@ init_gitignore();
|
|
|
1681
2218
|
// src/server.ts
|
|
1682
2219
|
import { serve } from "@hono/node-server";
|
|
1683
2220
|
import { exec } from "child_process";
|
|
1684
|
-
import { existsSync as
|
|
2221
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
1685
2222
|
import { Hono as Hono4 } from "hono";
|
|
1686
|
-
import { dirname, join as
|
|
2223
|
+
import { dirname, join as join10 } from "path";
|
|
1687
2224
|
import { fileURLToPath } from "url";
|
|
1688
2225
|
|
|
1689
2226
|
// src/routes/api.ts
|
|
1690
|
-
import { existsSync as
|
|
2227
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, rmSync as rmSync5 } from "fs";
|
|
1691
2228
|
import { Hono } from "hono";
|
|
1692
|
-
import { basename, extname, join as
|
|
2229
|
+
import { basename, extname, join as join9, relative as relative2 } from "path";
|
|
1693
2230
|
|
|
1694
2231
|
// src/skills.ts
|
|
1695
2232
|
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
@@ -1986,6 +2523,13 @@ async function formatTicket(ticket) {
|
|
|
1986
2523
|
lines.push(`- Priority: ${ticket.priority}`);
|
|
1987
2524
|
lines.push(`- Status: ${ticket.status.replace("_", " ")}`);
|
|
1988
2525
|
lines.push(`- Title: ${ticket.title}`);
|
|
2526
|
+
try {
|
|
2527
|
+
const tags = JSON.parse(ticket.tags);
|
|
2528
|
+
if (Array.isArray(tags) && tags.length > 0) {
|
|
2529
|
+
lines.push(`- Tags: ${tags.join(", ")}`);
|
|
2530
|
+
}
|
|
2531
|
+
} catch {
|
|
2532
|
+
}
|
|
1989
2533
|
if (ticket.details.trim()) {
|
|
1990
2534
|
const detailLines = ticket.details.split("\n");
|
|
1991
2535
|
lines.push(`- Details: ${detailLines[0]}`);
|
|
@@ -2133,8 +2677,8 @@ function notifyChange() {
|
|
|
2133
2677
|
changeVersion++;
|
|
2134
2678
|
const waiters = pollWaiters;
|
|
2135
2679
|
pollWaiters = [];
|
|
2136
|
-
for (const
|
|
2137
|
-
|
|
2680
|
+
for (const resolve3 of waiters) {
|
|
2681
|
+
resolve3(changeVersion);
|
|
2138
2682
|
}
|
|
2139
2683
|
}
|
|
2140
2684
|
apiRoutes.get("/poll", async (c) => {
|
|
@@ -2143,11 +2687,11 @@ apiRoutes.get("/poll", async (c) => {
|
|
|
2143
2687
|
return c.json({ version: changeVersion });
|
|
2144
2688
|
}
|
|
2145
2689
|
const version = await Promise.race([
|
|
2146
|
-
new Promise((
|
|
2147
|
-
pollWaiters.push(
|
|
2690
|
+
new Promise((resolve3) => {
|
|
2691
|
+
pollWaiters.push(resolve3);
|
|
2148
2692
|
}),
|
|
2149
|
-
new Promise((
|
|
2150
|
-
setTimeout(() =>
|
|
2693
|
+
new Promise((resolve3) => {
|
|
2694
|
+
setTimeout(() => resolve3(changeVersion), 3e4);
|
|
2151
2695
|
})
|
|
2152
2696
|
]);
|
|
2153
2697
|
return c.json({ version });
|
|
@@ -2183,7 +2727,8 @@ apiRoutes.get("/tickets/:id", async (c) => {
|
|
|
2183
2727
|
const ticket = await getTicket(id);
|
|
2184
2728
|
if (!ticket) return c.json({ error: "Not found" }, 404);
|
|
2185
2729
|
const attachments = await getAttachments(id);
|
|
2186
|
-
|
|
2730
|
+
const notes = parseNotes(ticket.notes);
|
|
2731
|
+
return c.json({ ...ticket, notes: JSON.stringify(notes), attachments });
|
|
2187
2732
|
});
|
|
2188
2733
|
apiRoutes.patch("/tickets/:id", async (c) => {
|
|
2189
2734
|
const id = parseInt(c.req.param("id"), 10);
|
|
@@ -2201,6 +2746,38 @@ apiRoutes.delete("/tickets/:id", async (c) => {
|
|
|
2201
2746
|
notifyChange();
|
|
2202
2747
|
return c.json({ ok: true });
|
|
2203
2748
|
});
|
|
2749
|
+
apiRoutes.put("/tickets/:id/notes-bulk", async (c) => {
|
|
2750
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
2751
|
+
const body = await c.req.json();
|
|
2752
|
+
const db2 = await Promise.resolve().then(() => (init_connection(), connection_exports)).then((m) => m.getDb());
|
|
2753
|
+
const result = await (await db2).query(
|
|
2754
|
+
`UPDATE tickets SET notes = $1, updated_at = NOW() WHERE id = $2 RETURNING id`,
|
|
2755
|
+
[body.notes, id]
|
|
2756
|
+
);
|
|
2757
|
+
if (result.rows.length === 0) return c.json({ error: "Not found" }, 404);
|
|
2758
|
+
scheduleAllSync();
|
|
2759
|
+
notifyChange();
|
|
2760
|
+
return c.json({ ok: true });
|
|
2761
|
+
});
|
|
2762
|
+
apiRoutes.patch("/tickets/:id/notes/:noteId", async (c) => {
|
|
2763
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
2764
|
+
const noteId2 = c.req.param("noteId");
|
|
2765
|
+
const body = await c.req.json();
|
|
2766
|
+
const notes = await editNote(id, noteId2, body.text);
|
|
2767
|
+
if (!notes) return c.json({ error: "Not found" }, 404);
|
|
2768
|
+
scheduleAllSync();
|
|
2769
|
+
notifyChange();
|
|
2770
|
+
return c.json(notes);
|
|
2771
|
+
});
|
|
2772
|
+
apiRoutes.delete("/tickets/:id/notes/:noteId", async (c) => {
|
|
2773
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
2774
|
+
const noteId2 = c.req.param("noteId");
|
|
2775
|
+
const notes = await deleteNote(id, noteId2);
|
|
2776
|
+
if (!notes) return c.json({ error: "Not found" }, 404);
|
|
2777
|
+
scheduleAllSync();
|
|
2778
|
+
notifyChange();
|
|
2779
|
+
return c.json(notes);
|
|
2780
|
+
});
|
|
2204
2781
|
apiRoutes.delete("/tickets/:id/hard", async (c) => {
|
|
2205
2782
|
const id = parseInt(c.req.param("id"), 10);
|
|
2206
2783
|
const attachments = await getAttachments(id);
|
|
@@ -2294,12 +2871,12 @@ apiRoutes.post("/tickets/:id/attachments", async (c) => {
|
|
|
2294
2871
|
const ext = extname(originalName);
|
|
2295
2872
|
const baseName = basename(originalName, ext);
|
|
2296
2873
|
const storedName = `${ticket.ticket_number}_${baseName}${ext}`;
|
|
2297
|
-
const attachDir =
|
|
2874
|
+
const attachDir = join9(dataDir2, "attachments");
|
|
2298
2875
|
mkdirSync4(attachDir, { recursive: true });
|
|
2299
|
-
const storedPath =
|
|
2876
|
+
const storedPath = join9(attachDir, storedName);
|
|
2300
2877
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
2301
|
-
const { writeFileSync:
|
|
2302
|
-
|
|
2878
|
+
const { writeFileSync: writeFileSync8 } = await import("fs");
|
|
2879
|
+
writeFileSync8(storedPath, buffer);
|
|
2303
2880
|
const attachment = await addAttachment(id, originalName, storedPath);
|
|
2304
2881
|
scheduleAllSync();
|
|
2305
2882
|
notifyChange();
|
|
@@ -2321,7 +2898,7 @@ apiRoutes.post("/attachments/:id/reveal", async (c) => {
|
|
|
2321
2898
|
const id = parseInt(c.req.param("id"), 10);
|
|
2322
2899
|
const attachment = await getAttachment(id);
|
|
2323
2900
|
if (!attachment) return c.json({ error: "Not found" }, 404);
|
|
2324
|
-
if (!
|
|
2901
|
+
if (!existsSync7(attachment.stored_path)) return c.json({ error: "File not found on disk" }, 404);
|
|
2325
2902
|
const { execFile } = await import("child_process");
|
|
2326
2903
|
const { dirname: dirname3 } = await import("path");
|
|
2327
2904
|
const platform = process.platform;
|
|
@@ -2337,12 +2914,12 @@ apiRoutes.post("/attachments/:id/reveal", async (c) => {
|
|
|
2337
2914
|
apiRoutes.get("/attachments/file/*", async (c) => {
|
|
2338
2915
|
const filePath = c.req.path.replace("/api/attachments/file/", "");
|
|
2339
2916
|
const dataDir2 = c.get("dataDir");
|
|
2340
|
-
const fullPath =
|
|
2341
|
-
if (!
|
|
2917
|
+
const fullPath = join9(dataDir2, "attachments", filePath);
|
|
2918
|
+
if (!existsSync7(fullPath)) {
|
|
2342
2919
|
return c.json({ error: "File not found" }, 404);
|
|
2343
2920
|
}
|
|
2344
|
-
const { readFileSync:
|
|
2345
|
-
const content =
|
|
2921
|
+
const { readFileSync: readFileSync9 } = await import("fs");
|
|
2922
|
+
const content = readFileSync9(fullPath);
|
|
2346
2923
|
const ext = extname(fullPath).toLowerCase();
|
|
2347
2924
|
const mimeTypes = {
|
|
2348
2925
|
".png": "image/png",
|
|
@@ -2360,6 +2937,15 @@ apiRoutes.get("/attachments/file/*", async (c) => {
|
|
|
2360
2937
|
headers: { "Content-Type": contentType }
|
|
2361
2938
|
});
|
|
2362
2939
|
});
|
|
2940
|
+
apiRoutes.post("/tickets/query", async (c) => {
|
|
2941
|
+
const body = await c.req.json();
|
|
2942
|
+
const tickets = await queryTickets(body.logic, body.conditions, body.sort_by, body.sort_dir);
|
|
2943
|
+
return c.json(tickets);
|
|
2944
|
+
});
|
|
2945
|
+
apiRoutes.get("/tags", async (c) => {
|
|
2946
|
+
const tags = await getAllTags();
|
|
2947
|
+
return c.json(tags);
|
|
2948
|
+
});
|
|
2363
2949
|
apiRoutes.get("/categories", async (c) => {
|
|
2364
2950
|
const categories = await getCategories();
|
|
2365
2951
|
return c.json(categories);
|
|
@@ -2378,6 +2964,15 @@ apiRoutes.get("/stats", async (c) => {
|
|
|
2378
2964
|
const stats = await getTicketStats();
|
|
2379
2965
|
return c.json(stats);
|
|
2380
2966
|
});
|
|
2967
|
+
apiRoutes.get("/dashboard", async (c) => {
|
|
2968
|
+
const { getDashboardStats: getDashboardStats2, getSnapshots: getSnapshots2 } = await Promise.resolve().then(() => (init_stats(), stats_exports));
|
|
2969
|
+
const days = parseInt(c.req.query("days") || "30", 10);
|
|
2970
|
+
const [stats, snapshots] = await Promise.all([
|
|
2971
|
+
getDashboardStats2(days),
|
|
2972
|
+
getSnapshots2(days)
|
|
2973
|
+
]);
|
|
2974
|
+
return c.json({ ...stats, snapshots });
|
|
2975
|
+
});
|
|
2381
2976
|
apiRoutes.get("/settings", async (c) => {
|
|
2382
2977
|
const settings = await getSettings();
|
|
2383
2978
|
return c.json(settings);
|
|
@@ -2404,7 +2999,7 @@ apiRoutes.patch("/file-settings", async (c) => {
|
|
|
2404
2999
|
apiRoutes.get("/worklist-info", (c) => {
|
|
2405
3000
|
const dataDir2 = c.get("dataDir");
|
|
2406
3001
|
const cwd = process.cwd();
|
|
2407
|
-
const worklistRel = relative2(cwd,
|
|
3002
|
+
const worklistRel = relative2(cwd, join9(dataDir2, "worklist.md"));
|
|
2408
3003
|
const prompt = `Read ${worklistRel} for current work items.`;
|
|
2409
3004
|
ensureSkills();
|
|
2410
3005
|
const skillCreated = consumeSkillsCreatedFlag();
|
|
@@ -2444,6 +3039,78 @@ apiRoutes.post("/gitignore/add", async (c) => {
|
|
|
2444
3039
|
ensureGitignore2(process.cwd());
|
|
2445
3040
|
return c.json({ ok: true });
|
|
2446
3041
|
});
|
|
3042
|
+
apiRoutes.get("/channel/claude-check", async (c) => {
|
|
3043
|
+
const { execFileSync } = await import("child_process");
|
|
3044
|
+
try {
|
|
3045
|
+
const version = execFileSync("claude", ["--version"], { timeout: 5e3, encoding: "utf-8" }).trim();
|
|
3046
|
+
const match = version.match(/(\d+\.\d+\.\d+)/);
|
|
3047
|
+
const versionNum = match ? match[1] : null;
|
|
3048
|
+
const parts = versionNum ? versionNum.split(".").map(Number) : [];
|
|
3049
|
+
const meetsMinimum = parts.length === 3 && (parts[0] > 2 || parts[0] === 2 && parts[1] > 1 || parts[0] === 2 && parts[1] === 1 && parts[2] >= 80);
|
|
3050
|
+
return c.json({ installed: true, version: versionNum, meetsMinimum });
|
|
3051
|
+
} catch {
|
|
3052
|
+
return c.json({ installed: false, version: null, meetsMinimum: false });
|
|
3053
|
+
}
|
|
3054
|
+
});
|
|
3055
|
+
var channelDoneFlag = false;
|
|
3056
|
+
apiRoutes.get("/channel/status", async (c) => {
|
|
3057
|
+
const { isChannelAlive: isChannelAlive2, getChannelPort: getChannelPort2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
3058
|
+
const dataDir2 = c.get("dataDir");
|
|
3059
|
+
const settings = await getSettings();
|
|
3060
|
+
const enabled = settings.channel_enabled === "true";
|
|
3061
|
+
const port2 = getChannelPort2(dataDir2);
|
|
3062
|
+
const alive = enabled ? await isChannelAlive2(dataDir2) : false;
|
|
3063
|
+
const done = channelDoneFlag;
|
|
3064
|
+
if (done) channelDoneFlag = false;
|
|
3065
|
+
return c.json({ enabled, alive, port: port2, done });
|
|
3066
|
+
});
|
|
3067
|
+
apiRoutes.post("/channel/trigger", async (c) => {
|
|
3068
|
+
const { triggerChannel: triggerChannel2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
3069
|
+
const dataDir2 = c.get("dataDir");
|
|
3070
|
+
const serverPort = parseInt(new URL(c.req.url).port || "4174", 10);
|
|
3071
|
+
const body = await c.req.json().catch(() => ({ message: void 0 }));
|
|
3072
|
+
channelDoneFlag = false;
|
|
3073
|
+
const ok = await triggerChannel2(dataDir2, serverPort, body.message);
|
|
3074
|
+
return c.json({ ok });
|
|
3075
|
+
});
|
|
3076
|
+
apiRoutes.post("/channel/done", async (_c) => {
|
|
3077
|
+
channelDoneFlag = true;
|
|
3078
|
+
notifyChange();
|
|
3079
|
+
return _c.json({ ok: true });
|
|
3080
|
+
});
|
|
3081
|
+
apiRoutes.post("/channel/enable", async (c) => {
|
|
3082
|
+
const { registerChannel: registerChannel2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
3083
|
+
const dataDir2 = c.get("dataDir");
|
|
3084
|
+
await updateSetting("channel_enabled", "true");
|
|
3085
|
+
registerChannel2(dataDir2);
|
|
3086
|
+
notifyChange();
|
|
3087
|
+
return c.json({ ok: true });
|
|
3088
|
+
});
|
|
3089
|
+
apiRoutes.post("/channel/disable", async (c) => {
|
|
3090
|
+
const { unregisterChannel: unregisterChannel2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
3091
|
+
await updateSetting("channel_enabled", "false");
|
|
3092
|
+
unregisterChannel2();
|
|
3093
|
+
notifyChange();
|
|
3094
|
+
return c.json({ ok: true });
|
|
3095
|
+
});
|
|
3096
|
+
apiRoutes.post("/print", async (c) => {
|
|
3097
|
+
const { html } = await c.req.json();
|
|
3098
|
+
const { writeFileSync: writeFileSync8 } = await import("fs");
|
|
3099
|
+
const { tmpdir: tmpdir2 } = await import("os");
|
|
3100
|
+
const { join: pathJoin } = await import("path");
|
|
3101
|
+
const { execFile } = await import("child_process");
|
|
3102
|
+
const tmpPath = pathJoin(tmpdir2(), `hotsheet-print-${Date.now()}.html`);
|
|
3103
|
+
writeFileSync8(tmpPath, html, "utf-8");
|
|
3104
|
+
const platform = process.platform;
|
|
3105
|
+
if (platform === "darwin") {
|
|
3106
|
+
execFile("open", [tmpPath]);
|
|
3107
|
+
} else if (platform === "win32") {
|
|
3108
|
+
execFile("start", ["", tmpPath], { shell: true });
|
|
3109
|
+
} else {
|
|
3110
|
+
execFile("xdg-open", [tmpPath]);
|
|
3111
|
+
}
|
|
3112
|
+
return c.json({ ok: true, path: tmpPath });
|
|
3113
|
+
});
|
|
2447
3114
|
|
|
2448
3115
|
// src/routes/backups.ts
|
|
2449
3116
|
import { Hono as Hono2 } from "hono";
|
|
@@ -2732,6 +3399,11 @@ pageRoutes.get("/", (c) => {
|
|
|
2732
3399
|
] }) })
|
|
2733
3400
|
] }),
|
|
2734
3401
|
/* @__PURE__ */ jsx("button", { className: "glassbox-btn", id: "glassbox-btn", title: "Open Glassbox", style: "display:none", children: /* @__PURE__ */ jsx("img", { id: "glassbox-icon", alt: "Glassbox" }) }),
|
|
3402
|
+
/* @__PURE__ */ jsx("button", { className: "settings-btn print-btn", id: "print-btn", title: "Print", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3403
|
+
/* @__PURE__ */ jsx("path", { d: "M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2" }),
|
|
3404
|
+
/* @__PURE__ */ jsx("path", { d: "M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6" }),
|
|
3405
|
+
/* @__PURE__ */ jsx("rect", { x: "6", y: "14", width: "12", height: "8", rx: "1" })
|
|
3406
|
+
] }) }),
|
|
2735
3407
|
/* @__PURE__ */ jsx("button", { className: "settings-btn", id: "settings-btn", title: "Settings", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2736
3408
|
/* @__PURE__ */ jsx("path", { d: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" }),
|
|
2737
3409
|
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" })
|
|
@@ -2758,6 +3430,13 @@ pageRoutes.get("/", (c) => {
|
|
|
2758
3430
|
] }),
|
|
2759
3431
|
/* @__PURE__ */ jsx("div", { className: "app-body", children: [
|
|
2760
3432
|
/* @__PURE__ */ jsx("nav", { className: "sidebar", children: [
|
|
3433
|
+
/* @__PURE__ */ jsx("div", { className: "sidebar-channel-play", id: "channel-play-section", style: "display:none", children: /* @__PURE__ */ jsx("button", { className: "channel-play-btn", id: "channel-play-btn", title: "Run worklist (double-click for auto mode)", children: [
|
|
3434
|
+
/* @__PURE__ */ jsx("span", { className: "channel-play-icon", id: "channel-play-icon", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", stroke: "none", children: /* @__PURE__ */ jsx("polygon", { points: "6 3 20 12 6 21 6 3" }) }) }),
|
|
3435
|
+
/* @__PURE__ */ jsx("span", { className: "channel-auto-icon", id: "channel-auto-icon", style: "display:none", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "white", stroke: "none", children: [
|
|
3436
|
+
/* @__PURE__ */ jsx("path", { d: "M12 6a2 2 0 0 1 3.414-1.414l6 6a2 2 0 0 1 0 2.828l-6 6A2 2 0 0 1 12 18z" }),
|
|
3437
|
+
/* @__PURE__ */ jsx("path", { d: "M2 6a2 2 0 0 1 3.414-1.414l6 6a2 2 0 0 1 0 2.828l-6 6A2 2 0 0 1 2 18z" })
|
|
3438
|
+
] }) })
|
|
3439
|
+
] }) }),
|
|
2761
3440
|
/* @__PURE__ */ jsx("div", { className: "sidebar-copy-prompt", id: "copy-prompt-section", style: "display:none", children: /* @__PURE__ */ jsx("button", { className: "copy-prompt-btn", id: "copy-prompt-btn", title: "Copy worklist prompt to clipboard", children: [
|
|
2762
3441
|
/* @__PURE__ */ jsx("span", { className: "copy-prompt-icon", id: "copy-prompt-icon", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2763
3442
|
/* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2" }),
|
|
@@ -2766,19 +3445,27 @@ pageRoutes.get("/", (c) => {
|
|
|
2766
3445
|
/* @__PURE__ */ jsx("span", { id: "copy-prompt-label", children: "Copy AI prompt" })
|
|
2767
3446
|
] }) }),
|
|
2768
3447
|
/* @__PURE__ */ jsx("div", { className: "sidebar-section", children: [
|
|
2769
|
-
/* @__PURE__ */ jsx("div", { className: "sidebar-label", children:
|
|
3448
|
+
/* @__PURE__ */ jsx("div", { className: "sidebar-label", children: [
|
|
3449
|
+
"Views ",
|
|
3450
|
+
/* @__PURE__ */ jsx("button", { className: "sidebar-add-view-btn", id: "add-custom-view-btn", title: "New custom view", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3451
|
+
/* @__PURE__ */ jsx("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
|
|
3452
|
+
/* @__PURE__ */ jsx("path", { d: "M8 12h8" }),
|
|
3453
|
+
/* @__PURE__ */ jsx("path", { d: "M12 8v8" })
|
|
3454
|
+
] }) })
|
|
3455
|
+
] }),
|
|
2770
3456
|
/* @__PURE__ */ jsx("button", { className: "sidebar-item active", "data-view": "all", children: "All Tickets" }),
|
|
2771
3457
|
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "non-verified", children: "Non-Verified" }),
|
|
2772
3458
|
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "up-next", children: "Up Next" }),
|
|
2773
3459
|
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "open", children: "Open" }),
|
|
2774
3460
|
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "completed", children: "Completed" }),
|
|
2775
3461
|
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "verified", children: "Verified" }),
|
|
3462
|
+
/* @__PURE__ */ jsx("div", { id: "custom-views-container" }),
|
|
2776
3463
|
/* @__PURE__ */ jsx("div", { className: "sidebar-divider" }),
|
|
2777
3464
|
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "backlog", children: "Backlog" }),
|
|
2778
3465
|
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "archive", children: "Archive" }),
|
|
2779
3466
|
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "trash", children: "Trash" })
|
|
2780
3467
|
] }),
|
|
2781
|
-
/* @__PURE__ */ jsx("div", { className: "sidebar-section", children: [
|
|
3468
|
+
/* @__PURE__ */ jsx("div", { className: "sidebar-section", id: "sidebar-categories", children: [
|
|
2782
3469
|
/* @__PURE__ */ jsx("div", { className: "sidebar-label", children: "Category" }),
|
|
2783
3470
|
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "category:issue", children: [
|
|
2784
3471
|
/* @__PURE__ */ jsx("span", { className: "cat-dot", style: "background:#6b7280" }),
|
|
@@ -2860,10 +3547,10 @@ pageRoutes.get("/", (c) => {
|
|
|
2860
3547
|
/* @__PURE__ */ jsx("label", { children: "Status" }),
|
|
2861
3548
|
/* @__PURE__ */ jsx("button", { id: "detail-status", className: "detail-dropdown-btn", "data-value": "not_started", children: "Not Started" })
|
|
2862
3549
|
] }),
|
|
2863
|
-
/* @__PURE__ */ jsx("div", { className: "detail-field", children:
|
|
2864
|
-
/* @__PURE__ */ jsx("
|
|
2865
|
-
"
|
|
2866
|
-
] })
|
|
3550
|
+
/* @__PURE__ */ jsx("div", { className: "detail-field", children: [
|
|
3551
|
+
/* @__PURE__ */ jsx("label", { children: "Up Next" }),
|
|
3552
|
+
/* @__PURE__ */ jsx("button", { className: "ticket-star detail-upnext-star", id: "detail-upnext", type: "button", children: "\u2606" })
|
|
3553
|
+
] })
|
|
2867
3554
|
] }),
|
|
2868
3555
|
/* @__PURE__ */ jsx("div", { className: "detail-field detail-field-full", children: [
|
|
2869
3556
|
/* @__PURE__ */ jsx("label", { children: "Title" }),
|
|
@@ -2873,6 +3560,11 @@ pageRoutes.get("/", (c) => {
|
|
|
2873
3560
|
/* @__PURE__ */ jsx("label", { children: "Details" }),
|
|
2874
3561
|
/* @__PURE__ */ jsx("textarea", { id: "detail-details", rows: 6, placeholder: "Add details..." })
|
|
2875
3562
|
] }),
|
|
3563
|
+
/* @__PURE__ */ jsx("div", { className: "detail-field detail-field-full", children: [
|
|
3564
|
+
/* @__PURE__ */ jsx("label", { children: "Tags" }),
|
|
3565
|
+
/* @__PURE__ */ jsx("div", { id: "detail-tags", className: "detail-tags" }),
|
|
3566
|
+
/* @__PURE__ */ jsx("input", { type: "text", id: "detail-tag-input", className: "detail-tag-input", placeholder: "Add tag..." })
|
|
3567
|
+
] }),
|
|
2876
3568
|
/* @__PURE__ */ jsx("div", { className: "detail-field detail-field-full", children: [
|
|
2877
3569
|
/* @__PURE__ */ jsx("label", { children: "Attachments" }),
|
|
2878
3570
|
/* @__PURE__ */ jsx("div", { id: "detail-attachments", className: "detail-attachments" }),
|
|
@@ -2881,8 +3573,15 @@ pageRoutes.get("/", (c) => {
|
|
|
2881
3573
|
/* @__PURE__ */ jsx("input", { type: "file", id: "detail-file-input", style: "display:none" })
|
|
2882
3574
|
] })
|
|
2883
3575
|
] }),
|
|
2884
|
-
/* @__PURE__ */ jsx("div", { className: "detail-field detail-field-full", id: "detail-notes-section",
|
|
2885
|
-
/* @__PURE__ */ jsx("
|
|
3576
|
+
/* @__PURE__ */ jsx("div", { className: "detail-field detail-field-full", id: "detail-notes-section", children: [
|
|
3577
|
+
/* @__PURE__ */ jsx("div", { className: "detail-notes-label", children: [
|
|
3578
|
+
/* @__PURE__ */ jsx("span", { children: "Notes" }),
|
|
3579
|
+
" ",
|
|
3580
|
+
/* @__PURE__ */ jsx("button", { className: "sidebar-add-view-btn", id: "detail-add-note-btn", title: "Add note", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3581
|
+
/* @__PURE__ */ jsx("path", { d: "M5 12h14" }),
|
|
3582
|
+
/* @__PURE__ */ jsx("path", { d: "M12 5v14" })
|
|
3583
|
+
] }) })
|
|
3584
|
+
] }),
|
|
2886
3585
|
/* @__PURE__ */ jsx("div", { id: "detail-notes", className: "detail-notes" })
|
|
2887
3586
|
] }),
|
|
2888
3587
|
/* @__PURE__ */ jsx("div", { className: "detail-meta detail-field-full", id: "detail-meta" })
|
|
@@ -2919,7 +3618,10 @@ pageRoutes.get("/", (c) => {
|
|
|
2919
3618
|
" close"
|
|
2920
3619
|
] })
|
|
2921
3620
|
] }),
|
|
2922
|
-
/* @__PURE__ */ jsx("div", {
|
|
3621
|
+
/* @__PURE__ */ jsx("div", { className: "status-bar-right", children: [
|
|
3622
|
+
/* @__PURE__ */ jsx("div", { id: "status-bar", className: "status-bar" }),
|
|
3623
|
+
/* @__PURE__ */ jsx("span", { id: "channel-status-indicator", className: "channel-status-indicator", style: "display:none" })
|
|
3624
|
+
] })
|
|
2923
3625
|
] })
|
|
2924
3626
|
] }),
|
|
2925
3627
|
/* @__PURE__ */ jsx("div", { className: "settings-overlay", id: "settings-overlay", style: "display:none", children: /* @__PURE__ */ jsx("div", { className: "settings-dialog", children: [
|
|
@@ -2981,7 +3683,24 @@ pageRoutes.get("/", (c) => {
|
|
|
2981
3683
|
/* @__PURE__ */ jsx("div", { className: "settings-field", children: [
|
|
2982
3684
|
/* @__PURE__ */ jsx("label", { children: "Auto-clear verified after (days)" }),
|
|
2983
3685
|
/* @__PURE__ */ jsx("input", { type: "number", id: "settings-verified-days", min: "1", value: "30" })
|
|
2984
|
-
] })
|
|
3686
|
+
] }),
|
|
3687
|
+
/* @__PURE__ */ jsx("div", { id: "settings-channel-section", style: "display:none", children: /* @__PURE__ */ jsx("div", { className: "settings-section", style: "margin-top:16px", children: [
|
|
3688
|
+
/* @__PURE__ */ jsx("h3", { className: "settings-experimental-heading", children: "Experimental" }),
|
|
3689
|
+
/* @__PURE__ */ jsx("div", { className: "settings-field", children: [
|
|
3690
|
+
/* @__PURE__ */ jsx("label", { className: "settings-checkbox-label", children: [
|
|
3691
|
+
/* @__PURE__ */ jsx("input", { type: "checkbox", id: "settings-channel-enabled" }),
|
|
3692
|
+
"Enable Claude Channel integration"
|
|
3693
|
+
] }),
|
|
3694
|
+
/* @__PURE__ */ jsx("span", { className: "settings-hint", id: "settings-channel-hint", children: "Push worklist events to a running Claude Code session via MCP channels." }),
|
|
3695
|
+
/* @__PURE__ */ jsx("div", { id: "settings-channel-instructions", style: "display:none", children: [
|
|
3696
|
+
/* @__PURE__ */ jsx("div", { className: "settings-hint", style: "margin-top:8px", children: "Launch Claude Code with channel support:" }),
|
|
3697
|
+
/* @__PURE__ */ jsx("div", { className: "settings-channel-command", children: [
|
|
3698
|
+
/* @__PURE__ */ jsx("code", { id: "settings-channel-cmd", children: "claude --dangerously-load-development-channels server:hotsheet-channel" }),
|
|
3699
|
+
/* @__PURE__ */ jsx("button", { className: "btn btn-sm", id: "settings-channel-copy-btn", title: "Copy command", children: "Copy" })
|
|
3700
|
+
] })
|
|
3701
|
+
] })
|
|
3702
|
+
] })
|
|
3703
|
+
] }) })
|
|
2985
3704
|
] }),
|
|
2986
3705
|
/* @__PURE__ */ jsx("div", { className: "settings-tab-panel", "data-panel": "categories", children: [
|
|
2987
3706
|
/* @__PURE__ */ jsx("div", { className: "settings-section-header", children: [
|
|
@@ -3017,11 +3736,11 @@ pageRoutes.get("/", (c) => {
|
|
|
3017
3736
|
});
|
|
3018
3737
|
|
|
3019
3738
|
// src/server.ts
|
|
3020
|
-
function tryServe(
|
|
3021
|
-
return new Promise((
|
|
3022
|
-
const server = serve({ fetch, port: port2 });
|
|
3739
|
+
function tryServe(fetch2, port2) {
|
|
3740
|
+
return new Promise((resolve3, reject) => {
|
|
3741
|
+
const server = serve({ fetch: fetch2, port: port2 });
|
|
3023
3742
|
server.on("listening", () => {
|
|
3024
|
-
|
|
3743
|
+
resolve3(port2);
|
|
3025
3744
|
});
|
|
3026
3745
|
server.on("error", (err) => {
|
|
3027
3746
|
reject(err);
|
|
@@ -3035,20 +3754,20 @@ async function startServer(port2, dataDir2, options) {
|
|
|
3035
3754
|
await next();
|
|
3036
3755
|
});
|
|
3037
3756
|
const selfDir = dirname(fileURLToPath(import.meta.url));
|
|
3038
|
-
const distDir =
|
|
3757
|
+
const distDir = existsSync8(join10(selfDir, "client", "styles.css")) ? join10(selfDir, "client") : join10(selfDir, "..", "dist", "client");
|
|
3039
3758
|
app.get("/static/styles.css", (c) => {
|
|
3040
|
-
const css =
|
|
3759
|
+
const css = readFileSync7(join10(distDir, "styles.css"), "utf-8");
|
|
3041
3760
|
return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
|
|
3042
3761
|
});
|
|
3043
3762
|
app.get("/static/app.js", (c) => {
|
|
3044
|
-
const js =
|
|
3763
|
+
const js = readFileSync7(join10(distDir, "app.global.js"), "utf-8");
|
|
3045
3764
|
return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
|
|
3046
3765
|
});
|
|
3047
3766
|
app.get("/static/assets/:filename", (c) => {
|
|
3048
3767
|
const filename = c.req.param("filename");
|
|
3049
|
-
const filePath =
|
|
3050
|
-
if (!
|
|
3051
|
-
const content =
|
|
3768
|
+
const filePath = join10(distDir, "assets", filename);
|
|
3769
|
+
if (!existsSync8(filePath)) return c.notFound();
|
|
3770
|
+
const content = readFileSync7(filePath);
|
|
3052
3771
|
const ext = filename.split(".").pop();
|
|
3053
3772
|
const mimeTypes = { png: "image/png", jpg: "image/jpeg", svg: "image/svg+xml" };
|
|
3054
3773
|
return new Response(content, { headers: { "Content-Type": mimeTypes[ext || ""] || "application/octet-stream", "Cache-Control": "max-age=86400" } });
|
|
@@ -3092,18 +3811,18 @@ async function startServer(port2, dataDir2, options) {
|
|
|
3092
3811
|
}
|
|
3093
3812
|
|
|
3094
3813
|
// src/update-check.ts
|
|
3095
|
-
import { existsSync as
|
|
3814
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
3096
3815
|
import { get } from "https";
|
|
3097
3816
|
import { homedir } from "os";
|
|
3098
|
-
import { dirname as dirname2, join as
|
|
3817
|
+
import { dirname as dirname2, join as join11 } from "path";
|
|
3099
3818
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3100
|
-
var DATA_DIR =
|
|
3101
|
-
var CHECK_FILE =
|
|
3819
|
+
var DATA_DIR = join11(homedir(), ".hotsheet");
|
|
3820
|
+
var CHECK_FILE = join11(DATA_DIR, "last-update-check");
|
|
3102
3821
|
var PACKAGE_NAME = "hotsheet";
|
|
3103
3822
|
function getCurrentVersion() {
|
|
3104
3823
|
try {
|
|
3105
3824
|
const dir = dirname2(fileURLToPath2(import.meta.url));
|
|
3106
|
-
const pkg = JSON.parse(
|
|
3825
|
+
const pkg = JSON.parse(readFileSync8(join11(dir, "..", "package.json"), "utf-8"));
|
|
3107
3826
|
return pkg.version;
|
|
3108
3827
|
} catch {
|
|
3109
3828
|
return "0.0.0";
|
|
@@ -3111,8 +3830,8 @@ function getCurrentVersion() {
|
|
|
3111
3830
|
}
|
|
3112
3831
|
function getLastCheckDate() {
|
|
3113
3832
|
try {
|
|
3114
|
-
if (
|
|
3115
|
-
return
|
|
3833
|
+
if (existsSync9(CHECK_FILE)) {
|
|
3834
|
+
return readFileSync8(CHECK_FILE, "utf-8").trim();
|
|
3116
3835
|
}
|
|
3117
3836
|
} catch {
|
|
3118
3837
|
}
|
|
@@ -3120,7 +3839,7 @@ function getLastCheckDate() {
|
|
|
3120
3839
|
}
|
|
3121
3840
|
function saveCheckDate() {
|
|
3122
3841
|
mkdirSync5(DATA_DIR, { recursive: true });
|
|
3123
|
-
|
|
3842
|
+
writeFileSync7(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
|
|
3124
3843
|
}
|
|
3125
3844
|
function isFirstUseToday() {
|
|
3126
3845
|
const last = getLastCheckDate();
|
|
@@ -3129,10 +3848,10 @@ function isFirstUseToday() {
|
|
|
3129
3848
|
return last !== today;
|
|
3130
3849
|
}
|
|
3131
3850
|
function fetchLatestVersion() {
|
|
3132
|
-
return new Promise((
|
|
3851
|
+
return new Promise((resolve3) => {
|
|
3133
3852
|
const req = get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { timeout: 5e3 }, (res) => {
|
|
3134
3853
|
if (res.statusCode !== 200) {
|
|
3135
|
-
|
|
3854
|
+
resolve3(null);
|
|
3136
3855
|
return;
|
|
3137
3856
|
}
|
|
3138
3857
|
let data = "";
|
|
@@ -3141,18 +3860,18 @@ function fetchLatestVersion() {
|
|
|
3141
3860
|
});
|
|
3142
3861
|
res.on("end", () => {
|
|
3143
3862
|
try {
|
|
3144
|
-
|
|
3863
|
+
resolve3(JSON.parse(data).version);
|
|
3145
3864
|
} catch {
|
|
3146
|
-
|
|
3865
|
+
resolve3(null);
|
|
3147
3866
|
}
|
|
3148
3867
|
});
|
|
3149
3868
|
});
|
|
3150
3869
|
req.on("error", () => {
|
|
3151
|
-
|
|
3870
|
+
resolve3(null);
|
|
3152
3871
|
});
|
|
3153
3872
|
req.on("timeout", () => {
|
|
3154
3873
|
req.destroy();
|
|
3155
|
-
|
|
3874
|
+
resolve3(null);
|
|
3156
3875
|
});
|
|
3157
3876
|
});
|
|
3158
3877
|
}
|
|
@@ -3225,7 +3944,7 @@ Examples:
|
|
|
3225
3944
|
function parseArgs(argv) {
|
|
3226
3945
|
const args = argv.slice(2);
|
|
3227
3946
|
let port2 = 4174;
|
|
3228
|
-
let dataDir2 =
|
|
3947
|
+
let dataDir2 = join12(process.cwd(), ".hotsheet");
|
|
3229
3948
|
let demo = null;
|
|
3230
3949
|
let forceUpdateCheck = false;
|
|
3231
3950
|
let noOpen = false;
|
|
@@ -3254,7 +3973,7 @@ function parseArgs(argv) {
|
|
|
3254
3973
|
}
|
|
3255
3974
|
break;
|
|
3256
3975
|
case "--data-dir":
|
|
3257
|
-
dataDir2 =
|
|
3976
|
+
dataDir2 = resolve2(args[++i]);
|
|
3258
3977
|
break;
|
|
3259
3978
|
case "--check-for-updates":
|
|
3260
3979
|
forceUpdateCheck = true;
|
|
@@ -3292,7 +4011,7 @@ async function main() {
|
|
|
3292
4011
|
}
|
|
3293
4012
|
process.exit(1);
|
|
3294
4013
|
}
|
|
3295
|
-
dataDir2 =
|
|
4014
|
+
dataDir2 = join12(tmpdir(), `hotsheet-demo-${demo}-${Date.now()}`);
|
|
3296
4015
|
console.log(`
|
|
3297
4016
|
DEMO MODE: ${scenario.label}
|
|
3298
4017
|
`);
|
|
@@ -3322,6 +4041,11 @@ async function main() {
|
|
|
3322
4041
|
AI tool skills created/updated for: ${updatedPlatforms.join(", ")}`);
|
|
3323
4042
|
console.log(" Restart your AI tool to pick up the new ticket creation skills.\n");
|
|
3324
4043
|
}
|
|
4044
|
+
Promise.resolve().then(() => (init_stats(), stats_exports)).then(async ({ recordDailySnapshot: recordDailySnapshot2, backfillSnapshots: backfillSnapshots2 }) => {
|
|
4045
|
+
await backfillSnapshots2();
|
|
4046
|
+
await recordDailySnapshot2();
|
|
4047
|
+
}).catch(() => {
|
|
4048
|
+
});
|
|
3325
4049
|
if (demo === null) {
|
|
3326
4050
|
initBackupScheduler(dataDir2);
|
|
3327
4051
|
}
|