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/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 join11, resolve } from "path";
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, noteId, text) {
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 === noteId);
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, noteId) {
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 === noteId);
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 category view" },
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) => ({ text: e.text, created_at: daysAgo(e.days_ago) })));
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. Need to trigger re-index on product save.",
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: 'Use WebSocket connections to push stock level changes to the product page. Show "Only X left" badges.',
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. Support mp4 and embedded YouTube URLs.",
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. Needs URL rewriting for existing images.",
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: "High-priority requirement change from product. Many users abandon at the registration step. Allow checkout with just email.",
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: "Current in-memory sessions don't survive restarts. Evaluate Redis and Memcached for persistence, speed, and ops complexity.",
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 in the funnel mobile users are dropping off.",
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. Need to maintain cache coherency on product updates.",
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. Support create, update, and skip-on-conflict modes.",
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 for malformed data.", days_ago: 2 }
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 with proper foreign keys.",
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. Old JSON column can be dropped in next release.", days_ago: 3 }
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. Use table-based layout.",
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, the checkout page shows a generic 500 error. Show a retry prompt instead.",
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. The OFFSET calculation is wrong when filters change.",
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. The client state isn't being updated.",
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. Pull data from the shipping provider API.",
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. Use a sliding window algorithm. Target: 100 req/min for anonymous, 500 for authenticated.",
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 on order history.',
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. Reduces server load and improves page load times globally.",
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) using the AWS CLI sync command.", days_ago: 7 },
1848
- { text: "Updated asset URLs in the application to use the CDN domain. Cache hit rate is at 94% after 48 hours. TTFB improved from 240ms to 35ms for static assets.", days_ago: 5 }
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. Show top 5 matches with thumbnails.",
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. Should redirect them to their identity provider instead.",
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: "Customers should be able to apply gift card codes during checkout. Support partial redemption and balance tracking.",
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. Profile the query and add proper indexing.",
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. Extract auth into a strategy pattern.",
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
- notes: notesJson([{ text: "Created the AuthStrategy interface and migrated session auth to use it. Working on the API key strategy next.", days_ago: 0.5 }]),
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: "After a successful order placement, the cart retains all items. The clearCart() call is inside a catch block by mistake.",
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. Current flat-rate calculation undercharges.",
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
- notes: notesJson([{ text: "Implemented dim weight formula. Comparing rates against the carrier API to validate accuracy.", days_ago: 1 }]),
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. Cover happy path and key error cases.",
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 on the product image carousel conflict with the browser back gesture. Use a swipe threshold to disambiguate.",
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
- notes: notesJson([{ text: "Added a 30px horizontal threshold before initiating carousel swipe. Tested on iOS Safari and Chrome Android.", days_ago: 2 }]),
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 a structured logger (pino). Send logs to a central aggregation service for search and alerting.",
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
- notes: notesJson([{ text: "Replaced all console.log calls with pino. Configured log shipping to the aggregation service. Alert rules set for error-level logs.", days_ago: 3 }]),
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::timestamp, $10::timestamp, $11::timestamp, $12::timestamp)
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 existsSync7, readFileSync as readFileSync6 } from "fs";
2222
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
2024
2223
  import { Hono as Hono4 } from "hono";
2025
- import { dirname, join as join9 } from "path";
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 existsSync6, mkdirSync as mkdirSync4, rmSync as rmSync5 } from "fs";
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 join8, relative as relative2 } from "path";
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 resolve2 of waiters) {
2483
- resolve2(changeVersion);
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((resolve2) => {
2493
- pollWaiters.push(resolve2);
2691
+ new Promise((resolve3) => {
2692
+ pollWaiters.push(resolve3);
2494
2693
  }),
2495
- new Promise((resolve2) => {
2496
- setTimeout(() => resolve2(changeVersion), 3e4);
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 noteId = c.req.param("noteId");
2765
+ const noteId2 = c.req.param("noteId");
2554
2766
  const body = await c.req.json();
2555
- const notes = await editNote(id, noteId, body.text);
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 noteId = c.req.param("noteId");
2564
- const notes = await deleteNote(id, noteId);
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 = join8(dataDir2, "attachments");
2875
+ const attachDir = join9(dataDir2, "attachments");
2664
2876
  mkdirSync4(attachDir, { recursive: true });
2665
- const storedPath = join8(attachDir, storedName);
2877
+ const storedPath = join9(attachDir, storedName);
2666
2878
  const buffer = Buffer.from(await file.arrayBuffer());
2667
- const { writeFileSync: writeFileSync7 } = await import("fs");
2668
- writeFileSync7(storedPath, buffer);
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 (!existsSync6(attachment.stored_path)) return c.json({ error: "File not found on disk" }, 404);
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: dirname3 } = await import("path");
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", [dirname3(attachment.stored_path)]);
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 = join8(dataDir2, "attachments", filePath);
2707
- if (!existsSync6(fullPath)) {
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: readFileSync8 } = await import("fs");
2711
- const content = readFileSync8(fullPath);
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, join8(dataDir2, "worklist.md"));
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: writeFileSync7 } = await import("fs");
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
- writeFileSync7(tmpPath, html, "utf-8");
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", { id: "status-bar", className: "status-bar" })
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(fetch, port2) {
3448
- return new Promise((resolve2, reject) => {
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
- resolve2(port2);
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 = dirname(fileURLToPath(import.meta.url));
3465
- const distDir = existsSync7(join9(selfDir, "client", "styles.css")) ? join9(selfDir, "client") : join9(selfDir, "..", "dist", "client");
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 = readFileSync6(join9(distDir, "styles.css"), "utf-8");
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 = readFileSync6(join9(distDir, "app.global.js"), "utf-8");
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 = join9(distDir, "assets", filename);
3477
- if (!existsSync7(filePath)) return c.notFound();
3478
- const content = readFileSync6(filePath);
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 existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
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 dirname2, join as join10 } from "path";
3526
- import { fileURLToPath as fileURLToPath2 } from "url";
3527
- var DATA_DIR = join10(homedir(), ".hotsheet");
3528
- var CHECK_FILE = join10(DATA_DIR, "last-update-check");
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 = dirname2(fileURLToPath2(import.meta.url));
3533
- const pkg = JSON.parse(readFileSync7(join10(dir, "..", "package.json"), "utf-8"));
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 (existsSync8(CHECK_FILE)) {
3542
- return readFileSync7(CHECK_FILE, "utf-8").trim();
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
- writeFileSync6(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
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((resolve2) => {
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
- resolve2(null);
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
- resolve2(JSON.parse(data).version);
3864
+ resolve3(JSON.parse(data).version);
3572
3865
  } catch {
3573
- resolve2(null);
3866
+ resolve3(null);
3574
3867
  }
3575
3868
  });
3576
3869
  });
3577
3870
  req.on("error", () => {
3578
- resolve2(null);
3871
+ resolve3(null);
3579
3872
  });
3580
3873
  req.on("timeout", () => {
3581
3874
  req.destroy();
3582
- resolve2(null);
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 = join11(process.cwd(), ".hotsheet");
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 = resolve(args[++i]);
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 = join11(tmpdir(), `hotsheet-demo-${demo}-${Date.now()}`);
4015
+ dataDir2 = join12(tmpdir(), `hotsheet-demo-${demo}-${Date.now()}`);
3723
4016
  console.log(`
3724
4017
  DEMO MODE: ${scenario.label}
3725
4018
  `);