hotsheet 0.3.0 → 0.5.0

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