hotsheet 0.4.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 +482 -190
- 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,173 @@ 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 { 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
|
+
|
|
423
524
|
// src/cli.ts
|
|
424
525
|
import { mkdirSync as mkdirSync6 } from "fs";
|
|
425
526
|
import { tmpdir } from "os";
|
|
426
|
-
import { join as
|
|
527
|
+
import { join as join12, resolve as resolve2 } from "path";
|
|
427
528
|
|
|
428
529
|
// src/backup.ts
|
|
429
530
|
init_connection();
|
|
@@ -712,23 +813,23 @@ function parseNotes(raw) {
|
|
|
712
813
|
}
|
|
713
814
|
return [{ id: generateNoteId(), text: raw, created_at: (/* @__PURE__ */ new Date()).toISOString() }];
|
|
714
815
|
}
|
|
715
|
-
async function editNote(ticketId,
|
|
816
|
+
async function editNote(ticketId, noteId2, text) {
|
|
716
817
|
const db2 = await getDb();
|
|
717
818
|
const result = await db2.query(`SELECT notes FROM tickets WHERE id = $1`, [ticketId]);
|
|
718
819
|
if (result.rows.length === 0) return null;
|
|
719
820
|
const notes = parseNotes(result.rows[0].notes);
|
|
720
|
-
const note = notes.find((n) => n.id ===
|
|
821
|
+
const note = notes.find((n) => n.id === noteId2);
|
|
721
822
|
if (!note) return null;
|
|
722
823
|
note.text = text;
|
|
723
824
|
await db2.query(`UPDATE tickets SET notes = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(notes), ticketId]);
|
|
724
825
|
return notes;
|
|
725
826
|
}
|
|
726
|
-
async function deleteNote(ticketId,
|
|
827
|
+
async function deleteNote(ticketId, noteId2) {
|
|
727
828
|
const db2 = await getDb();
|
|
728
829
|
const result = await db2.query(`SELECT notes FROM tickets WHERE id = $1`, [ticketId]);
|
|
729
830
|
if (result.rows.length === 0) return null;
|
|
730
831
|
const notes = parseNotes(result.rows[0].notes);
|
|
731
|
-
const idx = notes.findIndex((n) => n.id ===
|
|
832
|
+
const idx = notes.findIndex((n) => n.id === noteId2);
|
|
732
833
|
if (idx === -1) return null;
|
|
733
834
|
notes.splice(idx, 1);
|
|
734
835
|
await db2.query(`UPDATE tickets SET notes = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(notes), ticketId]);
|
|
@@ -1254,20 +1355,26 @@ init_connection();
|
|
|
1254
1355
|
var DEMO_SCENARIOS = [
|
|
1255
1356
|
{ id: 1, label: "Main UI \u2014 all tickets with detail panel" },
|
|
1256
1357
|
{ id: 2, label: "Quick entry \u2014 bullet-list ticket creation" },
|
|
1257
|
-
{ id: 3, label: "Sidebar filtering \u2014
|
|
1358
|
+
{ id: 3, label: "Sidebar filtering \u2014 custom views and categories" },
|
|
1258
1359
|
{ id: 4, label: "AI worklist \u2014 Up Next tickets with notes" },
|
|
1259
1360
|
{ 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" }
|
|
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" }
|
|
1262
1364
|
];
|
|
1263
1365
|
function daysAgo(days) {
|
|
1264
1366
|
const d = /* @__PURE__ */ new Date();
|
|
1265
1367
|
d.setTime(d.getTime() - days * 24 * 60 * 60 * 1e3);
|
|
1266
1368
|
return d.toISOString();
|
|
1267
1369
|
}
|
|
1370
|
+
var noteId = 0;
|
|
1268
1371
|
function notesJson(entries) {
|
|
1269
1372
|
if (entries.length === 0) return "";
|
|
1270
|
-
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
|
+
})));
|
|
1271
1378
|
}
|
|
1272
1379
|
var SCENARIO_1 = [
|
|
1273
1380
|
{
|
|
@@ -1277,6 +1384,7 @@ var SCENARIO_1 = [
|
|
|
1277
1384
|
priority: "highest",
|
|
1278
1385
|
status: "started",
|
|
1279
1386
|
up_next: true,
|
|
1387
|
+
tags: ["checkout", "shipping"],
|
|
1280
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 }]),
|
|
1281
1389
|
days_ago: 5,
|
|
1282
1390
|
updated_ago: 0.5
|
|
@@ -1288,6 +1396,7 @@ var SCENARIO_1 = [
|
|
|
1288
1396
|
priority: "high",
|
|
1289
1397
|
status: "not_started",
|
|
1290
1398
|
up_next: true,
|
|
1399
|
+
tags: ["ux", "product-pages"],
|
|
1291
1400
|
notes: "",
|
|
1292
1401
|
days_ago: 4,
|
|
1293
1402
|
updated_ago: 4
|
|
@@ -1299,6 +1408,7 @@ var SCENARIO_1 = [
|
|
|
1299
1408
|
priority: "high",
|
|
1300
1409
|
status: "started",
|
|
1301
1410
|
up_next: true,
|
|
1411
|
+
tags: ["infrastructure", "devops"],
|
|
1302
1412
|
notes: notesJson([{ text: "Created the backup script and IAM role. Testing the S3 lifecycle policy for retention.", days_ago: 1 }]),
|
|
1303
1413
|
days_ago: 7,
|
|
1304
1414
|
updated_ago: 1
|
|
@@ -1310,6 +1420,7 @@ var SCENARIO_1 = [
|
|
|
1310
1420
|
priority: "high",
|
|
1311
1421
|
status: "not_started",
|
|
1312
1422
|
up_next: true,
|
|
1423
|
+
tags: ["payments"],
|
|
1313
1424
|
notes: "",
|
|
1314
1425
|
days_ago: 3,
|
|
1315
1426
|
updated_ago: 3
|
|
@@ -1321,6 +1432,7 @@ var SCENARIO_1 = [
|
|
|
1321
1432
|
priority: "default",
|
|
1322
1433
|
status: "not_started",
|
|
1323
1434
|
up_next: false,
|
|
1435
|
+
tags: ["performance", "images"],
|
|
1324
1436
|
notes: "",
|
|
1325
1437
|
days_ago: 6,
|
|
1326
1438
|
updated_ago: 6
|
|
@@ -1332,6 +1444,7 @@ var SCENARIO_1 = [
|
|
|
1332
1444
|
priority: "default",
|
|
1333
1445
|
status: "not_started",
|
|
1334
1446
|
up_next: false,
|
|
1447
|
+
tags: ["checkout", "accounts"],
|
|
1335
1448
|
notes: "",
|
|
1336
1449
|
days_ago: 10,
|
|
1337
1450
|
updated_ago: 10
|
|
@@ -1343,6 +1456,7 @@ var SCENARIO_1 = [
|
|
|
1343
1456
|
priority: "default",
|
|
1344
1457
|
status: "started",
|
|
1345
1458
|
up_next: false,
|
|
1459
|
+
tags: ["tax", "eu", "compliance"],
|
|
1346
1460
|
notes: "",
|
|
1347
1461
|
days_ago: 8,
|
|
1348
1462
|
updated_ago: 2
|
|
@@ -1354,6 +1468,7 @@ var SCENARIO_1 = [
|
|
|
1354
1468
|
priority: "default",
|
|
1355
1469
|
status: "completed",
|
|
1356
1470
|
up_next: false,
|
|
1471
|
+
tags: ["docs", "api"],
|
|
1357
1472
|
notes: notesJson([{ text: "Documented all 12 order endpoints with examples. Published to /docs.", days_ago: 1 }]),
|
|
1358
1473
|
days_ago: 12,
|
|
1359
1474
|
updated_ago: 1,
|
|
@@ -1366,6 +1481,7 @@ var SCENARIO_1 = [
|
|
|
1366
1481
|
priority: "highest",
|
|
1367
1482
|
status: "verified",
|
|
1368
1483
|
up_next: false,
|
|
1484
|
+
tags: ["mobile", "api"],
|
|
1369
1485
|
notes: notesJson([
|
|
1370
1486
|
{ text: "Added CORS middleware with correct origins. Tested against staging with the mobile app builds.", days_ago: 3 },
|
|
1371
1487
|
{ text: "Verified fix is working in production. No more CORS errors in mobile app error logs.", days_ago: 2 }
|
|
@@ -1382,6 +1498,7 @@ var SCENARIO_1 = [
|
|
|
1382
1498
|
priority: "low",
|
|
1383
1499
|
status: "not_started",
|
|
1384
1500
|
up_next: false,
|
|
1501
|
+
tags: ["ux", "theming"],
|
|
1385
1502
|
notes: "",
|
|
1386
1503
|
days_ago: 15,
|
|
1387
1504
|
updated_ago: 15
|
|
@@ -1393,6 +1510,7 @@ var SCENARIO_1 = [
|
|
|
1393
1510
|
priority: "default",
|
|
1394
1511
|
status: "completed",
|
|
1395
1512
|
up_next: false,
|
|
1513
|
+
tags: ["infrastructure", "database"],
|
|
1396
1514
|
notes: notesJson([{ text: "Migrated to pg pool with max 20 connections. Load tested successfully at 500 concurrent requests.", days_ago: 4 }]),
|
|
1397
1515
|
days_ago: 18,
|
|
1398
1516
|
updated_ago: 4,
|
|
@@ -1405,6 +1523,7 @@ var SCENARIO_1 = [
|
|
|
1405
1523
|
priority: "lowest",
|
|
1406
1524
|
status: "not_started",
|
|
1407
1525
|
up_next: false,
|
|
1526
|
+
tags: ["frontend", "seo"],
|
|
1408
1527
|
notes: "",
|
|
1409
1528
|
days_ago: 20,
|
|
1410
1529
|
updated_ago: 20
|
|
@@ -1418,6 +1537,7 @@ var SCENARIO_2 = [
|
|
|
1418
1537
|
priority: "high",
|
|
1419
1538
|
status: "not_started",
|
|
1420
1539
|
up_next: true,
|
|
1540
|
+
tags: ["auth"],
|
|
1421
1541
|
notes: "",
|
|
1422
1542
|
days_ago: 2,
|
|
1423
1543
|
updated_ago: 2
|
|
@@ -1429,6 +1549,7 @@ var SCENARIO_2 = [
|
|
|
1429
1549
|
priority: "default",
|
|
1430
1550
|
status: "not_started",
|
|
1431
1551
|
up_next: false,
|
|
1552
|
+
tags: ["admin"],
|
|
1432
1553
|
notes: "",
|
|
1433
1554
|
days_ago: 3,
|
|
1434
1555
|
updated_ago: 3
|
|
@@ -1440,6 +1561,7 @@ var SCENARIO_2 = [
|
|
|
1440
1561
|
priority: "default",
|
|
1441
1562
|
status: "started",
|
|
1442
1563
|
up_next: false,
|
|
1564
|
+
tags: ["maintenance"],
|
|
1443
1565
|
notes: "",
|
|
1444
1566
|
days_ago: 1,
|
|
1445
1567
|
updated_ago: 0.5
|
|
@@ -1453,17 +1575,19 @@ var SCENARIO_3 = [
|
|
|
1453
1575
|
priority: "highest",
|
|
1454
1576
|
status: "started",
|
|
1455
1577
|
up_next: true,
|
|
1578
|
+
tags: ["checkout", "pricing"],
|
|
1456
1579
|
notes: "",
|
|
1457
1580
|
days_ago: 3,
|
|
1458
1581
|
updated_ago: 1
|
|
1459
1582
|
},
|
|
1460
1583
|
{
|
|
1461
1584
|
title: "Search returns stale results after product update",
|
|
1462
|
-
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.",
|
|
1463
1586
|
category: "bug",
|
|
1464
1587
|
priority: "high",
|
|
1465
1588
|
status: "not_started",
|
|
1466
1589
|
up_next: true,
|
|
1590
|
+
tags: ["search"],
|
|
1467
1591
|
notes: "",
|
|
1468
1592
|
days_ago: 5,
|
|
1469
1593
|
updated_ago: 5
|
|
@@ -1475,17 +1599,19 @@ var SCENARIO_3 = [
|
|
|
1475
1599
|
priority: "default",
|
|
1476
1600
|
status: "not_started",
|
|
1477
1601
|
up_next: false,
|
|
1602
|
+
tags: ["notifications"],
|
|
1478
1603
|
notes: "",
|
|
1479
1604
|
days_ago: 7,
|
|
1480
1605
|
updated_ago: 7
|
|
1481
1606
|
},
|
|
1482
1607
|
{
|
|
1483
1608
|
title: "Implement real-time inventory tracking",
|
|
1484
|
-
details:
|
|
1609
|
+
details: "Use WebSocket connections to push stock level changes to the product page.",
|
|
1485
1610
|
category: "feature",
|
|
1486
1611
|
priority: "high",
|
|
1487
1612
|
status: "started",
|
|
1488
1613
|
up_next: true,
|
|
1614
|
+
tags: ["real-time", "inventory"],
|
|
1489
1615
|
notes: notesJson([{ text: "WebSocket server is set up. Working on the client-side stock badge component.", days_ago: 0.5 }]),
|
|
1490
1616
|
days_ago: 6,
|
|
1491
1617
|
updated_ago: 0.5
|
|
@@ -1497,84 +1623,67 @@ var SCENARIO_3 = [
|
|
|
1497
1623
|
priority: "default",
|
|
1498
1624
|
status: "not_started",
|
|
1499
1625
|
up_next: false,
|
|
1626
|
+
tags: ["social"],
|
|
1500
1627
|
notes: "",
|
|
1501
1628
|
days_ago: 9,
|
|
1502
1629
|
updated_ago: 9
|
|
1503
1630
|
},
|
|
1504
1631
|
{
|
|
1505
1632
|
title: "Product video support on detail pages",
|
|
1506
|
-
details: "Allow merchants to upload product videos alongside photos.
|
|
1633
|
+
details: "Allow merchants to upload product videos alongside photos.",
|
|
1507
1634
|
category: "feature",
|
|
1508
1635
|
priority: "low",
|
|
1509
1636
|
status: "not_started",
|
|
1510
1637
|
up_next: false,
|
|
1638
|
+
tags: ["media"],
|
|
1511
1639
|
notes: "",
|
|
1512
1640
|
days_ago: 12,
|
|
1513
1641
|
updated_ago: 12
|
|
1514
1642
|
},
|
|
1515
1643
|
{
|
|
1516
1644
|
title: "Migrate image storage to CDN",
|
|
1517
|
-
details: "Move product images from local disk to CloudFront.
|
|
1645
|
+
details: "Move product images from local disk to CloudFront.",
|
|
1518
1646
|
category: "task",
|
|
1519
1647
|
priority: "high",
|
|
1520
1648
|
status: "started",
|
|
1521
1649
|
up_next: false,
|
|
1650
|
+
tags: ["infrastructure", "images"],
|
|
1522
1651
|
notes: "",
|
|
1523
1652
|
days_ago: 4,
|
|
1524
1653
|
updated_ago: 2
|
|
1525
1654
|
},
|
|
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
1655
|
{
|
|
1539
1656
|
title: "Support guest checkout without account creation",
|
|
1540
|
-
details: "
|
|
1657
|
+
details: "Many users abandon at the registration step. Allow checkout with just email.",
|
|
1541
1658
|
category: "requirement_change",
|
|
1542
1659
|
priority: "high",
|
|
1543
1660
|
status: "not_started",
|
|
1544
1661
|
up_next: true,
|
|
1662
|
+
tags: ["checkout", "conversion"],
|
|
1545
1663
|
notes: "",
|
|
1546
1664
|
days_ago: 2,
|
|
1547
1665
|
updated_ago: 2
|
|
1548
1666
|
},
|
|
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
1667
|
{
|
|
1561
1668
|
title: "Compare Redis vs Memcached for session storage",
|
|
1562
|
-
details: "
|
|
1669
|
+
details: "Evaluate Redis and Memcached for persistence, speed, and ops complexity.",
|
|
1563
1670
|
category: "investigation",
|
|
1564
1671
|
priority: "high",
|
|
1565
1672
|
status: "not_started",
|
|
1566
1673
|
up_next: false,
|
|
1674
|
+
tags: ["infrastructure"],
|
|
1567
1675
|
notes: "",
|
|
1568
1676
|
days_ago: 6,
|
|
1569
1677
|
updated_ago: 6
|
|
1570
1678
|
},
|
|
1571
1679
|
{
|
|
1572
1680
|
title: "Analyze mobile conversion drop-off funnel",
|
|
1573
|
-
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.",
|
|
1574
1682
|
category: "investigation",
|
|
1575
1683
|
priority: "default",
|
|
1576
1684
|
status: "not_started",
|
|
1577
1685
|
up_next: false,
|
|
1686
|
+
tags: ["analytics", "mobile"],
|
|
1578
1687
|
notes: "",
|
|
1579
1688
|
days_ago: 11,
|
|
1580
1689
|
updated_ago: 11
|
|
@@ -1588,6 +1697,7 @@ var SCENARIO_4 = [
|
|
|
1588
1697
|
priority: "highest",
|
|
1589
1698
|
status: "started",
|
|
1590
1699
|
up_next: true,
|
|
1700
|
+
tags: ["concurrency", "orders"],
|
|
1591
1701
|
notes: notesJson([
|
|
1592
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 },
|
|
1593
1703
|
{ 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 +1712,7 @@ var SCENARIO_4 = [
|
|
|
1602
1712
|
priority: "high",
|
|
1603
1713
|
status: "not_started",
|
|
1604
1714
|
up_next: true,
|
|
1715
|
+
tags: ["webhooks", "api"],
|
|
1605
1716
|
notes: "",
|
|
1606
1717
|
days_ago: 3,
|
|
1607
1718
|
updated_ago: 3
|
|
@@ -1613,6 +1724,7 @@ var SCENARIO_4 = [
|
|
|
1613
1724
|
priority: "high",
|
|
1614
1725
|
status: "not_started",
|
|
1615
1726
|
up_next: true,
|
|
1727
|
+
tags: ["security", "api"],
|
|
1616
1728
|
notes: "",
|
|
1617
1729
|
days_ago: 5,
|
|
1618
1730
|
updated_ago: 5
|
|
@@ -1624,31 +1736,34 @@ var SCENARIO_4 = [
|
|
|
1624
1736
|
priority: "default",
|
|
1625
1737
|
status: "not_started",
|
|
1626
1738
|
up_next: true,
|
|
1739
|
+
tags: ["pricing"],
|
|
1627
1740
|
notes: "",
|
|
1628
1741
|
days_ago: 6,
|
|
1629
1742
|
updated_ago: 6
|
|
1630
1743
|
},
|
|
1631
1744
|
{
|
|
1632
1745
|
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.
|
|
1746
|
+
details: "Product pages are slow under load. Investigate Redis caching, CDN edge caching, and stale-while-revalidate patterns.",
|
|
1634
1747
|
category: "investigation",
|
|
1635
1748
|
priority: "default",
|
|
1636
1749
|
status: "not_started",
|
|
1637
1750
|
up_next: true,
|
|
1751
|
+
tags: ["performance", "caching"],
|
|
1638
1752
|
notes: "",
|
|
1639
1753
|
days_ago: 7,
|
|
1640
1754
|
updated_ago: 7
|
|
1641
1755
|
},
|
|
1642
1756
|
{
|
|
1643
1757
|
title: "Add bulk product import from CSV",
|
|
1644
|
-
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.",
|
|
1645
1759
|
category: "feature",
|
|
1646
1760
|
priority: "low",
|
|
1647
1761
|
status: "completed",
|
|
1648
1762
|
up_next: false,
|
|
1763
|
+
tags: ["admin", "import"],
|
|
1649
1764
|
notes: notesJson([
|
|
1650
1765
|
{ 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
|
|
1766
|
+
{ text: "Added validation for required fields (name, price, SKU) and friendly error messages with row numbers.", days_ago: 2 }
|
|
1652
1767
|
]),
|
|
1653
1768
|
days_ago: 10,
|
|
1654
1769
|
updated_ago: 2,
|
|
@@ -1656,14 +1771,15 @@ var SCENARIO_4 = [
|
|
|
1656
1771
|
},
|
|
1657
1772
|
{
|
|
1658
1773
|
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
|
|
1774
|
+
details: "Addresses are currently embedded as JSON in the customers table. Extract to a separate addresses table.",
|
|
1660
1775
|
category: "task",
|
|
1661
1776
|
priority: "default",
|
|
1662
1777
|
status: "verified",
|
|
1663
1778
|
up_next: false,
|
|
1779
|
+
tags: ["database", "schema"],
|
|
1664
1780
|
notes: notesJson([
|
|
1665
1781
|
{ 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.
|
|
1782
|
+
{ text: "Verified the migration ran correctly. All address lookups use the new table.", days_ago: 3 }
|
|
1667
1783
|
]),
|
|
1668
1784
|
days_ago: 14,
|
|
1669
1785
|
updated_ago: 3,
|
|
@@ -1674,55 +1790,60 @@ var SCENARIO_4 = [
|
|
|
1674
1790
|
var SCENARIO_5 = [
|
|
1675
1791
|
{
|
|
1676
1792
|
title: "Fix email template rendering in Outlook",
|
|
1677
|
-
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.",
|
|
1678
1794
|
category: "bug",
|
|
1679
1795
|
priority: "default",
|
|
1680
1796
|
status: "not_started",
|
|
1681
1797
|
up_next: false,
|
|
1798
|
+
tags: ["email"],
|
|
1682
1799
|
notes: "",
|
|
1683
1800
|
days_ago: 3,
|
|
1684
1801
|
updated_ago: 3
|
|
1685
1802
|
},
|
|
1686
1803
|
{
|
|
1687
1804
|
title: "Handle timeout on third-party shipping rate API",
|
|
1688
|
-
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.",
|
|
1689
1806
|
category: "bug",
|
|
1690
1807
|
priority: "default",
|
|
1691
1808
|
status: "not_started",
|
|
1692
1809
|
up_next: false,
|
|
1810
|
+
tags: ["shipping", "error-handling"],
|
|
1693
1811
|
notes: "",
|
|
1694
1812
|
days_ago: 4,
|
|
1695
1813
|
updated_ago: 4
|
|
1696
1814
|
},
|
|
1697
1815
|
{
|
|
1698
1816
|
title: "Fix pagination on search results page",
|
|
1699
|
-
details: "Page 2+ of search results shows duplicate items.
|
|
1817
|
+
details: "Page 2+ of search results shows duplicate items.",
|
|
1700
1818
|
category: "bug",
|
|
1701
1819
|
priority: "default",
|
|
1702
1820
|
status: "not_started",
|
|
1703
1821
|
up_next: false,
|
|
1822
|
+
tags: ["search"],
|
|
1704
1823
|
notes: "",
|
|
1705
1824
|
days_ago: 5,
|
|
1706
1825
|
updated_ago: 5
|
|
1707
1826
|
},
|
|
1708
1827
|
{
|
|
1709
1828
|
title: "Cart badge count not updating after item removal",
|
|
1710
|
-
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.",
|
|
1711
1830
|
category: "bug",
|
|
1712
1831
|
priority: "high",
|
|
1713
1832
|
status: "not_started",
|
|
1714
1833
|
up_next: false,
|
|
1834
|
+
tags: ["cart", "ui"],
|
|
1715
1835
|
notes: "",
|
|
1716
1836
|
days_ago: 2,
|
|
1717
1837
|
updated_ago: 2
|
|
1718
1838
|
},
|
|
1719
1839
|
{
|
|
1720
1840
|
title: "Add order tracking page for customers",
|
|
1721
|
-
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.",
|
|
1722
1842
|
category: "feature",
|
|
1723
1843
|
priority: "default",
|
|
1724
1844
|
status: "not_started",
|
|
1725
1845
|
up_next: false,
|
|
1846
|
+
tags: ["orders", "ux"],
|
|
1726
1847
|
notes: "",
|
|
1727
1848
|
days_ago: 6,
|
|
1728
1849
|
updated_ago: 6
|
|
@@ -1734,17 +1855,19 @@ var SCENARIO_5 = [
|
|
|
1734
1855
|
priority: "default",
|
|
1735
1856
|
status: "not_started",
|
|
1736
1857
|
up_next: false,
|
|
1858
|
+
tags: ["admin", "reviews"],
|
|
1737
1859
|
notes: "",
|
|
1738
1860
|
days_ago: 7,
|
|
1739
1861
|
updated_ago: 7
|
|
1740
1862
|
},
|
|
1741
1863
|
{
|
|
1742
1864
|
title: "Add rate limiting to public API endpoints",
|
|
1743
|
-
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.",
|
|
1744
1866
|
category: "task",
|
|
1745
1867
|
priority: "default",
|
|
1746
1868
|
status: "not_started",
|
|
1747
1869
|
up_next: false,
|
|
1870
|
+
tags: ["security", "api"],
|
|
1748
1871
|
notes: "",
|
|
1749
1872
|
days_ago: 8,
|
|
1750
1873
|
updated_ago: 8
|
|
@@ -1756,6 +1879,7 @@ var SCENARIO_5 = [
|
|
|
1756
1879
|
priority: "default",
|
|
1757
1880
|
status: "not_started",
|
|
1758
1881
|
up_next: false,
|
|
1882
|
+
tags: ["infrastructure", "devops"],
|
|
1759
1883
|
notes: "",
|
|
1760
1884
|
days_ago: 9,
|
|
1761
1885
|
updated_ago: 9
|
|
@@ -1767,6 +1891,7 @@ var SCENARIO_5 = [
|
|
|
1767
1891
|
priority: "low",
|
|
1768
1892
|
status: "not_started",
|
|
1769
1893
|
up_next: false,
|
|
1894
|
+
tags: ["cleanup"],
|
|
1770
1895
|
notes: "",
|
|
1771
1896
|
days_ago: 12,
|
|
1772
1897
|
updated_ago: 12
|
|
@@ -1778,6 +1903,7 @@ var SCENARIO_5 = [
|
|
|
1778
1903
|
priority: "low",
|
|
1779
1904
|
status: "not_started",
|
|
1780
1905
|
up_next: false,
|
|
1906
|
+
tags: ["cleanup", "database"],
|
|
1781
1907
|
notes: "",
|
|
1782
1908
|
days_ago: 14,
|
|
1783
1909
|
updated_ago: 14
|
|
@@ -1791,6 +1917,7 @@ var SCENARIO_6 = [
|
|
|
1791
1917
|
priority: "highest",
|
|
1792
1918
|
status: "started",
|
|
1793
1919
|
up_next: true,
|
|
1920
|
+
tags: ["real-time", "orders", "websocket"],
|
|
1794
1921
|
notes: notesJson([
|
|
1795
1922
|
{ text: "Set up the WebSocket server using ws library. Basic connection lifecycle working \u2014 connect, heartbeat, disconnect with cleanup.", days_ago: 3 },
|
|
1796
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 },
|
|
@@ -1806,6 +1933,7 @@ var SCENARIO_6 = [
|
|
|
1806
1933
|
priority: "high",
|
|
1807
1934
|
status: "started",
|
|
1808
1935
|
up_next: true,
|
|
1936
|
+
tags: ["performance", "memory", "search"],
|
|
1809
1937
|
notes: notesJson([{ text: "Heap snapshot shows the BatchProcessor holding references to completed batches. The onComplete callbacks are never cleaned up.", days_ago: 1 }]),
|
|
1810
1938
|
days_ago: 5,
|
|
1811
1939
|
updated_ago: 1
|
|
@@ -1817,17 +1945,19 @@ var SCENARIO_6 = [
|
|
|
1817
1945
|
priority: "high",
|
|
1818
1946
|
status: "not_started",
|
|
1819
1947
|
up_next: true,
|
|
1948
|
+
tags: ["testing", "payments"],
|
|
1820
1949
|
notes: "",
|
|
1821
1950
|
days_ago: 4,
|
|
1822
1951
|
updated_ago: 4
|
|
1823
1952
|
},
|
|
1824
1953
|
{
|
|
1825
1954
|
title: "Add product recommendations based on purchase history",
|
|
1826
|
-
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.',
|
|
1827
1956
|
category: "feature",
|
|
1828
1957
|
priority: "default",
|
|
1829
1958
|
status: "completed",
|
|
1830
1959
|
up_next: false,
|
|
1960
|
+
tags: ["ml", "recommendations", "product-pages"],
|
|
1831
1961
|
notes: notesJson([
|
|
1832
1962
|
{ text: "Implemented a simple collaborative filtering algorithm. Computes item-item similarity from co-purchase frequency in the last 90 days.", days_ago: 5 },
|
|
1833
1963
|
{ 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 +1968,15 @@ var SCENARIO_6 = [
|
|
|
1838
1968
|
},
|
|
1839
1969
|
{
|
|
1840
1970
|
title: "Migrate static assets to CDN",
|
|
1841
|
-
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.",
|
|
1842
1972
|
category: "task",
|
|
1843
1973
|
priority: "default",
|
|
1844
1974
|
status: "verified",
|
|
1845
1975
|
up_next: false,
|
|
1976
|
+
tags: ["infrastructure", "cdn", "performance"],
|
|
1846
1977
|
notes: notesJson([
|
|
1847
|
-
{ text: "Configured CloudFront distribution with S3 origin. Migrated all product images (42GB)
|
|
1848
|
-
{ 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 }
|
|
1849
1980
|
]),
|
|
1850
1981
|
days_ago: 16,
|
|
1851
1982
|
updated_ago: 5,
|
|
@@ -1859,6 +1990,7 @@ var SCENARIO_6 = [
|
|
|
1859
1990
|
priority: "default",
|
|
1860
1991
|
status: "not_started",
|
|
1861
1992
|
up_next: false,
|
|
1993
|
+
tags: ["navigation", "ui"],
|
|
1862
1994
|
notes: "",
|
|
1863
1995
|
days_ago: 8,
|
|
1864
1996
|
updated_ago: 8
|
|
@@ -1867,88 +1999,96 @@ var SCENARIO_6 = [
|
|
|
1867
1999
|
var SCENARIO_7 = [
|
|
1868
2000
|
{
|
|
1869
2001
|
title: "Implement product search autocomplete",
|
|
1870
|
-
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.",
|
|
1871
2003
|
category: "feature",
|
|
1872
2004
|
priority: "highest",
|
|
1873
2005
|
status: "not_started",
|
|
1874
2006
|
up_next: true,
|
|
2007
|
+
tags: ["search", "ux"],
|
|
1875
2008
|
notes: "",
|
|
1876
2009
|
days_ago: 2,
|
|
1877
2010
|
updated_ago: 2
|
|
1878
2011
|
},
|
|
1879
2012
|
{
|
|
1880
2013
|
title: "Fix broken password reset flow for SSO users",
|
|
1881
|
-
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.",
|
|
1882
2015
|
category: "bug",
|
|
1883
2016
|
priority: "high",
|
|
1884
2017
|
status: "not_started",
|
|
1885
2018
|
up_next: true,
|
|
2019
|
+
tags: ["auth", "sso"],
|
|
1886
2020
|
notes: "",
|
|
1887
2021
|
days_ago: 3,
|
|
1888
2022
|
updated_ago: 3
|
|
1889
2023
|
},
|
|
1890
2024
|
{
|
|
1891
2025
|
title: "Add support for gift cards at checkout",
|
|
1892
|
-
details: "
|
|
2026
|
+
details: "Support gift card codes during checkout with partial redemption and balance tracking.",
|
|
1893
2027
|
category: "feature",
|
|
1894
2028
|
priority: "default",
|
|
1895
2029
|
status: "not_started",
|
|
1896
2030
|
up_next: false,
|
|
2031
|
+
tags: ["checkout", "payments"],
|
|
1897
2032
|
notes: "",
|
|
1898
2033
|
days_ago: 5,
|
|
1899
2034
|
updated_ago: 5
|
|
1900
2035
|
},
|
|
1901
2036
|
{
|
|
1902
2037
|
title: "Investigate slow query on order history page",
|
|
1903
|
-
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.",
|
|
1904
2039
|
category: "investigation",
|
|
1905
2040
|
priority: "high",
|
|
1906
2041
|
status: "not_started",
|
|
1907
2042
|
up_next: false,
|
|
2043
|
+
tags: ["performance", "database"],
|
|
1908
2044
|
notes: "",
|
|
1909
2045
|
days_ago: 4,
|
|
1910
2046
|
updated_ago: 4
|
|
1911
2047
|
},
|
|
1912
2048
|
{
|
|
1913
2049
|
title: "Refactor authentication middleware to support API keys",
|
|
1914
|
-
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.",
|
|
1915
2051
|
category: "task",
|
|
1916
2052
|
priority: "high",
|
|
1917
2053
|
status: "started",
|
|
1918
2054
|
up_next: true,
|
|
1919
|
-
|
|
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 }]),
|
|
1920
2057
|
days_ago: 4,
|
|
1921
2058
|
updated_ago: 0.5
|
|
1922
2059
|
},
|
|
1923
2060
|
{
|
|
1924
2061
|
title: "Fix cart not clearing after successful checkout",
|
|
1925
|
-
details: "
|
|
2062
|
+
details: "The clearCart() call is inside a catch block by mistake.",
|
|
1926
2063
|
category: "bug",
|
|
1927
2064
|
priority: "highest",
|
|
1928
2065
|
status: "started",
|
|
1929
2066
|
up_next: true,
|
|
2067
|
+
tags: ["checkout", "cart"],
|
|
1930
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 }]),
|
|
1931
2069
|
days_ago: 1,
|
|
1932
2070
|
updated_ago: 0.3
|
|
1933
2071
|
},
|
|
1934
2072
|
{
|
|
1935
2073
|
title: "Update shipping rate calculation for oversized items",
|
|
1936
|
-
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.",
|
|
1937
2075
|
category: "requirement_change",
|
|
1938
2076
|
priority: "default",
|
|
1939
2077
|
status: "started",
|
|
1940
2078
|
up_next: false,
|
|
1941
|
-
|
|
2079
|
+
tags: ["shipping", "pricing"],
|
|
2080
|
+
notes: notesJson([{ text: "Implemented dim weight formula. Comparing rates against the carrier API.", days_ago: 1 }]),
|
|
1942
2081
|
days_ago: 6,
|
|
1943
2082
|
updated_ago: 1
|
|
1944
2083
|
},
|
|
1945
2084
|
{
|
|
1946
2085
|
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.
|
|
2086
|
+
details: "Write Playwright tests covering: add to cart, apply coupon, enter shipping, pay, and confirm.",
|
|
1948
2087
|
category: "task",
|
|
1949
2088
|
priority: "default",
|
|
1950
2089
|
status: "completed",
|
|
1951
2090
|
up_next: false,
|
|
2091
|
+
tags: ["testing", "e2e"],
|
|
1952
2092
|
notes: notesJson([{ text: "Wrote 8 E2E tests covering the full checkout flow including coupon application and payment decline handling.", days_ago: 1 }]),
|
|
1953
2093
|
days_ago: 8,
|
|
1954
2094
|
updated_ago: 1,
|
|
@@ -1956,29 +2096,54 @@ var SCENARIO_7 = [
|
|
|
1956
2096
|
},
|
|
1957
2097
|
{
|
|
1958
2098
|
title: "Fix product image carousel swipe on mobile",
|
|
1959
|
-
details: "Swipe gestures
|
|
2099
|
+
details: "Swipe gestures conflict with the browser back gesture.",
|
|
1960
2100
|
category: "bug",
|
|
1961
2101
|
priority: "default",
|
|
1962
2102
|
status: "completed",
|
|
1963
2103
|
up_next: false,
|
|
1964
|
-
|
|
2104
|
+
tags: ["mobile", "ui"],
|
|
2105
|
+
notes: notesJson([{ text: "Added a 30px horizontal threshold. Tested on iOS Safari and Chrome Android.", days_ago: 2 }]),
|
|
1965
2106
|
days_ago: 7,
|
|
1966
2107
|
updated_ago: 2,
|
|
1967
2108
|
completed_ago: 2
|
|
1968
2109
|
},
|
|
1969
2110
|
{
|
|
1970
2111
|
title: "Set up log aggregation with structured JSON logging",
|
|
1971
|
-
details: "Replace console.log calls with
|
|
2112
|
+
details: "Replace console.log calls with pino. Send logs to a central aggregation service.",
|
|
1972
2113
|
category: "task",
|
|
1973
2114
|
priority: "low",
|
|
1974
2115
|
status: "completed",
|
|
1975
2116
|
up_next: false,
|
|
1976
|
-
|
|
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 }]),
|
|
1977
2119
|
days_ago: 10,
|
|
1978
2120
|
updated_ago: 3,
|
|
1979
2121
|
completed_ago: 3
|
|
1980
2122
|
}
|
|
1981
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
|
+
}
|
|
1982
2147
|
var SCENARIO_DATA = {
|
|
1983
2148
|
1: SCENARIO_1,
|
|
1984
2149
|
2: SCENARIO_2,
|
|
@@ -1986,8 +2151,29 @@ var SCENARIO_DATA = {
|
|
|
1986
2151
|
4: SCENARIO_4,
|
|
1987
2152
|
5: SCENARIO_5,
|
|
1988
2153
|
6: SCENARIO_6,
|
|
1989
|
-
7: SCENARIO_7
|
|
2154
|
+
7: SCENARIO_7,
|
|
2155
|
+
8: SCENARIO_8
|
|
1990
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
|
+
];
|
|
1991
2177
|
async function seedDemoData(scenario) {
|
|
1992
2178
|
const db2 = await getDb();
|
|
1993
2179
|
const tickets = SCENARIO_DATA[scenario];
|
|
@@ -1999,12 +2185,19 @@ async function seedDemoData(scenario) {
|
|
|
1999
2185
|
const updatedAt = daysAgo(t.updated_ago);
|
|
2000
2186
|
const completedAt = t.completed_ago !== void 0 ? daysAgo(t.completed_ago) : null;
|
|
2001
2187
|
const verifiedAt = t.verified_ago !== void 0 ? daysAgo(t.verified_ago) : null;
|
|
2188
|
+
const tags = JSON.stringify(t.tags);
|
|
2002
2189
|
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]);
|
|
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]);
|
|
2006
2193
|
}
|
|
2007
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
|
+
}
|
|
2008
2201
|
if (scenario === 6) {
|
|
2009
2202
|
await db2.query(`UPDATE settings SET value = 'bottom' WHERE key = 'detail_position'`);
|
|
2010
2203
|
await db2.query(`UPDATE settings SET value = '280' WHERE key = 'detail_height'`);
|
|
@@ -2012,6 +2205,11 @@ async function seedDemoData(scenario) {
|
|
|
2012
2205
|
if (scenario === 7) {
|
|
2013
2206
|
await db2.query(`INSERT INTO settings (key, value) VALUES ('layout', 'columns') ON CONFLICT (key) DO UPDATE SET value = 'columns'`);
|
|
2014
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
|
+
}
|
|
2015
2213
|
}
|
|
2016
2214
|
|
|
2017
2215
|
// src/cli.ts
|
|
@@ -2020,15 +2218,15 @@ init_gitignore();
|
|
|
2020
2218
|
// src/server.ts
|
|
2021
2219
|
import { serve } from "@hono/node-server";
|
|
2022
2220
|
import { exec } from "child_process";
|
|
2023
|
-
import { existsSync as
|
|
2221
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
2024
2222
|
import { Hono as Hono4 } from "hono";
|
|
2025
|
-
import { dirname, join as
|
|
2223
|
+
import { dirname, join as join10 } from "path";
|
|
2026
2224
|
import { fileURLToPath } from "url";
|
|
2027
2225
|
|
|
2028
2226
|
// src/routes/api.ts
|
|
2029
|
-
import { existsSync as
|
|
2227
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, rmSync as rmSync5 } from "fs";
|
|
2030
2228
|
import { Hono } from "hono";
|
|
2031
|
-
import { basename, extname, join as
|
|
2229
|
+
import { basename, extname, join as join9, relative as relative2 } from "path";
|
|
2032
2230
|
|
|
2033
2231
|
// src/skills.ts
|
|
2034
2232
|
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
@@ -2479,8 +2677,8 @@ function notifyChange() {
|
|
|
2479
2677
|
changeVersion++;
|
|
2480
2678
|
const waiters = pollWaiters;
|
|
2481
2679
|
pollWaiters = [];
|
|
2482
|
-
for (const
|
|
2483
|
-
|
|
2680
|
+
for (const resolve3 of waiters) {
|
|
2681
|
+
resolve3(changeVersion);
|
|
2484
2682
|
}
|
|
2485
2683
|
}
|
|
2486
2684
|
apiRoutes.get("/poll", async (c) => {
|
|
@@ -2489,11 +2687,11 @@ apiRoutes.get("/poll", async (c) => {
|
|
|
2489
2687
|
return c.json({ version: changeVersion });
|
|
2490
2688
|
}
|
|
2491
2689
|
const version = await Promise.race([
|
|
2492
|
-
new Promise((
|
|
2493
|
-
pollWaiters.push(
|
|
2690
|
+
new Promise((resolve3) => {
|
|
2691
|
+
pollWaiters.push(resolve3);
|
|
2494
2692
|
}),
|
|
2495
|
-
new Promise((
|
|
2496
|
-
setTimeout(() =>
|
|
2693
|
+
new Promise((resolve3) => {
|
|
2694
|
+
setTimeout(() => resolve3(changeVersion), 3e4);
|
|
2497
2695
|
})
|
|
2498
2696
|
]);
|
|
2499
2697
|
return c.json({ version });
|
|
@@ -2548,11 +2746,24 @@ apiRoutes.delete("/tickets/:id", async (c) => {
|
|
|
2548
2746
|
notifyChange();
|
|
2549
2747
|
return c.json({ ok: true });
|
|
2550
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
|
+
});
|
|
2551
2762
|
apiRoutes.patch("/tickets/:id/notes/:noteId", async (c) => {
|
|
2552
2763
|
const id = parseInt(c.req.param("id"), 10);
|
|
2553
|
-
const
|
|
2764
|
+
const noteId2 = c.req.param("noteId");
|
|
2554
2765
|
const body = await c.req.json();
|
|
2555
|
-
const notes = await editNote(id,
|
|
2766
|
+
const notes = await editNote(id, noteId2, body.text);
|
|
2556
2767
|
if (!notes) return c.json({ error: "Not found" }, 404);
|
|
2557
2768
|
scheduleAllSync();
|
|
2558
2769
|
notifyChange();
|
|
@@ -2560,8 +2771,8 @@ apiRoutes.patch("/tickets/:id/notes/:noteId", async (c) => {
|
|
|
2560
2771
|
});
|
|
2561
2772
|
apiRoutes.delete("/tickets/:id/notes/:noteId", async (c) => {
|
|
2562
2773
|
const id = parseInt(c.req.param("id"), 10);
|
|
2563
|
-
const
|
|
2564
|
-
const notes = await deleteNote(id,
|
|
2774
|
+
const noteId2 = c.req.param("noteId");
|
|
2775
|
+
const notes = await deleteNote(id, noteId2);
|
|
2565
2776
|
if (!notes) return c.json({ error: "Not found" }, 404);
|
|
2566
2777
|
scheduleAllSync();
|
|
2567
2778
|
notifyChange();
|
|
@@ -2660,12 +2871,12 @@ apiRoutes.post("/tickets/:id/attachments", async (c) => {
|
|
|
2660
2871
|
const ext = extname(originalName);
|
|
2661
2872
|
const baseName = basename(originalName, ext);
|
|
2662
2873
|
const storedName = `${ticket.ticket_number}_${baseName}${ext}`;
|
|
2663
|
-
const attachDir =
|
|
2874
|
+
const attachDir = join9(dataDir2, "attachments");
|
|
2664
2875
|
mkdirSync4(attachDir, { recursive: true });
|
|
2665
|
-
const storedPath =
|
|
2876
|
+
const storedPath = join9(attachDir, storedName);
|
|
2666
2877
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
2667
|
-
const { writeFileSync:
|
|
2668
|
-
|
|
2878
|
+
const { writeFileSync: writeFileSync8 } = await import("fs");
|
|
2879
|
+
writeFileSync8(storedPath, buffer);
|
|
2669
2880
|
const attachment = await addAttachment(id, originalName, storedPath);
|
|
2670
2881
|
scheduleAllSync();
|
|
2671
2882
|
notifyChange();
|
|
@@ -2687,7 +2898,7 @@ apiRoutes.post("/attachments/:id/reveal", async (c) => {
|
|
|
2687
2898
|
const id = parseInt(c.req.param("id"), 10);
|
|
2688
2899
|
const attachment = await getAttachment(id);
|
|
2689
2900
|
if (!attachment) return c.json({ error: "Not found" }, 404);
|
|
2690
|
-
if (!
|
|
2901
|
+
if (!existsSync7(attachment.stored_path)) return c.json({ error: "File not found on disk" }, 404);
|
|
2691
2902
|
const { execFile } = await import("child_process");
|
|
2692
2903
|
const { dirname: dirname3 } = await import("path");
|
|
2693
2904
|
const platform = process.platform;
|
|
@@ -2703,12 +2914,12 @@ apiRoutes.post("/attachments/:id/reveal", async (c) => {
|
|
|
2703
2914
|
apiRoutes.get("/attachments/file/*", async (c) => {
|
|
2704
2915
|
const filePath = c.req.path.replace("/api/attachments/file/", "");
|
|
2705
2916
|
const dataDir2 = c.get("dataDir");
|
|
2706
|
-
const fullPath =
|
|
2707
|
-
if (!
|
|
2917
|
+
const fullPath = join9(dataDir2, "attachments", filePath);
|
|
2918
|
+
if (!existsSync7(fullPath)) {
|
|
2708
2919
|
return c.json({ error: "File not found" }, 404);
|
|
2709
2920
|
}
|
|
2710
|
-
const { readFileSync:
|
|
2711
|
-
const content =
|
|
2921
|
+
const { readFileSync: readFileSync9 } = await import("fs");
|
|
2922
|
+
const content = readFileSync9(fullPath);
|
|
2712
2923
|
const ext = extname(fullPath).toLowerCase();
|
|
2713
2924
|
const mimeTypes = {
|
|
2714
2925
|
".png": "image/png",
|
|
@@ -2788,7 +2999,7 @@ apiRoutes.patch("/file-settings", async (c) => {
|
|
|
2788
2999
|
apiRoutes.get("/worklist-info", (c) => {
|
|
2789
3000
|
const dataDir2 = c.get("dataDir");
|
|
2790
3001
|
const cwd = process.cwd();
|
|
2791
|
-
const worklistRel = relative2(cwd,
|
|
3002
|
+
const worklistRel = relative2(cwd, join9(dataDir2, "worklist.md"));
|
|
2792
3003
|
const prompt = `Read ${worklistRel} for current work items.`;
|
|
2793
3004
|
ensureSkills();
|
|
2794
3005
|
const skillCreated = consumeSkillsCreatedFlag();
|
|
@@ -2828,14 +3039,68 @@ apiRoutes.post("/gitignore/add", async (c) => {
|
|
|
2828
3039
|
ensureGitignore2(process.cwd());
|
|
2829
3040
|
return c.json({ ok: true });
|
|
2830
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
|
+
});
|
|
2831
3096
|
apiRoutes.post("/print", async (c) => {
|
|
2832
3097
|
const { html } = await c.req.json();
|
|
2833
|
-
const { writeFileSync:
|
|
3098
|
+
const { writeFileSync: writeFileSync8 } = await import("fs");
|
|
2834
3099
|
const { tmpdir: tmpdir2 } = await import("os");
|
|
2835
3100
|
const { join: pathJoin } = await import("path");
|
|
2836
3101
|
const { execFile } = await import("child_process");
|
|
2837
3102
|
const tmpPath = pathJoin(tmpdir2(), `hotsheet-print-${Date.now()}.html`);
|
|
2838
|
-
|
|
3103
|
+
writeFileSync8(tmpPath, html, "utf-8");
|
|
2839
3104
|
const platform = process.platform;
|
|
2840
3105
|
if (platform === "darwin") {
|
|
2841
3106
|
execFile("open", [tmpPath]);
|
|
@@ -3165,6 +3430,13 @@ pageRoutes.get("/", (c) => {
|
|
|
3165
3430
|
] }),
|
|
3166
3431
|
/* @__PURE__ */ jsx("div", { className: "app-body", children: [
|
|
3167
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
|
+
] }) }),
|
|
3168
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: [
|
|
3169
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: [
|
|
3170
3442
|
/* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2" }),
|
|
@@ -3193,7 +3465,7 @@ pageRoutes.get("/", (c) => {
|
|
|
3193
3465
|
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "archive", children: "Archive" }),
|
|
3194
3466
|
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "trash", children: "Trash" })
|
|
3195
3467
|
] }),
|
|
3196
|
-
/* @__PURE__ */ jsx("div", { className: "sidebar-section", children: [
|
|
3468
|
+
/* @__PURE__ */ jsx("div", { className: "sidebar-section", id: "sidebar-categories", children: [
|
|
3197
3469
|
/* @__PURE__ */ jsx("div", { className: "sidebar-label", children: "Category" }),
|
|
3198
3470
|
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "category:issue", children: [
|
|
3199
3471
|
/* @__PURE__ */ jsx("span", { className: "cat-dot", style: "background:#6b7280" }),
|
|
@@ -3346,7 +3618,10 @@ pageRoutes.get("/", (c) => {
|
|
|
3346
3618
|
" close"
|
|
3347
3619
|
] })
|
|
3348
3620
|
] }),
|
|
3349
|
-
/* @__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
|
+
] })
|
|
3350
3625
|
] })
|
|
3351
3626
|
] }),
|
|
3352
3627
|
/* @__PURE__ */ jsx("div", { className: "settings-overlay", id: "settings-overlay", style: "display:none", children: /* @__PURE__ */ jsx("div", { className: "settings-dialog", children: [
|
|
@@ -3408,7 +3683,24 @@ pageRoutes.get("/", (c) => {
|
|
|
3408
3683
|
/* @__PURE__ */ jsx("div", { className: "settings-field", children: [
|
|
3409
3684
|
/* @__PURE__ */ jsx("label", { children: "Auto-clear verified after (days)" }),
|
|
3410
3685
|
/* @__PURE__ */ jsx("input", { type: "number", id: "settings-verified-days", min: "1", value: "30" })
|
|
3411
|
-
] })
|
|
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
|
+
] }) })
|
|
3412
3704
|
] }),
|
|
3413
3705
|
/* @__PURE__ */ jsx("div", { className: "settings-tab-panel", "data-panel": "categories", children: [
|
|
3414
3706
|
/* @__PURE__ */ jsx("div", { className: "settings-section-header", children: [
|
|
@@ -3444,11 +3736,11 @@ pageRoutes.get("/", (c) => {
|
|
|
3444
3736
|
});
|
|
3445
3737
|
|
|
3446
3738
|
// src/server.ts
|
|
3447
|
-
function tryServe(
|
|
3448
|
-
return new Promise((
|
|
3449
|
-
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 });
|
|
3450
3742
|
server.on("listening", () => {
|
|
3451
|
-
|
|
3743
|
+
resolve3(port2);
|
|
3452
3744
|
});
|
|
3453
3745
|
server.on("error", (err) => {
|
|
3454
3746
|
reject(err);
|
|
@@ -3462,20 +3754,20 @@ async function startServer(port2, dataDir2, options) {
|
|
|
3462
3754
|
await next();
|
|
3463
3755
|
});
|
|
3464
3756
|
const selfDir = dirname(fileURLToPath(import.meta.url));
|
|
3465
|
-
const distDir =
|
|
3757
|
+
const distDir = existsSync8(join10(selfDir, "client", "styles.css")) ? join10(selfDir, "client") : join10(selfDir, "..", "dist", "client");
|
|
3466
3758
|
app.get("/static/styles.css", (c) => {
|
|
3467
|
-
const css =
|
|
3759
|
+
const css = readFileSync7(join10(distDir, "styles.css"), "utf-8");
|
|
3468
3760
|
return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
|
|
3469
3761
|
});
|
|
3470
3762
|
app.get("/static/app.js", (c) => {
|
|
3471
|
-
const js =
|
|
3763
|
+
const js = readFileSync7(join10(distDir, "app.global.js"), "utf-8");
|
|
3472
3764
|
return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
|
|
3473
3765
|
});
|
|
3474
3766
|
app.get("/static/assets/:filename", (c) => {
|
|
3475
3767
|
const filename = c.req.param("filename");
|
|
3476
|
-
const filePath =
|
|
3477
|
-
if (!
|
|
3478
|
-
const content =
|
|
3768
|
+
const filePath = join10(distDir, "assets", filename);
|
|
3769
|
+
if (!existsSync8(filePath)) return c.notFound();
|
|
3770
|
+
const content = readFileSync7(filePath);
|
|
3479
3771
|
const ext = filename.split(".").pop();
|
|
3480
3772
|
const mimeTypes = { png: "image/png", jpg: "image/jpeg", svg: "image/svg+xml" };
|
|
3481
3773
|
return new Response(content, { headers: { "Content-Type": mimeTypes[ext || ""] || "application/octet-stream", "Cache-Control": "max-age=86400" } });
|
|
@@ -3519,18 +3811,18 @@ async function startServer(port2, dataDir2, options) {
|
|
|
3519
3811
|
}
|
|
3520
3812
|
|
|
3521
3813
|
// src/update-check.ts
|
|
3522
|
-
import { existsSync as
|
|
3814
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
3523
3815
|
import { get } from "https";
|
|
3524
3816
|
import { homedir } from "os";
|
|
3525
|
-
import { dirname as dirname2, join as
|
|
3817
|
+
import { dirname as dirname2, join as join11 } from "path";
|
|
3526
3818
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3527
|
-
var DATA_DIR =
|
|
3528
|
-
var CHECK_FILE =
|
|
3819
|
+
var DATA_DIR = join11(homedir(), ".hotsheet");
|
|
3820
|
+
var CHECK_FILE = join11(DATA_DIR, "last-update-check");
|
|
3529
3821
|
var PACKAGE_NAME = "hotsheet";
|
|
3530
3822
|
function getCurrentVersion() {
|
|
3531
3823
|
try {
|
|
3532
3824
|
const dir = dirname2(fileURLToPath2(import.meta.url));
|
|
3533
|
-
const pkg = JSON.parse(
|
|
3825
|
+
const pkg = JSON.parse(readFileSync8(join11(dir, "..", "package.json"), "utf-8"));
|
|
3534
3826
|
return pkg.version;
|
|
3535
3827
|
} catch {
|
|
3536
3828
|
return "0.0.0";
|
|
@@ -3538,8 +3830,8 @@ function getCurrentVersion() {
|
|
|
3538
3830
|
}
|
|
3539
3831
|
function getLastCheckDate() {
|
|
3540
3832
|
try {
|
|
3541
|
-
if (
|
|
3542
|
-
return
|
|
3833
|
+
if (existsSync9(CHECK_FILE)) {
|
|
3834
|
+
return readFileSync8(CHECK_FILE, "utf-8").trim();
|
|
3543
3835
|
}
|
|
3544
3836
|
} catch {
|
|
3545
3837
|
}
|
|
@@ -3547,7 +3839,7 @@ function getLastCheckDate() {
|
|
|
3547
3839
|
}
|
|
3548
3840
|
function saveCheckDate() {
|
|
3549
3841
|
mkdirSync5(DATA_DIR, { recursive: true });
|
|
3550
|
-
|
|
3842
|
+
writeFileSync7(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
|
|
3551
3843
|
}
|
|
3552
3844
|
function isFirstUseToday() {
|
|
3553
3845
|
const last = getLastCheckDate();
|
|
@@ -3556,10 +3848,10 @@ function isFirstUseToday() {
|
|
|
3556
3848
|
return last !== today;
|
|
3557
3849
|
}
|
|
3558
3850
|
function fetchLatestVersion() {
|
|
3559
|
-
return new Promise((
|
|
3851
|
+
return new Promise((resolve3) => {
|
|
3560
3852
|
const req = get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { timeout: 5e3 }, (res) => {
|
|
3561
3853
|
if (res.statusCode !== 200) {
|
|
3562
|
-
|
|
3854
|
+
resolve3(null);
|
|
3563
3855
|
return;
|
|
3564
3856
|
}
|
|
3565
3857
|
let data = "";
|
|
@@ -3568,18 +3860,18 @@ function fetchLatestVersion() {
|
|
|
3568
3860
|
});
|
|
3569
3861
|
res.on("end", () => {
|
|
3570
3862
|
try {
|
|
3571
|
-
|
|
3863
|
+
resolve3(JSON.parse(data).version);
|
|
3572
3864
|
} catch {
|
|
3573
|
-
|
|
3865
|
+
resolve3(null);
|
|
3574
3866
|
}
|
|
3575
3867
|
});
|
|
3576
3868
|
});
|
|
3577
3869
|
req.on("error", () => {
|
|
3578
|
-
|
|
3870
|
+
resolve3(null);
|
|
3579
3871
|
});
|
|
3580
3872
|
req.on("timeout", () => {
|
|
3581
3873
|
req.destroy();
|
|
3582
|
-
|
|
3874
|
+
resolve3(null);
|
|
3583
3875
|
});
|
|
3584
3876
|
});
|
|
3585
3877
|
}
|
|
@@ -3652,7 +3944,7 @@ Examples:
|
|
|
3652
3944
|
function parseArgs(argv) {
|
|
3653
3945
|
const args = argv.slice(2);
|
|
3654
3946
|
let port2 = 4174;
|
|
3655
|
-
let dataDir2 =
|
|
3947
|
+
let dataDir2 = join12(process.cwd(), ".hotsheet");
|
|
3656
3948
|
let demo = null;
|
|
3657
3949
|
let forceUpdateCheck = false;
|
|
3658
3950
|
let noOpen = false;
|
|
@@ -3681,7 +3973,7 @@ function parseArgs(argv) {
|
|
|
3681
3973
|
}
|
|
3682
3974
|
break;
|
|
3683
3975
|
case "--data-dir":
|
|
3684
|
-
dataDir2 =
|
|
3976
|
+
dataDir2 = resolve2(args[++i]);
|
|
3685
3977
|
break;
|
|
3686
3978
|
case "--check-for-updates":
|
|
3687
3979
|
forceUpdateCheck = true;
|
|
@@ -3719,7 +4011,7 @@ async function main() {
|
|
|
3719
4011
|
}
|
|
3720
4012
|
process.exit(1);
|
|
3721
4013
|
}
|
|
3722
|
-
dataDir2 =
|
|
4014
|
+
dataDir2 = join12(tmpdir(), `hotsheet-demo-${demo}-${Date.now()}`);
|
|
3723
4015
|
console.log(`
|
|
3724
4016
|
DEMO MODE: ${scenario.label}
|
|
3725
4017
|
`);
|