hotsheet 0.4.0 → 0.5.1
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 +489 -196
- package/dist/client/app.global.js +38 -38
- package/dist/client/styles.css +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -163,68 +163,6 @@ var init_file_settings = __esm({
|
|
|
163
163
|
}
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
-
// src/gitignore.ts
|
|
167
|
-
var gitignore_exports = {};
|
|
168
|
-
__export(gitignore_exports, {
|
|
169
|
-
addHotsheetToGitignore: () => addHotsheetToGitignore,
|
|
170
|
-
ensureGitignore: () => ensureGitignore,
|
|
171
|
-
getGitRoot: () => getGitRoot,
|
|
172
|
-
isGitRepo: () => isGitRepo,
|
|
173
|
-
isHotsheetGitignored: () => isHotsheetGitignored
|
|
174
|
-
});
|
|
175
|
-
import { execSync } from "child_process";
|
|
176
|
-
import { appendFileSync, existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
177
|
-
import { join as join5 } from "path";
|
|
178
|
-
function isHotsheetGitignored(repoRoot) {
|
|
179
|
-
try {
|
|
180
|
-
execSync("git check-ignore -q .hotsheet", { cwd: repoRoot, stdio: "ignore" });
|
|
181
|
-
return true;
|
|
182
|
-
} catch {
|
|
183
|
-
return false;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
function isGitRepo(dir) {
|
|
187
|
-
try {
|
|
188
|
-
execSync("git rev-parse --is-inside-work-tree", { cwd: dir, stdio: "ignore" });
|
|
189
|
-
return true;
|
|
190
|
-
} catch {
|
|
191
|
-
return false;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
function getGitRoot(dir) {
|
|
195
|
-
try {
|
|
196
|
-
return execSync("git rev-parse --show-toplevel", { cwd: dir, encoding: "utf-8" }).trim();
|
|
197
|
-
} catch {
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
function addHotsheetToGitignore(repoRoot) {
|
|
202
|
-
const gitignorePath = join5(repoRoot, ".gitignore");
|
|
203
|
-
if (existsSync4(gitignorePath)) {
|
|
204
|
-
const content = readFileSync4(gitignorePath, "utf-8");
|
|
205
|
-
if (content.includes(".hotsheet")) return;
|
|
206
|
-
const prefix = content.endsWith("\n") ? "" : "\n";
|
|
207
|
-
appendFileSync(gitignorePath, `${prefix}.hotsheet/
|
|
208
|
-
`);
|
|
209
|
-
} else {
|
|
210
|
-
appendFileSync(gitignorePath, ".hotsheet/\n");
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
function ensureGitignore(cwd) {
|
|
214
|
-
if (!isGitRepo(cwd)) return;
|
|
215
|
-
const gitRoot = getGitRoot(cwd);
|
|
216
|
-
if (gitRoot === null) return;
|
|
217
|
-
if (!isHotsheetGitignored(gitRoot)) {
|
|
218
|
-
addHotsheetToGitignore(gitRoot);
|
|
219
|
-
console.log(" Added .hotsheet/ to .gitignore");
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
var init_gitignore = __esm({
|
|
223
|
-
"src/gitignore.ts"() {
|
|
224
|
-
"use strict";
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
|
|
228
166
|
// src/db/stats.ts
|
|
229
167
|
var stats_exports = {};
|
|
230
168
|
__export(stats_exports, {
|
|
@@ -420,10 +358,174 @@ var init_stats = __esm({
|
|
|
420
358
|
}
|
|
421
359
|
});
|
|
422
360
|
|
|
361
|
+
// src/gitignore.ts
|
|
362
|
+
var gitignore_exports = {};
|
|
363
|
+
__export(gitignore_exports, {
|
|
364
|
+
addHotsheetToGitignore: () => addHotsheetToGitignore,
|
|
365
|
+
ensureGitignore: () => ensureGitignore,
|
|
366
|
+
getGitRoot: () => getGitRoot,
|
|
367
|
+
isGitRepo: () => isGitRepo,
|
|
368
|
+
isHotsheetGitignored: () => isHotsheetGitignored
|
|
369
|
+
});
|
|
370
|
+
import { execSync } from "child_process";
|
|
371
|
+
import { appendFileSync, existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
372
|
+
import { join as join5 } from "path";
|
|
373
|
+
function isHotsheetGitignored(repoRoot) {
|
|
374
|
+
try {
|
|
375
|
+
execSync("git check-ignore -q .hotsheet", { cwd: repoRoot, stdio: "ignore" });
|
|
376
|
+
return true;
|
|
377
|
+
} catch {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function isGitRepo(dir) {
|
|
382
|
+
try {
|
|
383
|
+
execSync("git rev-parse --is-inside-work-tree", { cwd: dir, stdio: "ignore" });
|
|
384
|
+
return true;
|
|
385
|
+
} catch {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function getGitRoot(dir) {
|
|
390
|
+
try {
|
|
391
|
+
return execSync("git rev-parse --show-toplevel", { cwd: dir, encoding: "utf-8" }).trim();
|
|
392
|
+
} catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function addHotsheetToGitignore(repoRoot) {
|
|
397
|
+
const gitignorePath = join5(repoRoot, ".gitignore");
|
|
398
|
+
if (existsSync4(gitignorePath)) {
|
|
399
|
+
const content = readFileSync4(gitignorePath, "utf-8");
|
|
400
|
+
if (content.includes(".hotsheet")) return;
|
|
401
|
+
const prefix = content.endsWith("\n") ? "" : "\n";
|
|
402
|
+
appendFileSync(gitignorePath, `${prefix}.hotsheet/
|
|
403
|
+
`);
|
|
404
|
+
} else {
|
|
405
|
+
appendFileSync(gitignorePath, ".hotsheet/\n");
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function ensureGitignore(cwd) {
|
|
409
|
+
if (!isGitRepo(cwd)) return;
|
|
410
|
+
const gitRoot = getGitRoot(cwd);
|
|
411
|
+
if (gitRoot === null) return;
|
|
412
|
+
if (!isHotsheetGitignored(gitRoot)) {
|
|
413
|
+
addHotsheetToGitignore(gitRoot);
|
|
414
|
+
console.log(" Added .hotsheet/ to .gitignore");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
var init_gitignore = __esm({
|
|
418
|
+
"src/gitignore.ts"() {
|
|
419
|
+
"use strict";
|
|
420
|
+
}
|
|
421
|
+
});
|
|
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 { dirname, join as join8, resolve } from "path";
|
|
434
|
+
import { fileURLToPath } from "url";
|
|
435
|
+
function getChannelServerPath() {
|
|
436
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
437
|
+
const distPath = resolve(thisDir, "channel.js");
|
|
438
|
+
if (existsSync6(distPath)) {
|
|
439
|
+
return { command: "node", args: [distPath] };
|
|
440
|
+
}
|
|
441
|
+
const srcPath = resolve(thisDir, "channel.ts");
|
|
442
|
+
if (existsSync6(srcPath)) {
|
|
443
|
+
return { command: "npx", args: ["tsx", srcPath] };
|
|
444
|
+
}
|
|
445
|
+
return { command: "node", args: [distPath] };
|
|
446
|
+
}
|
|
447
|
+
function registerChannel(dataDir2) {
|
|
448
|
+
const cwd = process.cwd();
|
|
449
|
+
const mcpPath = join8(cwd, ".mcp.json");
|
|
450
|
+
const { command, args } = getChannelServerPath();
|
|
451
|
+
let config = {};
|
|
452
|
+
if (existsSync6(mcpPath)) {
|
|
453
|
+
try {
|
|
454
|
+
config = JSON.parse(readFileSync6(mcpPath, "utf-8"));
|
|
455
|
+
} catch {
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
459
|
+
config.mcpServers[MCP_SERVER_KEY] = {
|
|
460
|
+
command,
|
|
461
|
+
args: [...args, "--data-dir", dataDir2]
|
|
462
|
+
};
|
|
463
|
+
writeFileSync6(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
464
|
+
}
|
|
465
|
+
function unregisterChannel() {
|
|
466
|
+
const cwd = process.cwd();
|
|
467
|
+
const mcpPath = join8(cwd, ".mcp.json");
|
|
468
|
+
if (!existsSync6(mcpPath)) return;
|
|
469
|
+
try {
|
|
470
|
+
const config = JSON.parse(readFileSync6(mcpPath, "utf-8"));
|
|
471
|
+
if (config.mcpServers?.[MCP_SERVER_KEY]) {
|
|
472
|
+
delete config.mcpServers[MCP_SERVER_KEY];
|
|
473
|
+
writeFileSync6(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
474
|
+
}
|
|
475
|
+
} catch {
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function getChannelPort(dataDir2) {
|
|
479
|
+
try {
|
|
480
|
+
const portStr = readFileSync6(join8(dataDir2, "channel-port"), "utf-8").trim();
|
|
481
|
+
const port2 = parseInt(portStr, 10);
|
|
482
|
+
return isNaN(port2) ? null : port2;
|
|
483
|
+
} catch {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async function isChannelAlive(dataDir2) {
|
|
488
|
+
const port2 = getChannelPort(dataDir2);
|
|
489
|
+
if (!port2) return false;
|
|
490
|
+
try {
|
|
491
|
+
const res = await fetch(`http://127.0.0.1:${port2}/health`);
|
|
492
|
+
const data = await res.json();
|
|
493
|
+
return data.ok === true;
|
|
494
|
+
} catch {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async function triggerChannel(dataDir2, serverPort, message) {
|
|
499
|
+
const port2 = getChannelPort(dataDir2);
|
|
500
|
+
if (!port2) return false;
|
|
501
|
+
const defaultMessage = [
|
|
502
|
+
"Process the Hot Sheet worklist. Run /hotsheet to work through the current Up Next items.",
|
|
503
|
+
"",
|
|
504
|
+
`When you are completely finished processing all items (or if the worklist was empty), signal completion by running:`,
|
|
505
|
+
`curl -s -X POST http://localhost:${serverPort}/api/channel/done`
|
|
506
|
+
].join("\n");
|
|
507
|
+
try {
|
|
508
|
+
const res = await fetch(`http://127.0.0.1:${port2}/trigger`, {
|
|
509
|
+
method: "POST",
|
|
510
|
+
body: message || defaultMessage
|
|
511
|
+
});
|
|
512
|
+
return res.ok;
|
|
513
|
+
} catch {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
var MCP_SERVER_KEY;
|
|
518
|
+
var init_channel_config = __esm({
|
|
519
|
+
"src/channel-config.ts"() {
|
|
520
|
+
"use strict";
|
|
521
|
+
MCP_SERVER_KEY = "hotsheet-channel";
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
423
525
|
// src/cli.ts
|
|
424
526
|
import { mkdirSync as mkdirSync6 } from "fs";
|
|
425
527
|
import { tmpdir } from "os";
|
|
426
|
-
import { join as
|
|
528
|
+
import { join as join12, resolve as resolve2 } from "path";
|
|
427
529
|
|
|
428
530
|
// src/backup.ts
|
|
429
531
|
init_connection();
|
|
@@ -712,23 +814,23 @@ function parseNotes(raw) {
|
|
|
712
814
|
}
|
|
713
815
|
return [{ id: generateNoteId(), text: raw, created_at: (/* @__PURE__ */ new Date()).toISOString() }];
|
|
714
816
|
}
|
|
715
|
-
async function editNote(ticketId,
|
|
817
|
+
async function editNote(ticketId, noteId2, text) {
|
|
716
818
|
const db2 = await getDb();
|
|
717
819
|
const result = await db2.query(`SELECT notes FROM tickets WHERE id = $1`, [ticketId]);
|
|
718
820
|
if (result.rows.length === 0) return null;
|
|
719
821
|
const notes = parseNotes(result.rows[0].notes);
|
|
720
|
-
const note = notes.find((n) => n.id ===
|
|
822
|
+
const note = notes.find((n) => n.id === noteId2);
|
|
721
823
|
if (!note) return null;
|
|
722
824
|
note.text = text;
|
|
723
825
|
await db2.query(`UPDATE tickets SET notes = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(notes), ticketId]);
|
|
724
826
|
return notes;
|
|
725
827
|
}
|
|
726
|
-
async function deleteNote(ticketId,
|
|
828
|
+
async function deleteNote(ticketId, noteId2) {
|
|
727
829
|
const db2 = await getDb();
|
|
728
830
|
const result = await db2.query(`SELECT notes FROM tickets WHERE id = $1`, [ticketId]);
|
|
729
831
|
if (result.rows.length === 0) return null;
|
|
730
832
|
const notes = parseNotes(result.rows[0].notes);
|
|
731
|
-
const idx = notes.findIndex((n) => n.id ===
|
|
833
|
+
const idx = notes.findIndex((n) => n.id === noteId2);
|
|
732
834
|
if (idx === -1) return null;
|
|
733
835
|
notes.splice(idx, 1);
|
|
734
836
|
await db2.query(`UPDATE tickets SET notes = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(notes), ticketId]);
|
|
@@ -1254,20 +1356,26 @@ init_connection();
|
|
|
1254
1356
|
var DEMO_SCENARIOS = [
|
|
1255
1357
|
{ id: 1, label: "Main UI \u2014 all tickets with detail panel" },
|
|
1256
1358
|
{ id: 2, label: "Quick entry \u2014 bullet-list ticket creation" },
|
|
1257
|
-
{ id: 3, label: "Sidebar filtering \u2014
|
|
1359
|
+
{ id: 3, label: "Sidebar filtering \u2014 custom views and categories" },
|
|
1258
1360
|
{ id: 4, label: "AI worklist \u2014 Up Next tickets with notes" },
|
|
1259
1361
|
{ id: 5, label: "Batch operations \u2014 multi-select toolbar" },
|
|
1260
|
-
{ id: 6, label: "Detail panel \u2014 bottom orientation with notes" },
|
|
1261
|
-
{ id: 7, label: "Column view \u2014 kanban board by status" }
|
|
1362
|
+
{ id: 6, label: "Detail panel \u2014 bottom orientation with tags and notes" },
|
|
1363
|
+
{ id: 7, label: "Column view \u2014 kanban board by status" },
|
|
1364
|
+
{ id: 8, label: "Dashboard \u2014 stats and charts" }
|
|
1262
1365
|
];
|
|
1263
1366
|
function daysAgo(days) {
|
|
1264
1367
|
const d = /* @__PURE__ */ new Date();
|
|
1265
1368
|
d.setTime(d.getTime() - days * 24 * 60 * 60 * 1e3);
|
|
1266
1369
|
return d.toISOString();
|
|
1267
1370
|
}
|
|
1371
|
+
var noteId = 0;
|
|
1268
1372
|
function notesJson(entries) {
|
|
1269
1373
|
if (entries.length === 0) return "";
|
|
1270
|
-
return JSON.stringify(entries.map((e) => ({
|
|
1374
|
+
return JSON.stringify(entries.map((e) => ({
|
|
1375
|
+
id: `n_demo_${noteId++}`,
|
|
1376
|
+
text: e.text,
|
|
1377
|
+
created_at: daysAgo(e.days_ago)
|
|
1378
|
+
})));
|
|
1271
1379
|
}
|
|
1272
1380
|
var SCENARIO_1 = [
|
|
1273
1381
|
{
|
|
@@ -1277,6 +1385,7 @@ var SCENARIO_1 = [
|
|
|
1277
1385
|
priority: "highest",
|
|
1278
1386
|
status: "started",
|
|
1279
1387
|
up_next: true,
|
|
1388
|
+
tags: ["checkout", "shipping"],
|
|
1280
1389
|
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 }]),
|
|
1281
1390
|
days_ago: 5,
|
|
1282
1391
|
updated_ago: 0.5
|
|
@@ -1288,6 +1397,7 @@ var SCENARIO_1 = [
|
|
|
1288
1397
|
priority: "high",
|
|
1289
1398
|
status: "not_started",
|
|
1290
1399
|
up_next: true,
|
|
1400
|
+
tags: ["ux", "product-pages"],
|
|
1291
1401
|
notes: "",
|
|
1292
1402
|
days_ago: 4,
|
|
1293
1403
|
updated_ago: 4
|
|
@@ -1299,6 +1409,7 @@ var SCENARIO_1 = [
|
|
|
1299
1409
|
priority: "high",
|
|
1300
1410
|
status: "started",
|
|
1301
1411
|
up_next: true,
|
|
1412
|
+
tags: ["infrastructure", "devops"],
|
|
1302
1413
|
notes: notesJson([{ text: "Created the backup script and IAM role. Testing the S3 lifecycle policy for retention.", days_ago: 1 }]),
|
|
1303
1414
|
days_ago: 7,
|
|
1304
1415
|
updated_ago: 1
|
|
@@ -1310,6 +1421,7 @@ var SCENARIO_1 = [
|
|
|
1310
1421
|
priority: "high",
|
|
1311
1422
|
status: "not_started",
|
|
1312
1423
|
up_next: true,
|
|
1424
|
+
tags: ["payments"],
|
|
1313
1425
|
notes: "",
|
|
1314
1426
|
days_ago: 3,
|
|
1315
1427
|
updated_ago: 3
|
|
@@ -1321,6 +1433,7 @@ var SCENARIO_1 = [
|
|
|
1321
1433
|
priority: "default",
|
|
1322
1434
|
status: "not_started",
|
|
1323
1435
|
up_next: false,
|
|
1436
|
+
tags: ["performance", "images"],
|
|
1324
1437
|
notes: "",
|
|
1325
1438
|
days_ago: 6,
|
|
1326
1439
|
updated_ago: 6
|
|
@@ -1332,6 +1445,7 @@ var SCENARIO_1 = [
|
|
|
1332
1445
|
priority: "default",
|
|
1333
1446
|
status: "not_started",
|
|
1334
1447
|
up_next: false,
|
|
1448
|
+
tags: ["checkout", "accounts"],
|
|
1335
1449
|
notes: "",
|
|
1336
1450
|
days_ago: 10,
|
|
1337
1451
|
updated_ago: 10
|
|
@@ -1343,6 +1457,7 @@ var SCENARIO_1 = [
|
|
|
1343
1457
|
priority: "default",
|
|
1344
1458
|
status: "started",
|
|
1345
1459
|
up_next: false,
|
|
1460
|
+
tags: ["tax", "eu", "compliance"],
|
|
1346
1461
|
notes: "",
|
|
1347
1462
|
days_ago: 8,
|
|
1348
1463
|
updated_ago: 2
|
|
@@ -1354,6 +1469,7 @@ var SCENARIO_1 = [
|
|
|
1354
1469
|
priority: "default",
|
|
1355
1470
|
status: "completed",
|
|
1356
1471
|
up_next: false,
|
|
1472
|
+
tags: ["docs", "api"],
|
|
1357
1473
|
notes: notesJson([{ text: "Documented all 12 order endpoints with examples. Published to /docs.", days_ago: 1 }]),
|
|
1358
1474
|
days_ago: 12,
|
|
1359
1475
|
updated_ago: 1,
|
|
@@ -1366,6 +1482,7 @@ var SCENARIO_1 = [
|
|
|
1366
1482
|
priority: "highest",
|
|
1367
1483
|
status: "verified",
|
|
1368
1484
|
up_next: false,
|
|
1485
|
+
tags: ["mobile", "api"],
|
|
1369
1486
|
notes: notesJson([
|
|
1370
1487
|
{ text: "Added CORS middleware with correct origins. Tested against staging with the mobile app builds.", days_ago: 3 },
|
|
1371
1488
|
{ text: "Verified fix is working in production. No more CORS errors in mobile app error logs.", days_ago: 2 }
|
|
@@ -1382,6 +1499,7 @@ var SCENARIO_1 = [
|
|
|
1382
1499
|
priority: "low",
|
|
1383
1500
|
status: "not_started",
|
|
1384
1501
|
up_next: false,
|
|
1502
|
+
tags: ["ux", "theming"],
|
|
1385
1503
|
notes: "",
|
|
1386
1504
|
days_ago: 15,
|
|
1387
1505
|
updated_ago: 15
|
|
@@ -1393,6 +1511,7 @@ var SCENARIO_1 = [
|
|
|
1393
1511
|
priority: "default",
|
|
1394
1512
|
status: "completed",
|
|
1395
1513
|
up_next: false,
|
|
1514
|
+
tags: ["infrastructure", "database"],
|
|
1396
1515
|
notes: notesJson([{ text: "Migrated to pg pool with max 20 connections. Load tested successfully at 500 concurrent requests.", days_ago: 4 }]),
|
|
1397
1516
|
days_ago: 18,
|
|
1398
1517
|
updated_ago: 4,
|
|
@@ -1405,6 +1524,7 @@ var SCENARIO_1 = [
|
|
|
1405
1524
|
priority: "lowest",
|
|
1406
1525
|
status: "not_started",
|
|
1407
1526
|
up_next: false,
|
|
1527
|
+
tags: ["frontend", "seo"],
|
|
1408
1528
|
notes: "",
|
|
1409
1529
|
days_ago: 20,
|
|
1410
1530
|
updated_ago: 20
|
|
@@ -1418,6 +1538,7 @@ var SCENARIO_2 = [
|
|
|
1418
1538
|
priority: "high",
|
|
1419
1539
|
status: "not_started",
|
|
1420
1540
|
up_next: true,
|
|
1541
|
+
tags: ["auth"],
|
|
1421
1542
|
notes: "",
|
|
1422
1543
|
days_ago: 2,
|
|
1423
1544
|
updated_ago: 2
|
|
@@ -1429,6 +1550,7 @@ var SCENARIO_2 = [
|
|
|
1429
1550
|
priority: "default",
|
|
1430
1551
|
status: "not_started",
|
|
1431
1552
|
up_next: false,
|
|
1553
|
+
tags: ["admin"],
|
|
1432
1554
|
notes: "",
|
|
1433
1555
|
days_ago: 3,
|
|
1434
1556
|
updated_ago: 3
|
|
@@ -1440,6 +1562,7 @@ var SCENARIO_2 = [
|
|
|
1440
1562
|
priority: "default",
|
|
1441
1563
|
status: "started",
|
|
1442
1564
|
up_next: false,
|
|
1565
|
+
tags: ["maintenance"],
|
|
1443
1566
|
notes: "",
|
|
1444
1567
|
days_ago: 1,
|
|
1445
1568
|
updated_ago: 0.5
|
|
@@ -1453,17 +1576,19 @@ var SCENARIO_3 = [
|
|
|
1453
1576
|
priority: "highest",
|
|
1454
1577
|
status: "started",
|
|
1455
1578
|
up_next: true,
|
|
1579
|
+
tags: ["checkout", "pricing"],
|
|
1456
1580
|
notes: "",
|
|
1457
1581
|
days_ago: 3,
|
|
1458
1582
|
updated_ago: 1
|
|
1459
1583
|
},
|
|
1460
1584
|
{
|
|
1461
1585
|
title: "Search returns stale results after product update",
|
|
1462
|
-
details: "The search index isn't being refreshed when product details change.
|
|
1586
|
+
details: "The search index isn't being refreshed when product details change.",
|
|
1463
1587
|
category: "bug",
|
|
1464
1588
|
priority: "high",
|
|
1465
1589
|
status: "not_started",
|
|
1466
1590
|
up_next: true,
|
|
1591
|
+
tags: ["search"],
|
|
1467
1592
|
notes: "",
|
|
1468
1593
|
days_ago: 5,
|
|
1469
1594
|
updated_ago: 5
|
|
@@ -1475,17 +1600,19 @@ var SCENARIO_3 = [
|
|
|
1475
1600
|
priority: "default",
|
|
1476
1601
|
status: "not_started",
|
|
1477
1602
|
up_next: false,
|
|
1603
|
+
tags: ["notifications"],
|
|
1478
1604
|
notes: "",
|
|
1479
1605
|
days_ago: 7,
|
|
1480
1606
|
updated_ago: 7
|
|
1481
1607
|
},
|
|
1482
1608
|
{
|
|
1483
1609
|
title: "Implement real-time inventory tracking",
|
|
1484
|
-
details:
|
|
1610
|
+
details: "Use WebSocket connections to push stock level changes to the product page.",
|
|
1485
1611
|
category: "feature",
|
|
1486
1612
|
priority: "high",
|
|
1487
1613
|
status: "started",
|
|
1488
1614
|
up_next: true,
|
|
1615
|
+
tags: ["real-time", "inventory"],
|
|
1489
1616
|
notes: notesJson([{ text: "WebSocket server is set up. Working on the client-side stock badge component.", days_ago: 0.5 }]),
|
|
1490
1617
|
days_ago: 6,
|
|
1491
1618
|
updated_ago: 0.5
|
|
@@ -1497,84 +1624,67 @@ var SCENARIO_3 = [
|
|
|
1497
1624
|
priority: "default",
|
|
1498
1625
|
status: "not_started",
|
|
1499
1626
|
up_next: false,
|
|
1627
|
+
tags: ["social"],
|
|
1500
1628
|
notes: "",
|
|
1501
1629
|
days_ago: 9,
|
|
1502
1630
|
updated_ago: 9
|
|
1503
1631
|
},
|
|
1504
1632
|
{
|
|
1505
1633
|
title: "Product video support on detail pages",
|
|
1506
|
-
details: "Allow merchants to upload product videos alongside photos.
|
|
1634
|
+
details: "Allow merchants to upload product videos alongside photos.",
|
|
1507
1635
|
category: "feature",
|
|
1508
1636
|
priority: "low",
|
|
1509
1637
|
status: "not_started",
|
|
1510
1638
|
up_next: false,
|
|
1639
|
+
tags: ["media"],
|
|
1511
1640
|
notes: "",
|
|
1512
1641
|
days_ago: 12,
|
|
1513
1642
|
updated_ago: 12
|
|
1514
1643
|
},
|
|
1515
1644
|
{
|
|
1516
1645
|
title: "Migrate image storage to CDN",
|
|
1517
|
-
details: "Move product images from local disk to CloudFront.
|
|
1646
|
+
details: "Move product images from local disk to CloudFront.",
|
|
1518
1647
|
category: "task",
|
|
1519
1648
|
priority: "high",
|
|
1520
1649
|
status: "started",
|
|
1521
1650
|
up_next: false,
|
|
1651
|
+
tags: ["infrastructure", "images"],
|
|
1522
1652
|
notes: "",
|
|
1523
1653
|
days_ago: 4,
|
|
1524
1654
|
updated_ago: 2
|
|
1525
1655
|
},
|
|
1526
|
-
{
|
|
1527
|
-
title: "Set up error monitoring with Sentry",
|
|
1528
|
-
details: "Configure Sentry for both server and client-side error tracking. Set up alert rules for critical errors.",
|
|
1529
|
-
category: "task",
|
|
1530
|
-
priority: "default",
|
|
1531
|
-
status: "completed",
|
|
1532
|
-
up_next: false,
|
|
1533
|
-
notes: notesJson([{ text: "Sentry configured for Node.js backend and React frontend. Alert rules set for 5xx errors.", days_ago: 3 }]),
|
|
1534
|
-
days_ago: 10,
|
|
1535
|
-
updated_ago: 3,
|
|
1536
|
-
completed_ago: 3
|
|
1537
|
-
},
|
|
1538
1656
|
{
|
|
1539
1657
|
title: "Support guest checkout without account creation",
|
|
1540
|
-
details: "
|
|
1658
|
+
details: "Many users abandon at the registration step. Allow checkout with just email.",
|
|
1541
1659
|
category: "requirement_change",
|
|
1542
1660
|
priority: "high",
|
|
1543
1661
|
status: "not_started",
|
|
1544
1662
|
up_next: true,
|
|
1663
|
+
tags: ["checkout", "conversion"],
|
|
1545
1664
|
notes: "",
|
|
1546
1665
|
days_ago: 2,
|
|
1547
1666
|
updated_ago: 2
|
|
1548
1667
|
},
|
|
1549
|
-
{
|
|
1550
|
-
title: "Update return policy to 60-day window",
|
|
1551
|
-
details: "Legal team requires extending the return window from 30 to 60 days. Update all customer-facing copy and the returns API logic.",
|
|
1552
|
-
category: "requirement_change",
|
|
1553
|
-
priority: "default",
|
|
1554
|
-
status: "started",
|
|
1555
|
-
up_next: false,
|
|
1556
|
-
notes: "",
|
|
1557
|
-
days_ago: 8,
|
|
1558
|
-
updated_ago: 3
|
|
1559
|
-
},
|
|
1560
1668
|
{
|
|
1561
1669
|
title: "Compare Redis vs Memcached for session storage",
|
|
1562
|
-
details: "
|
|
1670
|
+
details: "Evaluate Redis and Memcached for persistence, speed, and ops complexity.",
|
|
1563
1671
|
category: "investigation",
|
|
1564
1672
|
priority: "high",
|
|
1565
1673
|
status: "not_started",
|
|
1566
1674
|
up_next: false,
|
|
1675
|
+
tags: ["infrastructure"],
|
|
1567
1676
|
notes: "",
|
|
1568
1677
|
days_ago: 6,
|
|
1569
1678
|
updated_ago: 6
|
|
1570
1679
|
},
|
|
1571
1680
|
{
|
|
1572
1681
|
title: "Analyze mobile conversion drop-off funnel",
|
|
1573
|
-
details: "Mobile users convert at 1.2% vs 3.8% desktop. Investigate where
|
|
1682
|
+
details: "Mobile users convert at 1.2% vs 3.8% desktop. Investigate where users drop off.",
|
|
1574
1683
|
category: "investigation",
|
|
1575
1684
|
priority: "default",
|
|
1576
1685
|
status: "not_started",
|
|
1577
1686
|
up_next: false,
|
|
1687
|
+
tags: ["analytics", "mobile"],
|
|
1578
1688
|
notes: "",
|
|
1579
1689
|
days_ago: 11,
|
|
1580
1690
|
updated_ago: 11
|
|
@@ -1588,6 +1698,7 @@ var SCENARIO_4 = [
|
|
|
1588
1698
|
priority: "highest",
|
|
1589
1699
|
status: "started",
|
|
1590
1700
|
up_next: true,
|
|
1701
|
+
tags: ["concurrency", "orders"],
|
|
1591
1702
|
notes: notesJson([
|
|
1592
1703
|
{ 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 },
|
|
1593
1704
|
{ text: "Implemented SELECT ... FOR UPDATE on the inventory row. Running stress tests to confirm the fix holds under load.", days_ago: 0.3 }
|
|
@@ -1602,6 +1713,7 @@ var SCENARIO_4 = [
|
|
|
1602
1713
|
priority: "high",
|
|
1603
1714
|
status: "not_started",
|
|
1604
1715
|
up_next: true,
|
|
1716
|
+
tags: ["webhooks", "api"],
|
|
1605
1717
|
notes: "",
|
|
1606
1718
|
days_ago: 3,
|
|
1607
1719
|
updated_ago: 3
|
|
@@ -1613,6 +1725,7 @@ var SCENARIO_4 = [
|
|
|
1613
1725
|
priority: "high",
|
|
1614
1726
|
status: "not_started",
|
|
1615
1727
|
up_next: true,
|
|
1728
|
+
tags: ["security", "api"],
|
|
1616
1729
|
notes: "",
|
|
1617
1730
|
days_ago: 5,
|
|
1618
1731
|
updated_ago: 5
|
|
@@ -1624,31 +1737,34 @@ var SCENARIO_4 = [
|
|
|
1624
1737
|
priority: "default",
|
|
1625
1738
|
status: "not_started",
|
|
1626
1739
|
up_next: true,
|
|
1740
|
+
tags: ["pricing"],
|
|
1627
1741
|
notes: "",
|
|
1628
1742
|
days_ago: 6,
|
|
1629
1743
|
updated_ago: 6
|
|
1630
1744
|
},
|
|
1631
1745
|
{
|
|
1632
1746
|
title: "Evaluate caching strategies for product catalog",
|
|
1633
|
-
details: "Product pages are slow under load. Investigate Redis caching, CDN edge caching, and stale-while-revalidate patterns.
|
|
1747
|
+
details: "Product pages are slow under load. Investigate Redis caching, CDN edge caching, and stale-while-revalidate patterns.",
|
|
1634
1748
|
category: "investigation",
|
|
1635
1749
|
priority: "default",
|
|
1636
1750
|
status: "not_started",
|
|
1637
1751
|
up_next: true,
|
|
1752
|
+
tags: ["performance", "caching"],
|
|
1638
1753
|
notes: "",
|
|
1639
1754
|
days_ago: 7,
|
|
1640
1755
|
updated_ago: 7
|
|
1641
1756
|
},
|
|
1642
1757
|
{
|
|
1643
1758
|
title: "Add bulk product import from CSV",
|
|
1644
|
-
details: "Merchants need to upload a CSV of products to create/update inventory in batch.
|
|
1759
|
+
details: "Merchants need to upload a CSV of products to create/update inventory in batch.",
|
|
1645
1760
|
category: "feature",
|
|
1646
1761
|
priority: "low",
|
|
1647
1762
|
status: "completed",
|
|
1648
1763
|
up_next: false,
|
|
1764
|
+
tags: ["admin", "import"],
|
|
1649
1765
|
notes: notesJson([
|
|
1650
1766
|
{ text: "Implemented CSV parser using papaparse. Supports create and update modes with duplicate detection by SKU.", days_ago: 3 },
|
|
1651
|
-
{ text: "Added validation for required fields (name, price, SKU) and friendly error messages with row numbers
|
|
1767
|
+
{ text: "Added validation for required fields (name, price, SKU) and friendly error messages with row numbers.", days_ago: 2 }
|
|
1652
1768
|
]),
|
|
1653
1769
|
days_ago: 10,
|
|
1654
1770
|
updated_ago: 2,
|
|
@@ -1656,14 +1772,15 @@ var SCENARIO_4 = [
|
|
|
1656
1772
|
},
|
|
1657
1773
|
{
|
|
1658
1774
|
title: "Normalize database schema for customer addresses",
|
|
1659
|
-
details: "Addresses are currently embedded as JSON in the customers table. Extract to a separate addresses table
|
|
1775
|
+
details: "Addresses are currently embedded as JSON in the customers table. Extract to a separate addresses table.",
|
|
1660
1776
|
category: "task",
|
|
1661
1777
|
priority: "default",
|
|
1662
1778
|
status: "verified",
|
|
1663
1779
|
up_next: false,
|
|
1780
|
+
tags: ["database", "schema"],
|
|
1664
1781
|
notes: notesJson([
|
|
1665
1782
|
{ text: "Created migration to extract addresses into a new table. Backfilled 12,400 existing address records.", days_ago: 5 },
|
|
1666
|
-
{ text: "Verified the migration ran correctly. All address lookups use the new table.
|
|
1783
|
+
{ text: "Verified the migration ran correctly. All address lookups use the new table.", days_ago: 3 }
|
|
1667
1784
|
]),
|
|
1668
1785
|
days_ago: 14,
|
|
1669
1786
|
updated_ago: 3,
|
|
@@ -1674,55 +1791,60 @@ var SCENARIO_4 = [
|
|
|
1674
1791
|
var SCENARIO_5 = [
|
|
1675
1792
|
{
|
|
1676
1793
|
title: "Fix email template rendering in Outlook",
|
|
1677
|
-
details: "Order confirmation emails break in Outlook due to unsupported CSS flexbox.
|
|
1794
|
+
details: "Order confirmation emails break in Outlook due to unsupported CSS flexbox.",
|
|
1678
1795
|
category: "bug",
|
|
1679
1796
|
priority: "default",
|
|
1680
1797
|
status: "not_started",
|
|
1681
1798
|
up_next: false,
|
|
1799
|
+
tags: ["email"],
|
|
1682
1800
|
notes: "",
|
|
1683
1801
|
days_ago: 3,
|
|
1684
1802
|
updated_ago: 3
|
|
1685
1803
|
},
|
|
1686
1804
|
{
|
|
1687
1805
|
title: "Handle timeout on third-party shipping rate API",
|
|
1688
|
-
details: "When the shipping provider API times out,
|
|
1806
|
+
details: "When the shipping provider API times out, show a retry prompt instead of a 500 error.",
|
|
1689
1807
|
category: "bug",
|
|
1690
1808
|
priority: "default",
|
|
1691
1809
|
status: "not_started",
|
|
1692
1810
|
up_next: false,
|
|
1811
|
+
tags: ["shipping", "error-handling"],
|
|
1693
1812
|
notes: "",
|
|
1694
1813
|
days_ago: 4,
|
|
1695
1814
|
updated_ago: 4
|
|
1696
1815
|
},
|
|
1697
1816
|
{
|
|
1698
1817
|
title: "Fix pagination on search results page",
|
|
1699
|
-
details: "Page 2+ of search results shows duplicate items.
|
|
1818
|
+
details: "Page 2+ of search results shows duplicate items.",
|
|
1700
1819
|
category: "bug",
|
|
1701
1820
|
priority: "default",
|
|
1702
1821
|
status: "not_started",
|
|
1703
1822
|
up_next: false,
|
|
1823
|
+
tags: ["search"],
|
|
1704
1824
|
notes: "",
|
|
1705
1825
|
days_ago: 5,
|
|
1706
1826
|
updated_ago: 5
|
|
1707
1827
|
},
|
|
1708
1828
|
{
|
|
1709
1829
|
title: "Cart badge count not updating after item removal",
|
|
1710
|
-
details: "The header cart icon shows the old count until a full page refresh.
|
|
1830
|
+
details: "The header cart icon shows the old count until a full page refresh.",
|
|
1711
1831
|
category: "bug",
|
|
1712
1832
|
priority: "high",
|
|
1713
1833
|
status: "not_started",
|
|
1714
1834
|
up_next: false,
|
|
1835
|
+
tags: ["cart", "ui"],
|
|
1715
1836
|
notes: "",
|
|
1716
1837
|
days_ago: 2,
|
|
1717
1838
|
updated_ago: 2
|
|
1718
1839
|
},
|
|
1719
1840
|
{
|
|
1720
1841
|
title: "Add order tracking page for customers",
|
|
1721
|
-
details: "Customers need a page showing shipment status, tracking number, and estimated delivery.
|
|
1842
|
+
details: "Customers need a page showing shipment status, tracking number, and estimated delivery.",
|
|
1722
1843
|
category: "feature",
|
|
1723
1844
|
priority: "default",
|
|
1724
1845
|
status: "not_started",
|
|
1725
1846
|
up_next: false,
|
|
1847
|
+
tags: ["orders", "ux"],
|
|
1726
1848
|
notes: "",
|
|
1727
1849
|
days_ago: 6,
|
|
1728
1850
|
updated_ago: 6
|
|
@@ -1734,17 +1856,19 @@ var SCENARIO_5 = [
|
|
|
1734
1856
|
priority: "default",
|
|
1735
1857
|
status: "not_started",
|
|
1736
1858
|
up_next: false,
|
|
1859
|
+
tags: ["admin", "reviews"],
|
|
1737
1860
|
notes: "",
|
|
1738
1861
|
days_ago: 7,
|
|
1739
1862
|
updated_ago: 7
|
|
1740
1863
|
},
|
|
1741
1864
|
{
|
|
1742
1865
|
title: "Add rate limiting to public API endpoints",
|
|
1743
|
-
details: "Protect against abuse with per-IP rate limiting.
|
|
1866
|
+
details: "Protect against abuse with per-IP rate limiting. Target: 100 req/min anonymous, 500 authenticated.",
|
|
1744
1867
|
category: "task",
|
|
1745
1868
|
priority: "default",
|
|
1746
1869
|
status: "not_started",
|
|
1747
1870
|
up_next: false,
|
|
1871
|
+
tags: ["security", "api"],
|
|
1748
1872
|
notes: "",
|
|
1749
1873
|
days_ago: 8,
|
|
1750
1874
|
updated_ago: 8
|
|
@@ -1756,6 +1880,7 @@ var SCENARIO_5 = [
|
|
|
1756
1880
|
priority: "default",
|
|
1757
1881
|
status: "not_started",
|
|
1758
1882
|
up_next: false,
|
|
1883
|
+
tags: ["infrastructure", "devops"],
|
|
1759
1884
|
notes: "",
|
|
1760
1885
|
days_ago: 9,
|
|
1761
1886
|
updated_ago: 9
|
|
@@ -1767,6 +1892,7 @@ var SCENARIO_5 = [
|
|
|
1767
1892
|
priority: "low",
|
|
1768
1893
|
status: "not_started",
|
|
1769
1894
|
up_next: false,
|
|
1895
|
+
tags: ["cleanup"],
|
|
1770
1896
|
notes: "",
|
|
1771
1897
|
days_ago: 12,
|
|
1772
1898
|
updated_ago: 12
|
|
@@ -1778,6 +1904,7 @@ var SCENARIO_5 = [
|
|
|
1778
1904
|
priority: "low",
|
|
1779
1905
|
status: "not_started",
|
|
1780
1906
|
up_next: false,
|
|
1907
|
+
tags: ["cleanup", "database"],
|
|
1781
1908
|
notes: "",
|
|
1782
1909
|
days_ago: 14,
|
|
1783
1910
|
updated_ago: 14
|
|
@@ -1791,6 +1918,7 @@ var SCENARIO_6 = [
|
|
|
1791
1918
|
priority: "highest",
|
|
1792
1919
|
status: "started",
|
|
1793
1920
|
up_next: true,
|
|
1921
|
+
tags: ["real-time", "orders", "websocket"],
|
|
1794
1922
|
notes: notesJson([
|
|
1795
1923
|
{ text: "Set up the WebSocket server using ws library. Basic connection lifecycle working \u2014 connect, heartbeat, disconnect with cleanup.", days_ago: 3 },
|
|
1796
1924
|
{ 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 },
|
|
@@ -1806,6 +1934,7 @@ var SCENARIO_6 = [
|
|
|
1806
1934
|
priority: "high",
|
|
1807
1935
|
status: "started",
|
|
1808
1936
|
up_next: true,
|
|
1937
|
+
tags: ["performance", "memory", "search"],
|
|
1809
1938
|
notes: notesJson([{ text: "Heap snapshot shows the BatchProcessor holding references to completed batches. The onComplete callbacks are never cleaned up.", days_ago: 1 }]),
|
|
1810
1939
|
days_ago: 5,
|
|
1811
1940
|
updated_ago: 1
|
|
@@ -1817,17 +1946,19 @@ var SCENARIO_6 = [
|
|
|
1817
1946
|
priority: "high",
|
|
1818
1947
|
status: "not_started",
|
|
1819
1948
|
up_next: true,
|
|
1949
|
+
tags: ["testing", "payments"],
|
|
1820
1950
|
notes: "",
|
|
1821
1951
|
days_ago: 4,
|
|
1822
1952
|
updated_ago: 4
|
|
1823
1953
|
},
|
|
1824
1954
|
{
|
|
1825
1955
|
title: "Add product recommendations based on purchase history",
|
|
1826
|
-
details: 'Show "Customers also bought" recommendations on product pages using collaborative filtering
|
|
1956
|
+
details: 'Show "Customers also bought" recommendations on product pages using collaborative filtering.',
|
|
1827
1957
|
category: "feature",
|
|
1828
1958
|
priority: "default",
|
|
1829
1959
|
status: "completed",
|
|
1830
1960
|
up_next: false,
|
|
1961
|
+
tags: ["ml", "recommendations", "product-pages"],
|
|
1831
1962
|
notes: notesJson([
|
|
1832
1963
|
{ text: "Implemented a simple collaborative filtering algorithm. Computes item-item similarity from co-purchase frequency in the last 90 days.", days_ago: 5 },
|
|
1833
1964
|
{ text: "Added the recommendations API endpoint and the product page widget. Limited to 4 recommendations. Recalculation runs nightly via cron.", days_ago: 3 }
|
|
@@ -1838,14 +1969,15 @@ var SCENARIO_6 = [
|
|
|
1838
1969
|
},
|
|
1839
1970
|
{
|
|
1840
1971
|
title: "Migrate static assets to CDN",
|
|
1841
|
-
details: "Product images, CSS, and JS bundles should be served from CloudFront.
|
|
1972
|
+
details: "Product images, CSS, and JS bundles should be served from CloudFront.",
|
|
1842
1973
|
category: "task",
|
|
1843
1974
|
priority: "default",
|
|
1844
1975
|
status: "verified",
|
|
1845
1976
|
up_next: false,
|
|
1977
|
+
tags: ["infrastructure", "cdn", "performance"],
|
|
1846
1978
|
notes: notesJson([
|
|
1847
|
-
{ text: "Configured CloudFront distribution with S3 origin. Migrated all product images (42GB)
|
|
1848
|
-
{ text: "Updated asset URLs
|
|
1979
|
+
{ text: "Configured CloudFront distribution with S3 origin. Migrated all product images (42GB).", days_ago: 7 },
|
|
1980
|
+
{ text: "Updated asset URLs. Cache hit rate is at 94% after 48 hours. TTFB improved from 240ms to 35ms.", days_ago: 5 }
|
|
1849
1981
|
]),
|
|
1850
1982
|
days_ago: 16,
|
|
1851
1983
|
updated_ago: 5,
|
|
@@ -1859,6 +1991,7 @@ var SCENARIO_6 = [
|
|
|
1859
1991
|
priority: "default",
|
|
1860
1992
|
status: "not_started",
|
|
1861
1993
|
up_next: false,
|
|
1994
|
+
tags: ["navigation", "ui"],
|
|
1862
1995
|
notes: "",
|
|
1863
1996
|
days_ago: 8,
|
|
1864
1997
|
updated_ago: 8
|
|
@@ -1867,88 +2000,96 @@ var SCENARIO_6 = [
|
|
|
1867
2000
|
var SCENARIO_7 = [
|
|
1868
2001
|
{
|
|
1869
2002
|
title: "Implement product search autocomplete",
|
|
1870
|
-
details: "Add typeahead suggestions to the search bar using the product name index.
|
|
2003
|
+
details: "Add typeahead suggestions to the search bar using the product name index.",
|
|
1871
2004
|
category: "feature",
|
|
1872
2005
|
priority: "highest",
|
|
1873
2006
|
status: "not_started",
|
|
1874
2007
|
up_next: true,
|
|
2008
|
+
tags: ["search", "ux"],
|
|
1875
2009
|
notes: "",
|
|
1876
2010
|
days_ago: 2,
|
|
1877
2011
|
updated_ago: 2
|
|
1878
2012
|
},
|
|
1879
2013
|
{
|
|
1880
2014
|
title: "Fix broken password reset flow for SSO users",
|
|
1881
|
-
details: "SSO users who try to reset their password get a generic error.
|
|
2015
|
+
details: "SSO users who try to reset their password get a generic error.",
|
|
1882
2016
|
category: "bug",
|
|
1883
2017
|
priority: "high",
|
|
1884
2018
|
status: "not_started",
|
|
1885
2019
|
up_next: true,
|
|
2020
|
+
tags: ["auth", "sso"],
|
|
1886
2021
|
notes: "",
|
|
1887
2022
|
days_ago: 3,
|
|
1888
2023
|
updated_ago: 3
|
|
1889
2024
|
},
|
|
1890
2025
|
{
|
|
1891
2026
|
title: "Add support for gift cards at checkout",
|
|
1892
|
-
details: "
|
|
2027
|
+
details: "Support gift card codes during checkout with partial redemption and balance tracking.",
|
|
1893
2028
|
category: "feature",
|
|
1894
2029
|
priority: "default",
|
|
1895
2030
|
status: "not_started",
|
|
1896
2031
|
up_next: false,
|
|
2032
|
+
tags: ["checkout", "payments"],
|
|
1897
2033
|
notes: "",
|
|
1898
2034
|
days_ago: 5,
|
|
1899
2035
|
updated_ago: 5
|
|
1900
2036
|
},
|
|
1901
2037
|
{
|
|
1902
2038
|
title: "Investigate slow query on order history page",
|
|
1903
|
-
details: "The order history page takes 4+ seconds for users with 200+ orders.
|
|
2039
|
+
details: "The order history page takes 4+ seconds for users with 200+ orders.",
|
|
1904
2040
|
category: "investigation",
|
|
1905
2041
|
priority: "high",
|
|
1906
2042
|
status: "not_started",
|
|
1907
2043
|
up_next: false,
|
|
2044
|
+
tags: ["performance", "database"],
|
|
1908
2045
|
notes: "",
|
|
1909
2046
|
days_ago: 4,
|
|
1910
2047
|
updated_ago: 4
|
|
1911
2048
|
},
|
|
1912
2049
|
{
|
|
1913
2050
|
title: "Refactor authentication middleware to support API keys",
|
|
1914
|
-
details: "Third-party integrations need API key auth in addition to session cookies.
|
|
2051
|
+
details: "Third-party integrations need API key auth in addition to session cookies.",
|
|
1915
2052
|
category: "task",
|
|
1916
2053
|
priority: "high",
|
|
1917
2054
|
status: "started",
|
|
1918
2055
|
up_next: true,
|
|
1919
|
-
|
|
2056
|
+
tags: ["auth", "api"],
|
|
2057
|
+
notes: notesJson([{ text: "Created the AuthStrategy interface and migrated session auth. Working on the API key strategy next.", days_ago: 0.5 }]),
|
|
1920
2058
|
days_ago: 4,
|
|
1921
2059
|
updated_ago: 0.5
|
|
1922
2060
|
},
|
|
1923
2061
|
{
|
|
1924
2062
|
title: "Fix cart not clearing after successful checkout",
|
|
1925
|
-
details: "
|
|
2063
|
+
details: "The clearCart() call is inside a catch block by mistake.",
|
|
1926
2064
|
category: "bug",
|
|
1927
2065
|
priority: "highest",
|
|
1928
2066
|
status: "started",
|
|
1929
2067
|
up_next: true,
|
|
2068
|
+
tags: ["checkout", "cart"],
|
|
1930
2069
|
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 }]),
|
|
1931
2070
|
days_ago: 1,
|
|
1932
2071
|
updated_ago: 0.3
|
|
1933
2072
|
},
|
|
1934
2073
|
{
|
|
1935
2074
|
title: "Update shipping rate calculation for oversized items",
|
|
1936
|
-
details: "Dimensional weight pricing is required for packages over 1 cubic foot.
|
|
2075
|
+
details: "Dimensional weight pricing is required for packages over 1 cubic foot.",
|
|
1937
2076
|
category: "requirement_change",
|
|
1938
2077
|
priority: "default",
|
|
1939
2078
|
status: "started",
|
|
1940
2079
|
up_next: false,
|
|
1941
|
-
|
|
2080
|
+
tags: ["shipping", "pricing"],
|
|
2081
|
+
notes: notesJson([{ text: "Implemented dim weight formula. Comparing rates against the carrier API.", days_ago: 1 }]),
|
|
1942
2082
|
days_ago: 6,
|
|
1943
2083
|
updated_ago: 1
|
|
1944
2084
|
},
|
|
1945
2085
|
{
|
|
1946
2086
|
title: "Add end-to-end tests for the checkout flow",
|
|
1947
|
-
details: "Write Playwright tests covering: add to cart, apply coupon, enter shipping, pay, and confirm.
|
|
2087
|
+
details: "Write Playwright tests covering: add to cart, apply coupon, enter shipping, pay, and confirm.",
|
|
1948
2088
|
category: "task",
|
|
1949
2089
|
priority: "default",
|
|
1950
2090
|
status: "completed",
|
|
1951
2091
|
up_next: false,
|
|
2092
|
+
tags: ["testing", "e2e"],
|
|
1952
2093
|
notes: notesJson([{ text: "Wrote 8 E2E tests covering the full checkout flow including coupon application and payment decline handling.", days_ago: 1 }]),
|
|
1953
2094
|
days_ago: 8,
|
|
1954
2095
|
updated_ago: 1,
|
|
@@ -1956,29 +2097,54 @@ var SCENARIO_7 = [
|
|
|
1956
2097
|
},
|
|
1957
2098
|
{
|
|
1958
2099
|
title: "Fix product image carousel swipe on mobile",
|
|
1959
|
-
details: "Swipe gestures
|
|
2100
|
+
details: "Swipe gestures conflict with the browser back gesture.",
|
|
1960
2101
|
category: "bug",
|
|
1961
2102
|
priority: "default",
|
|
1962
2103
|
status: "completed",
|
|
1963
2104
|
up_next: false,
|
|
1964
|
-
|
|
2105
|
+
tags: ["mobile", "ui"],
|
|
2106
|
+
notes: notesJson([{ text: "Added a 30px horizontal threshold. Tested on iOS Safari and Chrome Android.", days_ago: 2 }]),
|
|
1965
2107
|
days_ago: 7,
|
|
1966
2108
|
updated_ago: 2,
|
|
1967
2109
|
completed_ago: 2
|
|
1968
2110
|
},
|
|
1969
2111
|
{
|
|
1970
2112
|
title: "Set up log aggregation with structured JSON logging",
|
|
1971
|
-
details: "Replace console.log calls with
|
|
2113
|
+
details: "Replace console.log calls with pino. Send logs to a central aggregation service.",
|
|
1972
2114
|
category: "task",
|
|
1973
2115
|
priority: "low",
|
|
1974
2116
|
status: "completed",
|
|
1975
2117
|
up_next: false,
|
|
1976
|
-
|
|
2118
|
+
tags: ["observability", "logging"],
|
|
2119
|
+
notes: notesJson([{ text: "Replaced all console.log with pino. Configured log shipping. Alert rules set for error-level logs.", days_ago: 3 }]),
|
|
1977
2120
|
days_ago: 10,
|
|
1978
2121
|
updated_ago: 3,
|
|
1979
2122
|
completed_ago: 3
|
|
1980
2123
|
}
|
|
1981
2124
|
];
|
|
2125
|
+
var SCENARIO_8 = [];
|
|
2126
|
+
for (let i = 0; i < 30; i++) {
|
|
2127
|
+
const cats = ["bug", "feature", "task", "investigation", "requirement_change", "issue"];
|
|
2128
|
+
const pris = ["highest", "high", "default", "low", "lowest"];
|
|
2129
|
+
const statuses = ["not_started", "started", "completed", "verified"];
|
|
2130
|
+
const status = statuses[i < 8 ? 0 : i < 14 ? 1 : i < 24 ? 2 : 3];
|
|
2131
|
+
const completed = status === "completed" || status === "verified" ? 30 - i + Math.floor(Math.random() * 5) : void 0;
|
|
2132
|
+
const verified = status === "verified" ? completed - 2 : void 0;
|
|
2133
|
+
SCENARIO_8.push({
|
|
2134
|
+
title: `Dashboard ticket ${i + 1} \u2014 ${cats[i % cats.length]} work item`,
|
|
2135
|
+
details: "",
|
|
2136
|
+
category: cats[i % cats.length],
|
|
2137
|
+
priority: pris[i % pris.length],
|
|
2138
|
+
status,
|
|
2139
|
+
up_next: i < 3,
|
|
2140
|
+
tags: [],
|
|
2141
|
+
notes: status === "completed" || status === "verified" ? notesJson([{ text: "Completed work.", days_ago: completed }]) : "",
|
|
2142
|
+
days_ago: 30 - i + Math.floor(Math.random() * 10),
|
|
2143
|
+
updated_ago: completed || Math.floor(Math.random() * 10),
|
|
2144
|
+
completed_ago: completed,
|
|
2145
|
+
verified_ago: verified
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
1982
2148
|
var SCENARIO_DATA = {
|
|
1983
2149
|
1: SCENARIO_1,
|
|
1984
2150
|
2: SCENARIO_2,
|
|
@@ -1986,8 +2152,29 @@ var SCENARIO_DATA = {
|
|
|
1986
2152
|
4: SCENARIO_4,
|
|
1987
2153
|
5: SCENARIO_5,
|
|
1988
2154
|
6: SCENARIO_6,
|
|
1989
|
-
7: SCENARIO_7
|
|
2155
|
+
7: SCENARIO_7,
|
|
2156
|
+
8: SCENARIO_8
|
|
1990
2157
|
};
|
|
2158
|
+
var SCENARIO_3_VIEWS = [
|
|
2159
|
+
{
|
|
2160
|
+
id: "high-priority-bugs",
|
|
2161
|
+
name: "High Priority Bugs",
|
|
2162
|
+
logic: "all",
|
|
2163
|
+
conditions: [
|
|
2164
|
+
{ field: "category", operator: "equals", value: "bug" },
|
|
2165
|
+
{ field: "priority", operator: "lte", value: "high" }
|
|
2166
|
+
]
|
|
2167
|
+
},
|
|
2168
|
+
{
|
|
2169
|
+
id: "active-features",
|
|
2170
|
+
name: "Active Features",
|
|
2171
|
+
logic: "all",
|
|
2172
|
+
conditions: [
|
|
2173
|
+
{ field: "category", operator: "equals", value: "feature" },
|
|
2174
|
+
{ field: "status", operator: "lte", value: "started" }
|
|
2175
|
+
]
|
|
2176
|
+
}
|
|
2177
|
+
];
|
|
1991
2178
|
async function seedDemoData(scenario) {
|
|
1992
2179
|
const db2 = await getDb();
|
|
1993
2180
|
const tickets = SCENARIO_DATA[scenario];
|
|
@@ -1999,12 +2186,19 @@ async function seedDemoData(scenario) {
|
|
|
1999
2186
|
const updatedAt = daysAgo(t.updated_ago);
|
|
2000
2187
|
const completedAt = t.completed_ago !== void 0 ? daysAgo(t.completed_ago) : null;
|
|
2001
2188
|
const verifiedAt = t.verified_ago !== void 0 ? daysAgo(t.verified_ago) : null;
|
|
2189
|
+
const tags = JSON.stringify(t.tags);
|
|
2002
2190
|
await db2.query(`
|
|
2003
|
-
INSERT INTO tickets (ticket_number, title, details, category, priority, status, up_next, notes, created_at, updated_at, completed_at, verified_at)
|
|
2004
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9
|
|
2005
|
-
`, [ticketNumber, t.title, t.details, t.category, t.priority, t.status, t.up_next, t.notes, createdAt, updatedAt, completedAt, verifiedAt]);
|
|
2191
|
+
INSERT INTO tickets (ticket_number, title, details, category, priority, status, up_next, notes, tags, created_at, updated_at, completed_at, verified_at)
|
|
2192
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::timestamp, $11::timestamp, $12::timestamp, $13::timestamp)
|
|
2193
|
+
`, [ticketNumber, t.title, t.details, t.category, t.priority, t.status, t.up_next, t.notes, tags, createdAt, updatedAt, completedAt, verifiedAt]);
|
|
2006
2194
|
}
|
|
2007
2195
|
await db2.query(`SELECT setval('ticket_seq', $1)`, [tickets.length]);
|
|
2196
|
+
if (scenario === 3) {
|
|
2197
|
+
await db2.query(
|
|
2198
|
+
`INSERT INTO settings (key, value) VALUES ('custom_views', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,
|
|
2199
|
+
[JSON.stringify(SCENARIO_3_VIEWS)]
|
|
2200
|
+
);
|
|
2201
|
+
}
|
|
2008
2202
|
if (scenario === 6) {
|
|
2009
2203
|
await db2.query(`UPDATE settings SET value = 'bottom' WHERE key = 'detail_position'`);
|
|
2010
2204
|
await db2.query(`UPDATE settings SET value = '280' WHERE key = 'detail_height'`);
|
|
@@ -2012,6 +2206,11 @@ async function seedDemoData(scenario) {
|
|
|
2012
2206
|
if (scenario === 7) {
|
|
2013
2207
|
await db2.query(`INSERT INTO settings (key, value) VALUES ('layout', 'columns') ON CONFLICT (key) DO UPDATE SET value = 'columns'`);
|
|
2014
2208
|
}
|
|
2209
|
+
if (scenario === 8) {
|
|
2210
|
+
const { backfillSnapshots: backfillSnapshots2, recordDailySnapshot: recordDailySnapshot2 } = await Promise.resolve().then(() => (init_stats(), stats_exports));
|
|
2211
|
+
await backfillSnapshots2();
|
|
2212
|
+
await recordDailySnapshot2();
|
|
2213
|
+
}
|
|
2015
2214
|
}
|
|
2016
2215
|
|
|
2017
2216
|
// src/cli.ts
|
|
@@ -2020,15 +2219,15 @@ init_gitignore();
|
|
|
2020
2219
|
// src/server.ts
|
|
2021
2220
|
import { serve } from "@hono/node-server";
|
|
2022
2221
|
import { exec } from "child_process";
|
|
2023
|
-
import { existsSync as
|
|
2222
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
2024
2223
|
import { Hono as Hono4 } from "hono";
|
|
2025
|
-
import { dirname, join as
|
|
2026
|
-
import { fileURLToPath } from "url";
|
|
2224
|
+
import { dirname as dirname2, join as join10 } from "path";
|
|
2225
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2027
2226
|
|
|
2028
2227
|
// src/routes/api.ts
|
|
2029
|
-
import { existsSync as
|
|
2228
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, rmSync as rmSync5 } from "fs";
|
|
2030
2229
|
import { Hono } from "hono";
|
|
2031
|
-
import { basename, extname, join as
|
|
2230
|
+
import { basename, extname, join as join9, relative as relative2 } from "path";
|
|
2032
2231
|
|
|
2033
2232
|
// src/skills.ts
|
|
2034
2233
|
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
@@ -2479,8 +2678,8 @@ function notifyChange() {
|
|
|
2479
2678
|
changeVersion++;
|
|
2480
2679
|
const waiters = pollWaiters;
|
|
2481
2680
|
pollWaiters = [];
|
|
2482
|
-
for (const
|
|
2483
|
-
|
|
2681
|
+
for (const resolve3 of waiters) {
|
|
2682
|
+
resolve3(changeVersion);
|
|
2484
2683
|
}
|
|
2485
2684
|
}
|
|
2486
2685
|
apiRoutes.get("/poll", async (c) => {
|
|
@@ -2489,11 +2688,11 @@ apiRoutes.get("/poll", async (c) => {
|
|
|
2489
2688
|
return c.json({ version: changeVersion });
|
|
2490
2689
|
}
|
|
2491
2690
|
const version = await Promise.race([
|
|
2492
|
-
new Promise((
|
|
2493
|
-
pollWaiters.push(
|
|
2691
|
+
new Promise((resolve3) => {
|
|
2692
|
+
pollWaiters.push(resolve3);
|
|
2494
2693
|
}),
|
|
2495
|
-
new Promise((
|
|
2496
|
-
setTimeout(() =>
|
|
2694
|
+
new Promise((resolve3) => {
|
|
2695
|
+
setTimeout(() => resolve3(changeVersion), 3e4);
|
|
2497
2696
|
})
|
|
2498
2697
|
]);
|
|
2499
2698
|
return c.json({ version });
|
|
@@ -2548,11 +2747,24 @@ apiRoutes.delete("/tickets/:id", async (c) => {
|
|
|
2548
2747
|
notifyChange();
|
|
2549
2748
|
return c.json({ ok: true });
|
|
2550
2749
|
});
|
|
2750
|
+
apiRoutes.put("/tickets/:id/notes-bulk", async (c) => {
|
|
2751
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
2752
|
+
const body = await c.req.json();
|
|
2753
|
+
const db2 = await Promise.resolve().then(() => (init_connection(), connection_exports)).then((m) => m.getDb());
|
|
2754
|
+
const result = await (await db2).query(
|
|
2755
|
+
`UPDATE tickets SET notes = $1, updated_at = NOW() WHERE id = $2 RETURNING id`,
|
|
2756
|
+
[body.notes, id]
|
|
2757
|
+
);
|
|
2758
|
+
if (result.rows.length === 0) return c.json({ error: "Not found" }, 404);
|
|
2759
|
+
scheduleAllSync();
|
|
2760
|
+
notifyChange();
|
|
2761
|
+
return c.json({ ok: true });
|
|
2762
|
+
});
|
|
2551
2763
|
apiRoutes.patch("/tickets/:id/notes/:noteId", async (c) => {
|
|
2552
2764
|
const id = parseInt(c.req.param("id"), 10);
|
|
2553
|
-
const
|
|
2765
|
+
const noteId2 = c.req.param("noteId");
|
|
2554
2766
|
const body = await c.req.json();
|
|
2555
|
-
const notes = await editNote(id,
|
|
2767
|
+
const notes = await editNote(id, noteId2, body.text);
|
|
2556
2768
|
if (!notes) return c.json({ error: "Not found" }, 404);
|
|
2557
2769
|
scheduleAllSync();
|
|
2558
2770
|
notifyChange();
|
|
@@ -2560,8 +2772,8 @@ apiRoutes.patch("/tickets/:id/notes/:noteId", async (c) => {
|
|
|
2560
2772
|
});
|
|
2561
2773
|
apiRoutes.delete("/tickets/:id/notes/:noteId", async (c) => {
|
|
2562
2774
|
const id = parseInt(c.req.param("id"), 10);
|
|
2563
|
-
const
|
|
2564
|
-
const notes = await deleteNote(id,
|
|
2775
|
+
const noteId2 = c.req.param("noteId");
|
|
2776
|
+
const notes = await deleteNote(id, noteId2);
|
|
2565
2777
|
if (!notes) return c.json({ error: "Not found" }, 404);
|
|
2566
2778
|
scheduleAllSync();
|
|
2567
2779
|
notifyChange();
|
|
@@ -2660,12 +2872,12 @@ apiRoutes.post("/tickets/:id/attachments", async (c) => {
|
|
|
2660
2872
|
const ext = extname(originalName);
|
|
2661
2873
|
const baseName = basename(originalName, ext);
|
|
2662
2874
|
const storedName = `${ticket.ticket_number}_${baseName}${ext}`;
|
|
2663
|
-
const attachDir =
|
|
2875
|
+
const attachDir = join9(dataDir2, "attachments");
|
|
2664
2876
|
mkdirSync4(attachDir, { recursive: true });
|
|
2665
|
-
const storedPath =
|
|
2877
|
+
const storedPath = join9(attachDir, storedName);
|
|
2666
2878
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
2667
|
-
const { writeFileSync:
|
|
2668
|
-
|
|
2879
|
+
const { writeFileSync: writeFileSync8 } = await import("fs");
|
|
2880
|
+
writeFileSync8(storedPath, buffer);
|
|
2669
2881
|
const attachment = await addAttachment(id, originalName, storedPath);
|
|
2670
2882
|
scheduleAllSync();
|
|
2671
2883
|
notifyChange();
|
|
@@ -2687,28 +2899,28 @@ apiRoutes.post("/attachments/:id/reveal", async (c) => {
|
|
|
2687
2899
|
const id = parseInt(c.req.param("id"), 10);
|
|
2688
2900
|
const attachment = await getAttachment(id);
|
|
2689
2901
|
if (!attachment) return c.json({ error: "Not found" }, 404);
|
|
2690
|
-
if (!
|
|
2902
|
+
if (!existsSync7(attachment.stored_path)) return c.json({ error: "File not found on disk" }, 404);
|
|
2691
2903
|
const { execFile } = await import("child_process");
|
|
2692
|
-
const { dirname:
|
|
2904
|
+
const { dirname: dirname4 } = await import("path");
|
|
2693
2905
|
const platform = process.platform;
|
|
2694
2906
|
if (platform === "darwin") {
|
|
2695
2907
|
execFile("open", ["-R", attachment.stored_path]);
|
|
2696
2908
|
} else if (platform === "win32") {
|
|
2697
2909
|
execFile("explorer", ["/select,", attachment.stored_path]);
|
|
2698
2910
|
} else {
|
|
2699
|
-
execFile("xdg-open", [
|
|
2911
|
+
execFile("xdg-open", [dirname4(attachment.stored_path)]);
|
|
2700
2912
|
}
|
|
2701
2913
|
return c.json({ ok: true });
|
|
2702
2914
|
});
|
|
2703
2915
|
apiRoutes.get("/attachments/file/*", async (c) => {
|
|
2704
2916
|
const filePath = c.req.path.replace("/api/attachments/file/", "");
|
|
2705
2917
|
const dataDir2 = c.get("dataDir");
|
|
2706
|
-
const fullPath =
|
|
2707
|
-
if (!
|
|
2918
|
+
const fullPath = join9(dataDir2, "attachments", filePath);
|
|
2919
|
+
if (!existsSync7(fullPath)) {
|
|
2708
2920
|
return c.json({ error: "File not found" }, 404);
|
|
2709
2921
|
}
|
|
2710
|
-
const { readFileSync:
|
|
2711
|
-
const content =
|
|
2922
|
+
const { readFileSync: readFileSync9 } = await import("fs");
|
|
2923
|
+
const content = readFileSync9(fullPath);
|
|
2712
2924
|
const ext = extname(fullPath).toLowerCase();
|
|
2713
2925
|
const mimeTypes = {
|
|
2714
2926
|
".png": "image/png",
|
|
@@ -2788,7 +3000,7 @@ apiRoutes.patch("/file-settings", async (c) => {
|
|
|
2788
3000
|
apiRoutes.get("/worklist-info", (c) => {
|
|
2789
3001
|
const dataDir2 = c.get("dataDir");
|
|
2790
3002
|
const cwd = process.cwd();
|
|
2791
|
-
const worklistRel = relative2(cwd,
|
|
3003
|
+
const worklistRel = relative2(cwd, join9(dataDir2, "worklist.md"));
|
|
2792
3004
|
const prompt = `Read ${worklistRel} for current work items.`;
|
|
2793
3005
|
ensureSkills();
|
|
2794
3006
|
const skillCreated = consumeSkillsCreatedFlag();
|
|
@@ -2828,14 +3040,68 @@ apiRoutes.post("/gitignore/add", async (c) => {
|
|
|
2828
3040
|
ensureGitignore2(process.cwd());
|
|
2829
3041
|
return c.json({ ok: true });
|
|
2830
3042
|
});
|
|
3043
|
+
apiRoutes.get("/channel/claude-check", async (c) => {
|
|
3044
|
+
const { execFileSync } = await import("child_process");
|
|
3045
|
+
try {
|
|
3046
|
+
const version = execFileSync("claude", ["--version"], { timeout: 5e3, encoding: "utf-8" }).trim();
|
|
3047
|
+
const match = version.match(/(\d+\.\d+\.\d+)/);
|
|
3048
|
+
const versionNum = match ? match[1] : null;
|
|
3049
|
+
const parts = versionNum ? versionNum.split(".").map(Number) : [];
|
|
3050
|
+
const meetsMinimum = parts.length === 3 && (parts[0] > 2 || parts[0] === 2 && parts[1] > 1 || parts[0] === 2 && parts[1] === 1 && parts[2] >= 80);
|
|
3051
|
+
return c.json({ installed: true, version: versionNum, meetsMinimum });
|
|
3052
|
+
} catch {
|
|
3053
|
+
return c.json({ installed: false, version: null, meetsMinimum: false });
|
|
3054
|
+
}
|
|
3055
|
+
});
|
|
3056
|
+
var channelDoneFlag = false;
|
|
3057
|
+
apiRoutes.get("/channel/status", async (c) => {
|
|
3058
|
+
const { isChannelAlive: isChannelAlive2, getChannelPort: getChannelPort2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
3059
|
+
const dataDir2 = c.get("dataDir");
|
|
3060
|
+
const settings = await getSettings();
|
|
3061
|
+
const enabled = settings.channel_enabled === "true";
|
|
3062
|
+
const port2 = getChannelPort2(dataDir2);
|
|
3063
|
+
const alive = enabled ? await isChannelAlive2(dataDir2) : false;
|
|
3064
|
+
const done = channelDoneFlag;
|
|
3065
|
+
if (done) channelDoneFlag = false;
|
|
3066
|
+
return c.json({ enabled, alive, port: port2, done });
|
|
3067
|
+
});
|
|
3068
|
+
apiRoutes.post("/channel/trigger", async (c) => {
|
|
3069
|
+
const { triggerChannel: triggerChannel2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
3070
|
+
const dataDir2 = c.get("dataDir");
|
|
3071
|
+
const serverPort = parseInt(new URL(c.req.url).port || "4174", 10);
|
|
3072
|
+
const body = await c.req.json().catch(() => ({ message: void 0 }));
|
|
3073
|
+
channelDoneFlag = false;
|
|
3074
|
+
const ok = await triggerChannel2(dataDir2, serverPort, body.message);
|
|
3075
|
+
return c.json({ ok });
|
|
3076
|
+
});
|
|
3077
|
+
apiRoutes.post("/channel/done", async (_c) => {
|
|
3078
|
+
channelDoneFlag = true;
|
|
3079
|
+
notifyChange();
|
|
3080
|
+
return _c.json({ ok: true });
|
|
3081
|
+
});
|
|
3082
|
+
apiRoutes.post("/channel/enable", async (c) => {
|
|
3083
|
+
const { registerChannel: registerChannel2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
3084
|
+
const dataDir2 = c.get("dataDir");
|
|
3085
|
+
await updateSetting("channel_enabled", "true");
|
|
3086
|
+
registerChannel2(dataDir2);
|
|
3087
|
+
notifyChange();
|
|
3088
|
+
return c.json({ ok: true });
|
|
3089
|
+
});
|
|
3090
|
+
apiRoutes.post("/channel/disable", async (c) => {
|
|
3091
|
+
const { unregisterChannel: unregisterChannel2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
3092
|
+
await updateSetting("channel_enabled", "false");
|
|
3093
|
+
unregisterChannel2();
|
|
3094
|
+
notifyChange();
|
|
3095
|
+
return c.json({ ok: true });
|
|
3096
|
+
});
|
|
2831
3097
|
apiRoutes.post("/print", async (c) => {
|
|
2832
3098
|
const { html } = await c.req.json();
|
|
2833
|
-
const { writeFileSync:
|
|
3099
|
+
const { writeFileSync: writeFileSync8 } = await import("fs");
|
|
2834
3100
|
const { tmpdir: tmpdir2 } = await import("os");
|
|
2835
3101
|
const { join: pathJoin } = await import("path");
|
|
2836
3102
|
const { execFile } = await import("child_process");
|
|
2837
3103
|
const tmpPath = pathJoin(tmpdir2(), `hotsheet-print-${Date.now()}.html`);
|
|
2838
|
-
|
|
3104
|
+
writeFileSync8(tmpPath, html, "utf-8");
|
|
2839
3105
|
const platform = process.platform;
|
|
2840
3106
|
if (platform === "darwin") {
|
|
2841
3107
|
execFile("open", [tmpPath]);
|
|
@@ -3165,6 +3431,13 @@ pageRoutes.get("/", (c) => {
|
|
|
3165
3431
|
] }),
|
|
3166
3432
|
/* @__PURE__ */ jsx("div", { className: "app-body", children: [
|
|
3167
3433
|
/* @__PURE__ */ jsx("nav", { className: "sidebar", children: [
|
|
3434
|
+
/* @__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: [
|
|
3435
|
+
/* @__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" }) }) }),
|
|
3436
|
+
/* @__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: [
|
|
3437
|
+
/* @__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" }),
|
|
3438
|
+
/* @__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" })
|
|
3439
|
+
] }) })
|
|
3440
|
+
] }) }),
|
|
3168
3441
|
/* @__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: [
|
|
3169
3442
|
/* @__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: [
|
|
3170
3443
|
/* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2" }),
|
|
@@ -3193,7 +3466,7 @@ pageRoutes.get("/", (c) => {
|
|
|
3193
3466
|
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "archive", children: "Archive" }),
|
|
3194
3467
|
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "trash", children: "Trash" })
|
|
3195
3468
|
] }),
|
|
3196
|
-
/* @__PURE__ */ jsx("div", { className: "sidebar-section", children: [
|
|
3469
|
+
/* @__PURE__ */ jsx("div", { className: "sidebar-section", id: "sidebar-categories", children: [
|
|
3197
3470
|
/* @__PURE__ */ jsx("div", { className: "sidebar-label", children: "Category" }),
|
|
3198
3471
|
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "category:issue", children: [
|
|
3199
3472
|
/* @__PURE__ */ jsx("span", { className: "cat-dot", style: "background:#6b7280" }),
|
|
@@ -3346,7 +3619,10 @@ pageRoutes.get("/", (c) => {
|
|
|
3346
3619
|
" close"
|
|
3347
3620
|
] })
|
|
3348
3621
|
] }),
|
|
3349
|
-
/* @__PURE__ */ jsx("div", {
|
|
3622
|
+
/* @__PURE__ */ jsx("div", { className: "status-bar-right", children: [
|
|
3623
|
+
/* @__PURE__ */ jsx("div", { id: "status-bar", className: "status-bar" }),
|
|
3624
|
+
/* @__PURE__ */ jsx("span", { id: "channel-status-indicator", className: "channel-status-indicator", style: "display:none" })
|
|
3625
|
+
] })
|
|
3350
3626
|
] })
|
|
3351
3627
|
] }),
|
|
3352
3628
|
/* @__PURE__ */ jsx("div", { className: "settings-overlay", id: "settings-overlay", style: "display:none", children: /* @__PURE__ */ jsx("div", { className: "settings-dialog", children: [
|
|
@@ -3408,7 +3684,24 @@ pageRoutes.get("/", (c) => {
|
|
|
3408
3684
|
/* @__PURE__ */ jsx("div", { className: "settings-field", children: [
|
|
3409
3685
|
/* @__PURE__ */ jsx("label", { children: "Auto-clear verified after (days)" }),
|
|
3410
3686
|
/* @__PURE__ */ jsx("input", { type: "number", id: "settings-verified-days", min: "1", value: "30" })
|
|
3411
|
-
] })
|
|
3687
|
+
] }),
|
|
3688
|
+
/* @__PURE__ */ jsx("div", { id: "settings-channel-section", style: "display:none", children: /* @__PURE__ */ jsx("div", { className: "settings-section", style: "margin-top:16px", children: [
|
|
3689
|
+
/* @__PURE__ */ jsx("h3", { className: "settings-experimental-heading", children: "Experimental" }),
|
|
3690
|
+
/* @__PURE__ */ jsx("div", { className: "settings-field", children: [
|
|
3691
|
+
/* @__PURE__ */ jsx("label", { className: "settings-checkbox-label", children: [
|
|
3692
|
+
/* @__PURE__ */ jsx("input", { type: "checkbox", id: "settings-channel-enabled" }),
|
|
3693
|
+
"Enable Claude Channel integration"
|
|
3694
|
+
] }),
|
|
3695
|
+
/* @__PURE__ */ jsx("span", { className: "settings-hint", id: "settings-channel-hint", children: "Push worklist events to a running Claude Code session via MCP channels." }),
|
|
3696
|
+
/* @__PURE__ */ jsx("div", { id: "settings-channel-instructions", style: "display:none", children: [
|
|
3697
|
+
/* @__PURE__ */ jsx("div", { className: "settings-hint", style: "margin-top:8px", children: "Launch Claude Code with channel support:" }),
|
|
3698
|
+
/* @__PURE__ */ jsx("div", { className: "settings-channel-command", children: [
|
|
3699
|
+
/* @__PURE__ */ jsx("code", { id: "settings-channel-cmd", children: "claude --dangerously-load-development-channels server:hotsheet-channel" }),
|
|
3700
|
+
/* @__PURE__ */ jsx("button", { className: "btn btn-sm", id: "settings-channel-copy-btn", title: "Copy command", children: "Copy" })
|
|
3701
|
+
] })
|
|
3702
|
+
] })
|
|
3703
|
+
] })
|
|
3704
|
+
] }) })
|
|
3412
3705
|
] }),
|
|
3413
3706
|
/* @__PURE__ */ jsx("div", { className: "settings-tab-panel", "data-panel": "categories", children: [
|
|
3414
3707
|
/* @__PURE__ */ jsx("div", { className: "settings-section-header", children: [
|
|
@@ -3444,11 +3737,11 @@ pageRoutes.get("/", (c) => {
|
|
|
3444
3737
|
});
|
|
3445
3738
|
|
|
3446
3739
|
// src/server.ts
|
|
3447
|
-
function tryServe(
|
|
3448
|
-
return new Promise((
|
|
3449
|
-
const server = serve({ fetch, port: port2 });
|
|
3740
|
+
function tryServe(fetch2, port2) {
|
|
3741
|
+
return new Promise((resolve3, reject) => {
|
|
3742
|
+
const server = serve({ fetch: fetch2, port: port2 });
|
|
3450
3743
|
server.on("listening", () => {
|
|
3451
|
-
|
|
3744
|
+
resolve3(port2);
|
|
3452
3745
|
});
|
|
3453
3746
|
server.on("error", (err) => {
|
|
3454
3747
|
reject(err);
|
|
@@ -3461,21 +3754,21 @@ async function startServer(port2, dataDir2, options) {
|
|
|
3461
3754
|
c.set("dataDir", dataDir2);
|
|
3462
3755
|
await next();
|
|
3463
3756
|
});
|
|
3464
|
-
const selfDir =
|
|
3465
|
-
const distDir =
|
|
3757
|
+
const selfDir = dirname2(fileURLToPath2(import.meta.url));
|
|
3758
|
+
const distDir = existsSync8(join10(selfDir, "client", "styles.css")) ? join10(selfDir, "client") : join10(selfDir, "..", "dist", "client");
|
|
3466
3759
|
app.get("/static/styles.css", (c) => {
|
|
3467
|
-
const css =
|
|
3760
|
+
const css = readFileSync7(join10(distDir, "styles.css"), "utf-8");
|
|
3468
3761
|
return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
|
|
3469
3762
|
});
|
|
3470
3763
|
app.get("/static/app.js", (c) => {
|
|
3471
|
-
const js =
|
|
3764
|
+
const js = readFileSync7(join10(distDir, "app.global.js"), "utf-8");
|
|
3472
3765
|
return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
|
|
3473
3766
|
});
|
|
3474
3767
|
app.get("/static/assets/:filename", (c) => {
|
|
3475
3768
|
const filename = c.req.param("filename");
|
|
3476
|
-
const filePath =
|
|
3477
|
-
if (!
|
|
3478
|
-
const content =
|
|
3769
|
+
const filePath = join10(distDir, "assets", filename);
|
|
3770
|
+
if (!existsSync8(filePath)) return c.notFound();
|
|
3771
|
+
const content = readFileSync7(filePath);
|
|
3479
3772
|
const ext = filename.split(".").pop();
|
|
3480
3773
|
const mimeTypes = { png: "image/png", jpg: "image/jpeg", svg: "image/svg+xml" };
|
|
3481
3774
|
return new Response(content, { headers: { "Content-Type": mimeTypes[ext || ""] || "application/octet-stream", "Cache-Control": "max-age=86400" } });
|
|
@@ -3519,18 +3812,18 @@ async function startServer(port2, dataDir2, options) {
|
|
|
3519
3812
|
}
|
|
3520
3813
|
|
|
3521
3814
|
// src/update-check.ts
|
|
3522
|
-
import { existsSync as
|
|
3815
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
3523
3816
|
import { get } from "https";
|
|
3524
3817
|
import { homedir } from "os";
|
|
3525
|
-
import { dirname as
|
|
3526
|
-
import { fileURLToPath as
|
|
3527
|
-
var DATA_DIR =
|
|
3528
|
-
var CHECK_FILE =
|
|
3818
|
+
import { dirname as dirname3, join as join11 } from "path";
|
|
3819
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3820
|
+
var DATA_DIR = join11(homedir(), ".hotsheet");
|
|
3821
|
+
var CHECK_FILE = join11(DATA_DIR, "last-update-check");
|
|
3529
3822
|
var PACKAGE_NAME = "hotsheet";
|
|
3530
3823
|
function getCurrentVersion() {
|
|
3531
3824
|
try {
|
|
3532
|
-
const dir =
|
|
3533
|
-
const pkg = JSON.parse(
|
|
3825
|
+
const dir = dirname3(fileURLToPath3(import.meta.url));
|
|
3826
|
+
const pkg = JSON.parse(readFileSync8(join11(dir, "..", "package.json"), "utf-8"));
|
|
3534
3827
|
return pkg.version;
|
|
3535
3828
|
} catch {
|
|
3536
3829
|
return "0.0.0";
|
|
@@ -3538,8 +3831,8 @@ function getCurrentVersion() {
|
|
|
3538
3831
|
}
|
|
3539
3832
|
function getLastCheckDate() {
|
|
3540
3833
|
try {
|
|
3541
|
-
if (
|
|
3542
|
-
return
|
|
3834
|
+
if (existsSync9(CHECK_FILE)) {
|
|
3835
|
+
return readFileSync8(CHECK_FILE, "utf-8").trim();
|
|
3543
3836
|
}
|
|
3544
3837
|
} catch {
|
|
3545
3838
|
}
|
|
@@ -3547,7 +3840,7 @@ function getLastCheckDate() {
|
|
|
3547
3840
|
}
|
|
3548
3841
|
function saveCheckDate() {
|
|
3549
3842
|
mkdirSync5(DATA_DIR, { recursive: true });
|
|
3550
|
-
|
|
3843
|
+
writeFileSync7(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
|
|
3551
3844
|
}
|
|
3552
3845
|
function isFirstUseToday() {
|
|
3553
3846
|
const last = getLastCheckDate();
|
|
@@ -3556,10 +3849,10 @@ function isFirstUseToday() {
|
|
|
3556
3849
|
return last !== today;
|
|
3557
3850
|
}
|
|
3558
3851
|
function fetchLatestVersion() {
|
|
3559
|
-
return new Promise((
|
|
3852
|
+
return new Promise((resolve3) => {
|
|
3560
3853
|
const req = get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { timeout: 5e3 }, (res) => {
|
|
3561
3854
|
if (res.statusCode !== 200) {
|
|
3562
|
-
|
|
3855
|
+
resolve3(null);
|
|
3563
3856
|
return;
|
|
3564
3857
|
}
|
|
3565
3858
|
let data = "";
|
|
@@ -3568,18 +3861,18 @@ function fetchLatestVersion() {
|
|
|
3568
3861
|
});
|
|
3569
3862
|
res.on("end", () => {
|
|
3570
3863
|
try {
|
|
3571
|
-
|
|
3864
|
+
resolve3(JSON.parse(data).version);
|
|
3572
3865
|
} catch {
|
|
3573
|
-
|
|
3866
|
+
resolve3(null);
|
|
3574
3867
|
}
|
|
3575
3868
|
});
|
|
3576
3869
|
});
|
|
3577
3870
|
req.on("error", () => {
|
|
3578
|
-
|
|
3871
|
+
resolve3(null);
|
|
3579
3872
|
});
|
|
3580
3873
|
req.on("timeout", () => {
|
|
3581
3874
|
req.destroy();
|
|
3582
|
-
|
|
3875
|
+
resolve3(null);
|
|
3583
3876
|
});
|
|
3584
3877
|
});
|
|
3585
3878
|
}
|
|
@@ -3652,7 +3945,7 @@ Examples:
|
|
|
3652
3945
|
function parseArgs(argv) {
|
|
3653
3946
|
const args = argv.slice(2);
|
|
3654
3947
|
let port2 = 4174;
|
|
3655
|
-
let dataDir2 =
|
|
3948
|
+
let dataDir2 = join12(process.cwd(), ".hotsheet");
|
|
3656
3949
|
let demo = null;
|
|
3657
3950
|
let forceUpdateCheck = false;
|
|
3658
3951
|
let noOpen = false;
|
|
@@ -3681,7 +3974,7 @@ function parseArgs(argv) {
|
|
|
3681
3974
|
}
|
|
3682
3975
|
break;
|
|
3683
3976
|
case "--data-dir":
|
|
3684
|
-
dataDir2 =
|
|
3977
|
+
dataDir2 = resolve2(args[++i]);
|
|
3685
3978
|
break;
|
|
3686
3979
|
case "--check-for-updates":
|
|
3687
3980
|
forceUpdateCheck = true;
|
|
@@ -3719,7 +4012,7 @@ async function main() {
|
|
|
3719
4012
|
}
|
|
3720
4013
|
process.exit(1);
|
|
3721
4014
|
}
|
|
3722
|
-
dataDir2 =
|
|
4015
|
+
dataDir2 = join12(tmpdir(), `hotsheet-demo-${demo}-${Date.now()}`);
|
|
3723
4016
|
console.log(`
|
|
3724
4017
|
DEMO MODE: ${scenario.label}
|
|
3725
4018
|
`);
|