hotsheet 0.2.14 → 0.4.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
  }
@@ -218,6 +225,201 @@ var init_gitignore = __esm({
218
225
  }
219
226
  });
220
227
 
228
+ // src/db/stats.ts
229
+ var stats_exports = {};
230
+ __export(stats_exports, {
231
+ backfillSnapshots: () => backfillSnapshots,
232
+ getDashboardStats: () => getDashboardStats,
233
+ getSnapshots: () => getSnapshots,
234
+ recordDailySnapshot: () => recordDailySnapshot
235
+ });
236
+ async function recordDailySnapshot() {
237
+ const db2 = await getDb();
238
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
239
+ const existing = await db2.query(`SELECT date FROM stats_snapshots WHERE date = $1`, [today]);
240
+ if (existing.rows.length > 0) return;
241
+ const result = await db2.query(
242
+ `SELECT status, COUNT(*) as count FROM tickets WHERE status != 'deleted' GROUP BY status`
243
+ );
244
+ const data = { not_started: 0, started: 0, completed: 0, verified: 0, backlog: 0, archive: 0 };
245
+ for (const row of result.rows) {
246
+ if (row.status in data) {
247
+ data[row.status] = parseInt(row.count, 10);
248
+ }
249
+ }
250
+ await db2.query(
251
+ `INSERT INTO stats_snapshots (date, data) VALUES ($1, $2) ON CONFLICT (date) DO UPDATE SET data = $2`,
252
+ [today, JSON.stringify(data)]
253
+ );
254
+ }
255
+ async function backfillSnapshots() {
256
+ const db2 = await getDb();
257
+ const earliest = await db2.query(`SELECT MIN(DATE(created_at)) as min_date FROM tickets`);
258
+ if (!earliest.rows[0]?.min_date) return;
259
+ const startDate = new Date(earliest.rows[0].min_date);
260
+ const today = /* @__PURE__ */ new Date();
261
+ today.setHours(0, 0, 0, 0);
262
+ const existingRows = await db2.query(`SELECT date FROM stats_snapshots`);
263
+ const existingDates = new Set(existingRows.rows.map((r) => r.date));
264
+ const current = new Date(startDate);
265
+ while (current <= today) {
266
+ const dateStr = current.toISOString().slice(0, 10);
267
+ if (!existingDates.has(dateStr)) {
268
+ const dateEnd = dateStr + "T23:59:59.999Z";
269
+ const result = await db2.query(`
270
+ SELECT
271
+ CASE
272
+ WHEN verified_at IS NOT NULL AND verified_at <= $1 THEN 'verified'
273
+ WHEN completed_at IS NOT NULL AND completed_at <= $1 THEN 'completed'
274
+ WHEN deleted_at IS NOT NULL AND deleted_at <= $1 THEN 'deleted'
275
+ WHEN status = 'backlog' THEN 'backlog'
276
+ WHEN status = 'archive' THEN 'archive'
277
+ WHEN status = 'started' THEN 'started'
278
+ ELSE 'not_started'
279
+ END as status,
280
+ COUNT(*) as count
281
+ FROM tickets
282
+ WHERE created_at <= $1
283
+ GROUP BY 1
284
+ `, [dateEnd]);
285
+ const data = { not_started: 0, started: 0, completed: 0, verified: 0, backlog: 0, archive: 0 };
286
+ for (const row of result.rows) {
287
+ if (row.status in data) {
288
+ data[row.status] = parseInt(row.count, 10);
289
+ }
290
+ }
291
+ await db2.query(
292
+ `INSERT INTO stats_snapshots (date, data) VALUES ($1, $2) ON CONFLICT (date) DO NOTHING`,
293
+ [dateStr, JSON.stringify(data)]
294
+ );
295
+ }
296
+ current.setDate(current.getDate() + 1);
297
+ }
298
+ }
299
+ async function getSnapshots(days) {
300
+ const db2 = await getDb();
301
+ const since = /* @__PURE__ */ new Date();
302
+ since.setDate(since.getDate() - days);
303
+ const sinceStr = since.toISOString().slice(0, 10);
304
+ const result = await db2.query(
305
+ `SELECT date, data FROM stats_snapshots WHERE date >= $1 ORDER BY date ASC`,
306
+ [sinceStr]
307
+ );
308
+ return result.rows.map((r) => ({
309
+ date: r.date,
310
+ data: JSON.parse(r.data)
311
+ }));
312
+ }
313
+ async function getDashboardStats(days) {
314
+ const db2 = await getDb();
315
+ const since = /* @__PURE__ */ new Date();
316
+ since.setDate(since.getDate() - days);
317
+ const sinceStr = since.toISOString();
318
+ const completedByDay = await db2.query(
319
+ `SELECT DATE(completed_at) as date, COUNT(*) as count FROM tickets
320
+ WHERE completed_at >= $1 AND completed_at IS NOT NULL
321
+ GROUP BY DATE(completed_at) ORDER BY date ASC`,
322
+ [sinceStr]
323
+ );
324
+ const createdByDay = await db2.query(
325
+ `SELECT DATE(created_at) as date, COUNT(*) as count FROM tickets
326
+ WHERE created_at >= $1
327
+ GROUP BY DATE(created_at) ORDER BY date ASC`,
328
+ [sinceStr]
329
+ );
330
+ const dateMap = /* @__PURE__ */ new Map();
331
+ const current = new Date(since);
332
+ const today = /* @__PURE__ */ new Date();
333
+ while (current <= today) {
334
+ const d = current.toISOString().slice(0, 10);
335
+ dateMap.set(d, { completed: 0, created: 0 });
336
+ current.setDate(current.getDate() + 1);
337
+ }
338
+ for (const r of completedByDay.rows) {
339
+ const d = typeof r.date === "string" ? r.date.slice(0, 10) : new Date(r.date).toISOString().slice(0, 10);
340
+ const entry = dateMap.get(d);
341
+ if (entry) entry.completed = parseInt(r.count, 10);
342
+ }
343
+ for (const r of createdByDay.rows) {
344
+ const d = typeof r.date === "string" ? r.date.slice(0, 10) : new Date(r.date).toISOString().slice(0, 10);
345
+ const entry = dateMap.get(d);
346
+ if (entry) entry.created = parseInt(r.count, 10);
347
+ }
348
+ const throughput = Array.from(dateMap.entries()).map(([date, counts]) => ({ date, ...counts }));
349
+ const cycleTimeResult = await db2.query(
350
+ `SELECT ticket_number, title, completed_at, created_at FROM tickets
351
+ WHERE completed_at >= $1 AND completed_at IS NOT NULL AND status IN ('completed', 'verified')
352
+ ORDER BY completed_at ASC`,
353
+ [sinceStr]
354
+ );
355
+ const cycleTime = cycleTimeResult.rows.map((r) => ({
356
+ ticket_number: r.ticket_number,
357
+ title: r.title,
358
+ completed_at: r.completed_at,
359
+ days: Math.max(0, Math.round((new Date(r.completed_at).getTime() - new Date(r.created_at).getTime()) / 864e5))
360
+ }));
361
+ const catResult = await db2.query(
362
+ `SELECT category, COUNT(*) as count FROM tickets
363
+ WHERE status IN ('not_started', 'started')
364
+ GROUP BY category ORDER BY count DESC`
365
+ );
366
+ const categoryBreakdown = catResult.rows.map((r) => ({ category: r.category, count: parseInt(r.count, 10) }));
367
+ const catPeriodResult = await db2.query(
368
+ `SELECT category, COUNT(*) as count FROM tickets
369
+ WHERE status != 'deleted' AND (
370
+ created_at >= $1 OR
371
+ (completed_at IS NOT NULL AND completed_at >= $1) OR
372
+ (verified_at IS NOT NULL AND verified_at >= $1) OR
373
+ updated_at >= $1
374
+ )
375
+ GROUP BY category ORDER BY count DESC`,
376
+ [sinceStr]
377
+ );
378
+ const categoryPeriod = catPeriodResult.rows.map((r) => ({ category: r.category, count: parseInt(r.count, 10) }));
379
+ const now = /* @__PURE__ */ new Date();
380
+ const weekStart = new Date(now);
381
+ weekStart.setDate(weekStart.getDate() - weekStart.getDay());
382
+ weekStart.setHours(0, 0, 0, 0);
383
+ const lastWeekStart = new Date(weekStart);
384
+ lastWeekStart.setDate(lastWeekStart.getDate() - 7);
385
+ const completedThisWeekR = await db2.query(
386
+ `SELECT COUNT(*) as count FROM tickets WHERE completed_at >= $1`,
387
+ [weekStart.toISOString()]
388
+ );
389
+ const completedLastWeekR = await db2.query(
390
+ `SELECT COUNT(*) as count FROM tickets WHERE completed_at >= $1 AND completed_at < $2`,
391
+ [lastWeekStart.toISOString(), weekStart.toISOString()]
392
+ );
393
+ const wipR = await db2.query(
394
+ `SELECT COUNT(*) as count FROM tickets WHERE status = 'started'`
395
+ );
396
+ const createdThisWeekR = await db2.query(
397
+ `SELECT COUNT(*) as count FROM tickets WHERE created_at >= $1`,
398
+ [weekStart.toISOString()]
399
+ );
400
+ const cycleDays = cycleTime.map((c) => c.days).sort((a, b) => a - b);
401
+ const medianCycleTimeDays = cycleDays.length > 0 ? cycleDays[Math.floor(cycleDays.length / 2)] : null;
402
+ return {
403
+ throughput,
404
+ cycleTime,
405
+ categoryBreakdown,
406
+ categoryPeriod,
407
+ kpi: {
408
+ completedThisWeek: parseInt(completedThisWeekR.rows[0].count, 10),
409
+ completedLastWeek: parseInt(completedLastWeekR.rows[0].count, 10),
410
+ wipCount: parseInt(wipR.rows[0].count, 10),
411
+ createdThisWeek: parseInt(createdThisWeekR.rows[0].count, 10),
412
+ medianCycleTimeDays
413
+ }
414
+ };
415
+ }
416
+ var init_stats = __esm({
417
+ "src/db/stats.ts"() {
418
+ "use strict";
419
+ init_connection();
420
+ }
421
+ });
422
+
221
423
  // src/cli.ts
222
424
  import { mkdirSync as mkdirSync6 } from "fs";
223
425
  import { tmpdir } from "os";
@@ -420,16 +622,117 @@ function initBackupScheduler(dataDir2) {
420
622
  // src/cleanup.ts
421
623
  import { rmSync as rmSync3 } from "fs";
422
624
 
625
+ // src/types.ts
626
+ var DEFAULT_CATEGORIES = [
627
+ { id: "issue", label: "Issue", shortLabel: "ISS", color: "#6b7280", shortcutKey: "i", description: "General issues that need attention" },
628
+ { id: "bug", label: "Bug", shortLabel: "BUG", color: "#ef4444", shortcutKey: "b", description: "Bugs that should be fixed in the codebase" },
629
+ { id: "feature", label: "Feature", shortLabel: "FEA", color: "#22c55e", shortcutKey: "f", description: "New features to be implemented" },
630
+ { id: "requirement_change", label: "Req Change", shortLabel: "REQ", color: "#f97316", shortcutKey: "r", description: "Changes to existing requirements" },
631
+ { id: "task", label: "Task", shortLabel: "TSK", color: "#3b82f6", shortcutKey: "k", description: "General tasks to complete" },
632
+ { id: "investigation", label: "Investigation", shortLabel: "INV", color: "#8b5cf6", shortcutKey: "g", description: "Items requiring research or analysis" }
633
+ ];
634
+ var CATEGORY_PRESETS = [
635
+ {
636
+ id: "software",
637
+ name: "Software Development",
638
+ categories: DEFAULT_CATEGORIES
639
+ },
640
+ {
641
+ id: "design",
642
+ name: "Design / Creative",
643
+ categories: [
644
+ { id: "concept", label: "Concept", shortLabel: "CON", color: "#8b5cf6", shortcutKey: "c", description: "Design concepts and explorations" },
645
+ { id: "revision", label: "Revision", shortLabel: "REV", color: "#f97316", shortcutKey: "r", description: "Revisions to existing designs" },
646
+ { id: "feedback", label: "Feedback", shortLabel: "FDB", color: "#3b82f6", shortcutKey: "f", description: "Client or stakeholder feedback to address" },
647
+ { id: "asset", label: "Asset", shortLabel: "AST", color: "#22c55e", shortcutKey: "a", description: "Assets to produce or deliver" },
648
+ { id: "research", label: "Research", shortLabel: "RSC", color: "#6b7280", shortcutKey: "s", description: "User research or competitive analysis" },
649
+ { id: "bug", label: "Bug", shortLabel: "BUG", color: "#ef4444", shortcutKey: "b", description: "Visual or UI bugs" }
650
+ ]
651
+ },
652
+ {
653
+ id: "product",
654
+ name: "Product Management",
655
+ categories: [
656
+ { id: "epic", label: "Epic", shortLabel: "EPC", color: "#8b5cf6", shortcutKey: "e", description: "Large initiatives spanning multiple stories" },
657
+ { id: "story", label: "Story", shortLabel: "STY", color: "#3b82f6", shortcutKey: "s", description: "User stories describing desired functionality" },
658
+ { id: "bug", label: "Bug", shortLabel: "BUG", color: "#ef4444", shortcutKey: "b", description: "Bugs that need to be fixed" },
659
+ { id: "task", label: "Task", shortLabel: "TSK", color: "#22c55e", shortcutKey: "t", description: "Tasks to complete" },
660
+ { id: "spike", label: "Spike", shortLabel: "SPK", color: "#f97316", shortcutKey: "k", description: "Research or investigation spikes" },
661
+ { id: "debt", label: "Tech Debt", shortLabel: "DBT", color: "#6b7280", shortcutKey: "d", description: "Technical debt to address" }
662
+ ]
663
+ },
664
+ {
665
+ id: "marketing",
666
+ name: "Marketing",
667
+ categories: [
668
+ { id: "campaign", label: "Campaign", shortLabel: "CMP", color: "#8b5cf6", shortcutKey: "c", description: "Marketing campaigns" },
669
+ { id: "content", label: "Content", shortLabel: "CNT", color: "#3b82f6", shortcutKey: "n", description: "Content to create or publish" },
670
+ { id: "design", label: "Design", shortLabel: "DES", color: "#22c55e", shortcutKey: "d", description: "Design requests and assets" },
671
+ { id: "analytics", label: "Analytics", shortLabel: "ANL", color: "#f97316", shortcutKey: "a", description: "Analytics and reporting tasks" },
672
+ { id: "outreach", label: "Outreach", shortLabel: "OUT", color: "#6b7280", shortcutKey: "o", description: "Outreach and partnership activities" },
673
+ { id: "event", label: "Event", shortLabel: "EVT", color: "#ef4444", shortcutKey: "e", description: "Events to plan or manage" }
674
+ ]
675
+ },
676
+ {
677
+ id: "personal",
678
+ name: "Personal",
679
+ categories: [
680
+ { id: "task", label: "Task", shortLabel: "TSK", color: "#3b82f6", shortcutKey: "t", description: "Things to do" },
681
+ { id: "idea", label: "Idea", shortLabel: "IDA", color: "#22c55e", shortcutKey: "i", description: "Ideas to explore" },
682
+ { id: "note", label: "Note", shortLabel: "NTE", color: "#6b7280", shortcutKey: "n", description: "Notes and references" },
683
+ { id: "errand", label: "Errand", shortLabel: "ERR", color: "#f97316", shortcutKey: "e", description: "Errands and appointments" },
684
+ { id: "project", label: "Project", shortLabel: "PRJ", color: "#8b5cf6", shortcutKey: "p", description: "Larger projects" },
685
+ { id: "urgent", label: "Urgent", shortLabel: "URG", color: "#ef4444", shortcutKey: "u", description: "Urgent items" }
686
+ ]
687
+ }
688
+ ];
689
+ var CATEGORIES = DEFAULT_CATEGORIES.map((c) => ({ value: c.id, label: c.label, color: c.color }));
690
+ var CATEGORY_DESCRIPTIONS = Object.fromEntries(
691
+ DEFAULT_CATEGORIES.map((c) => [c.id, c.description])
692
+ );
693
+
423
694
  // src/db/queries.ts
424
695
  init_connection();
425
- function parseNotes(raw2) {
426
- if (!raw2 || raw2 === "") return [];
696
+ var noteCounter = 0;
697
+ function generateNoteId() {
698
+ return `n_${Date.now().toString(36)}_${(noteCounter++).toString(36)}`;
699
+ }
700
+ function parseNotes(raw) {
701
+ if (!raw || raw === "") return [];
427
702
  try {
428
- const parsed = JSON.parse(raw2);
429
- if (Array.isArray(parsed)) return parsed;
703
+ const parsed = JSON.parse(raw);
704
+ if (Array.isArray(parsed)) {
705
+ return parsed.map((n) => ({
706
+ id: n.id || generateNoteId(),
707
+ text: n.text,
708
+ created_at: n.created_at
709
+ }));
710
+ }
430
711
  } catch {
431
712
  }
432
- return [{ text: raw2, created_at: (/* @__PURE__ */ new Date()).toISOString() }];
713
+ return [{ id: generateNoteId(), text: raw, created_at: (/* @__PURE__ */ new Date()).toISOString() }];
714
+ }
715
+ async function editNote(ticketId, noteId, text) {
716
+ const db2 = await getDb();
717
+ const result = await db2.query(`SELECT notes FROM tickets WHERE id = $1`, [ticketId]);
718
+ if (result.rows.length === 0) return null;
719
+ const notes = parseNotes(result.rows[0].notes);
720
+ const note = notes.find((n) => n.id === noteId);
721
+ if (!note) return null;
722
+ note.text = text;
723
+ await db2.query(`UPDATE tickets SET notes = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(notes), ticketId]);
724
+ return notes;
725
+ }
726
+ async function deleteNote(ticketId, noteId) {
727
+ const db2 = await getDb();
728
+ const result = await db2.query(`SELECT notes FROM tickets WHERE id = $1`, [ticketId]);
729
+ if (result.rows.length === 0) return null;
730
+ const notes = parseNotes(result.rows[0].notes);
731
+ const idx = notes.findIndex((n) => n.id === noteId);
732
+ if (idx === -1) return null;
733
+ notes.splice(idx, 1);
734
+ await db2.query(`UPDATE tickets SET notes = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(notes), ticketId]);
735
+ return notes;
433
736
  }
434
737
  async function nextTicketNumber() {
435
738
  const db2 = await getDb();
@@ -488,7 +791,7 @@ async function updateTicket(id, updates) {
488
791
  if (updates.notes !== void 0 && updates.notes !== "") {
489
792
  const current = await db2.query(`SELECT notes FROM tickets WHERE id = $1`, [id]);
490
793
  const existing = parseNotes(current.rows[0]?.notes || "");
491
- existing.push({ text: updates.notes, created_at: (/* @__PURE__ */ new Date()).toISOString() });
794
+ existing.push({ id: generateNoteId(), text: updates.notes, created_at: (/* @__PURE__ */ new Date()).toISOString() });
492
795
  sets.push(`notes = $${paramIdx}`);
493
796
  values.push(JSON.stringify(existing));
494
797
  paramIdx++;
@@ -505,8 +808,6 @@ async function updateTicket(id, updates) {
505
808
  sets.push("deleted_at = NOW()");
506
809
  } else if (updates.status === "backlog" || updates.status === "archive") {
507
810
  sets.push("up_next = FALSE");
508
- sets.push("completed_at = NULL");
509
- sets.push("verified_at = NULL");
510
811
  sets.push("deleted_at = NULL");
511
812
  } else if (updates.status === "not_started" || updates.status === "started") {
512
813
  sets.push("completed_at = NULL");
@@ -578,8 +879,8 @@ async function getTickets(filters = {}) {
578
879
  break;
579
880
  case "status":
580
881
  orderBy = `CASE status
581
- WHEN 'started' THEN 1 WHEN 'not_started' THEN 2 WHEN 'completed' THEN 3
582
- WHEN 'verified' THEN 4 WHEN 'backlog' THEN 5 WHEN 'archive' THEN 6 END`;
882
+ WHEN 'backlog' THEN 1 WHEN 'not_started' THEN 2 WHEN 'started' THEN 3
883
+ WHEN 'completed' THEN 4 WHEN 'verified' THEN 5 WHEN 'archive' THEN 6 END`;
583
884
  break;
584
885
  case "ticket_number":
585
886
  orderBy = "id";
@@ -681,6 +982,127 @@ async function getTicketsForCleanup(verifiedDays = 30, trashDays = 3) {
681
982
  `, [verifiedDays, trashDays]);
682
983
  return result.rows;
683
984
  }
985
+ var QUERYABLE_FIELDS = /* @__PURE__ */ new Set(["category", "priority", "status", "title", "details", "up_next", "tags"]);
986
+ 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`;
987
+ 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`;
988
+ var PRIORITY_RANK = { highest: 1, high: 2, default: 3, low: 4, lowest: 5 };
989
+ var STATUS_RANK = { backlog: 1, not_started: 2, started: 3, completed: 4, verified: 5, archive: 6 };
990
+ function ordinalExpr(field) {
991
+ if (field === "priority") return PRIORITY_ORD;
992
+ if (field === "status") return STATUS_ORD;
993
+ return null;
994
+ }
995
+ function ordinalValue(field, value) {
996
+ if (field === "priority") return PRIORITY_RANK[value] ?? null;
997
+ if (field === "status") return STATUS_RANK[value] ?? null;
998
+ return null;
999
+ }
1000
+ async function queryTickets(logic, conditions, sortBy, sortDir) {
1001
+ const db2 = await getDb();
1002
+ const where = [];
1003
+ const values = [];
1004
+ let paramIdx = 1;
1005
+ where.push(`status != 'deleted'`);
1006
+ for (const cond of conditions) {
1007
+ if (!QUERYABLE_FIELDS.has(cond.field)) continue;
1008
+ const field = cond.field;
1009
+ if (field === "up_next") {
1010
+ where.push(`up_next = $${paramIdx}`);
1011
+ values.push(cond.value === "true");
1012
+ paramIdx++;
1013
+ continue;
1014
+ }
1015
+ const ordExpr = ordinalExpr(field);
1016
+ const ordVal = ordExpr ? ordinalValue(field, cond.value) : null;
1017
+ if (ordExpr && ordVal !== null && ["lt", "lte", "gt", "gte"].includes(cond.operator)) {
1018
+ const op = cond.operator === "lt" ? "<" : cond.operator === "lte" ? "<=" : cond.operator === "gt" ? ">" : ">=";
1019
+ where.push(`(${ordExpr}) ${op} $${paramIdx}`);
1020
+ values.push(ordVal);
1021
+ paramIdx++;
1022
+ continue;
1023
+ }
1024
+ switch (cond.operator) {
1025
+ case "equals":
1026
+ where.push(`${field} = $${paramIdx}`);
1027
+ values.push(cond.value);
1028
+ paramIdx++;
1029
+ break;
1030
+ case "not_equals":
1031
+ where.push(`${field} != $${paramIdx}`);
1032
+ values.push(cond.value);
1033
+ paramIdx++;
1034
+ break;
1035
+ case "contains":
1036
+ where.push(`${field} ILIKE $${paramIdx}`);
1037
+ values.push(`%${cond.value}%`);
1038
+ paramIdx++;
1039
+ break;
1040
+ case "not_contains":
1041
+ where.push(`${field} NOT ILIKE $${paramIdx}`);
1042
+ values.push(`%${cond.value}%`);
1043
+ paramIdx++;
1044
+ break;
1045
+ }
1046
+ }
1047
+ const joiner = logic === "any" ? " OR " : " AND ";
1048
+ const userConditions = where.slice(1);
1049
+ let whereClause = where[0];
1050
+ if (userConditions.length > 0) {
1051
+ whereClause += ` AND (${userConditions.join(joiner)})`;
1052
+ }
1053
+ let orderBy;
1054
+ switch (sortBy) {
1055
+ case "priority":
1056
+ orderBy = `CASE priority WHEN 'highest' THEN 1 WHEN 'high' THEN 2 WHEN 'default' THEN 3 WHEN 'low' THEN 4 WHEN 'lowest' THEN 5 END`;
1057
+ break;
1058
+ case "category":
1059
+ orderBy = "category";
1060
+ break;
1061
+ case "status":
1062
+ 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`;
1063
+ break;
1064
+ default:
1065
+ orderBy = "created_at";
1066
+ break;
1067
+ }
1068
+ const dir = sortDir === "asc" ? "ASC" : "DESC";
1069
+ const result = await db2.query(
1070
+ `SELECT * FROM tickets WHERE ${whereClause} ORDER BY ${orderBy} ${dir}, id DESC`,
1071
+ values
1072
+ );
1073
+ return result.rows;
1074
+ }
1075
+ async function getAllTags() {
1076
+ const db2 = await getDb();
1077
+ const result = await db2.query(`SELECT DISTINCT tags FROM tickets WHERE tags != '[]' AND status != 'deleted'`);
1078
+ const tagSet = /* @__PURE__ */ new Set();
1079
+ for (const row of result.rows) {
1080
+ try {
1081
+ const parsed = JSON.parse(row.tags);
1082
+ if (Array.isArray(parsed)) {
1083
+ for (const tag of parsed) {
1084
+ if (typeof tag === "string" && tag.trim()) tagSet.add(tag.trim());
1085
+ }
1086
+ }
1087
+ } catch {
1088
+ }
1089
+ }
1090
+ return Array.from(tagSet).sort();
1091
+ }
1092
+ async function getCategories() {
1093
+ const settings = await getSettings();
1094
+ if (settings.categories) {
1095
+ try {
1096
+ const parsed = JSON.parse(settings.categories);
1097
+ if (Array.isArray(parsed) && parsed.length > 0) return parsed;
1098
+ } catch {
1099
+ }
1100
+ }
1101
+ return DEFAULT_CATEGORIES;
1102
+ }
1103
+ async function saveCategories(categories) {
1104
+ await updateSetting("categories", JSON.stringify(categories));
1105
+ }
684
1106
  async function getSettings() {
685
1107
  const db2 = await getDb();
686
1108
  const result = await db2.query("SELECT key, value FROM settings");
@@ -1611,33 +2033,25 @@ import { basename, extname, join as join8, relative as relative2 } from "path";
1611
2033
  // src/skills.ts
1612
2034
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
1613
2035
  import { join as join6, relative } from "path";
1614
-
1615
- // src/types.ts
1616
- var CATEGORY_DESCRIPTIONS = {
1617
- issue: "General issues that need attention",
1618
- bug: "Bugs that should be fixed in the codebase",
1619
- feature: "New features to be implemented",
1620
- requirement_change: "Changes to existing requirements",
1621
- task: "General tasks to complete",
1622
- investigation: "Items requiring research or analysis"
1623
- };
1624
-
1625
- // src/skills.ts
1626
- var SKILL_VERSION = 2;
2036
+ var SKILL_VERSION = 3;
1627
2037
  var skillPort;
1628
2038
  var skillDataDir;
2039
+ var skillCategories = DEFAULT_CATEGORIES;
1629
2040
  function initSkills(port2, dataDir2) {
1630
2041
  skillPort = port2;
1631
2042
  skillDataDir = dataDir2;
1632
2043
  }
1633
- var TICKET_SKILLS = [
1634
- { name: "hs-bug", category: "bug", label: "bug" },
1635
- { name: "hs-feature", category: "feature", label: "feature" },
1636
- { name: "hs-task", category: "task", label: "task" },
1637
- { name: "hs-issue", category: "issue", label: "issue" },
1638
- { name: "hs-investigation", category: "investigation", label: "investigation" },
1639
- { name: "hs-req-change", category: "requirement_change", label: "requirement change" }
1640
- ];
2044
+ function setSkillCategories(categories) {
2045
+ skillCategories = categories;
2046
+ }
2047
+ function buildTicketSkills() {
2048
+ return skillCategories.map((cat) => ({
2049
+ name: `hs-${cat.id.replace(/_/g, "-")}`,
2050
+ category: cat.id,
2051
+ label: cat.label.toLowerCase(),
2052
+ description: cat.description
2053
+ }));
2054
+ }
1641
2055
  function versionHeader() {
1642
2056
  return `<!-- hotsheet-skill-version: ${SKILL_VERSION} -->`;
1643
2057
  }
@@ -1658,9 +2072,8 @@ function updateFile(path, content) {
1658
2072
  return true;
1659
2073
  }
1660
2074
  function ticketSkillBody(skill) {
1661
- const desc = CATEGORY_DESCRIPTIONS[skill.category];
1662
2075
  return [
1663
- `Create a new Hot Sheet **${skill.label}** ticket. ${desc}.`,
2076
+ `Create a new Hot Sheet **${skill.label}** ticket. ${skill.description}.`,
1664
2077
  "",
1665
2078
  "**Parsing the input:**",
1666
2079
  '- If the input starts with "next", "up next", or "do next" (case-insensitive), set `up_next` to `true` and use the remaining text as the title',
@@ -1731,7 +2144,7 @@ function ensureClaudeSkills(cwd) {
1731
2144
  ""
1732
2145
  ].join("\n");
1733
2146
  if (updateFile(join6(mainDir, "SKILL.md"), mainContent)) updated = true;
1734
- for (const skill of TICKET_SKILLS) {
2147
+ for (const skill of buildTicketSkills()) {
1735
2148
  const dir = join6(skillsDir, skill.name);
1736
2149
  mkdirSync3(dir, { recursive: true });
1737
2150
  const content = [
@@ -1764,7 +2177,7 @@ function ensureCursorRules(cwd) {
1764
2177
  ""
1765
2178
  ].join("\n");
1766
2179
  if (updateFile(join6(rulesDir, "hotsheet.mdc"), mainContent)) updated = true;
1767
- for (const skill of TICKET_SKILLS) {
2180
+ for (const skill of buildTicketSkills()) {
1768
2181
  const content = [
1769
2182
  "---",
1770
2183
  `description: Create a new ${skill.label} ticket in Hot Sheet`,
@@ -1793,7 +2206,7 @@ function ensureCopilotPrompts(cwd) {
1793
2206
  ""
1794
2207
  ].join("\n");
1795
2208
  if (updateFile(join6(promptsDir, "hotsheet.prompt.md"), mainContent)) updated = true;
1796
- for (const skill of TICKET_SKILLS) {
2209
+ for (const skill of buildTicketSkills()) {
1797
2210
  const content = [
1798
2211
  "---",
1799
2212
  `description: Create a new ${skill.label} ticket in Hot Sheet`,
@@ -1822,7 +2235,7 @@ function ensureWindsurfRules(cwd) {
1822
2235
  ""
1823
2236
  ].join("\n");
1824
2237
  if (updateFile(join6(rulesDir, "hotsheet.md"), mainContent)) updated = true;
1825
- for (const skill of TICKET_SKILLS) {
2238
+ for (const skill of buildTicketSkills()) {
1826
2239
  const content = [
1827
2240
  "---",
1828
2241
  "trigger: manual",
@@ -1893,14 +2306,14 @@ function scheduleAllSync() {
1893
2306
  scheduleWorklistSync();
1894
2307
  scheduleOpenTicketsSync();
1895
2308
  }
1896
- function parseTicketNotes(raw2) {
1897
- if (!raw2 || raw2 === "") return [];
2309
+ function parseTicketNotes(raw) {
2310
+ if (!raw || raw === "") return [];
1898
2311
  try {
1899
- const parsed = JSON.parse(raw2);
2312
+ const parsed = JSON.parse(raw);
1900
2313
  if (Array.isArray(parsed)) return parsed;
1901
2314
  } catch {
1902
2315
  }
1903
- if (raw2.trim()) return [{ text: raw2, created_at: "" }];
2316
+ if (raw.trim()) return [{ text: raw, created_at: "" }];
1904
2317
  return [];
1905
2318
  }
1906
2319
  async function formatTicket(ticket) {
@@ -1912,6 +2325,13 @@ async function formatTicket(ticket) {
1912
2325
  lines.push(`- Priority: ${ticket.priority}`);
1913
2326
  lines.push(`- Status: ${ticket.status.replace("_", " ")}`);
1914
2327
  lines.push(`- Title: ${ticket.title}`);
2328
+ try {
2329
+ const tags = JSON.parse(ticket.tags);
2330
+ if (Array.isArray(tags) && tags.length > 0) {
2331
+ lines.push(`- Tags: ${tags.join(", ")}`);
2332
+ }
2333
+ } catch {
2334
+ }
1915
2335
  if (ticket.details.trim()) {
1916
2336
  const detailLines = ticket.details.split("\n");
1917
2337
  lines.push(`- Details: ${detailLines[0]}`);
@@ -1935,10 +2355,12 @@ async function formatTicket(ticket) {
1935
2355
  }
1936
2356
  return lines.join("\n");
1937
2357
  }
1938
- function formatCategoryDescriptions(categories) {
2358
+ async function formatCategoryDescriptions(usedCategories) {
2359
+ const allCategories = await getCategories();
2360
+ const descMap = Object.fromEntries(allCategories.map((c) => [c.id, c.description]));
1939
2361
  const lines = ["Ticket Types:"];
1940
- for (const cat of categories) {
1941
- lines.push(`- ${cat} - ${CATEGORY_DESCRIPTIONS[cat]}`);
2362
+ for (const cat of usedCategories) {
2363
+ lines.push(`- ${cat} - ${descMap[cat] || cat}`);
1942
2364
  }
1943
2365
  return lines.join("\n");
1944
2366
  }
@@ -1976,7 +2398,9 @@ async function syncWorklist() {
1976
2398
  sections.push("- Create follow-up tasks for items outside the current scope");
1977
2399
  sections.push("");
1978
2400
  sections.push("To create a ticket:");
1979
- sections.push(` \`curl -s -X POST http://localhost:${port}/api/tickets -H "Content-Type: application/json" -d '{"title": "Title", "defaults": {"category": "bug|feature|task|issue|investigation|requirement_change", "up_next": false}}'\``);
2401
+ const allCats = await getCategories();
2402
+ const catIds = allCats.map((c) => c.id).join("|");
2403
+ sections.push(` \`curl -s -X POST http://localhost:${port}/api/tickets -H "Content-Type: application/json" -d '{"title": "Title", "defaults": {"category": "${catIds}", "up_next": false}}'\``);
1980
2404
  sections.push("");
1981
2405
  sections.push('You can also include `"details"` in the defaults object for longer descriptions.');
1982
2406
  sections.push("Set `up_next: true` only for items that should be prioritized immediately.");
@@ -1994,7 +2418,7 @@ async function syncWorklist() {
1994
2418
  }
1995
2419
  sections.push("---");
1996
2420
  sections.push("");
1997
- sections.push(formatCategoryDescriptions(categories));
2421
+ sections.push(await formatCategoryDescriptions(categories));
1998
2422
  }
1999
2423
  sections.push("");
2000
2424
  writeFileSync5(join7(dataDir, "worklist.md"), sections.join("\n"), "utf-8");
@@ -2038,7 +2462,7 @@ async function syncOpenTickets() {
2038
2462
  } else {
2039
2463
  sections.push("---");
2040
2464
  sections.push("");
2041
- sections.push(formatCategoryDescriptions(categories));
2465
+ sections.push(await formatCategoryDescriptions(categories));
2042
2466
  }
2043
2467
  sections.push("");
2044
2468
  writeFileSync5(join7(dataDir, "open-tickets.md"), sections.join("\n"), "utf-8");
@@ -2105,7 +2529,8 @@ apiRoutes.get("/tickets/:id", async (c) => {
2105
2529
  const ticket = await getTicket(id);
2106
2530
  if (!ticket) return c.json({ error: "Not found" }, 404);
2107
2531
  const attachments = await getAttachments(id);
2108
- return c.json({ ...ticket, attachments });
2532
+ const notes = parseNotes(ticket.notes);
2533
+ return c.json({ ...ticket, notes: JSON.stringify(notes), attachments });
2109
2534
  });
2110
2535
  apiRoutes.patch("/tickets/:id", async (c) => {
2111
2536
  const id = parseInt(c.req.param("id"), 10);
@@ -2123,6 +2548,25 @@ apiRoutes.delete("/tickets/:id", async (c) => {
2123
2548
  notifyChange();
2124
2549
  return c.json({ ok: true });
2125
2550
  });
2551
+ apiRoutes.patch("/tickets/:id/notes/:noteId", async (c) => {
2552
+ const id = parseInt(c.req.param("id"), 10);
2553
+ const noteId = c.req.param("noteId");
2554
+ const body = await c.req.json();
2555
+ const notes = await editNote(id, noteId, body.text);
2556
+ if (!notes) return c.json({ error: "Not found" }, 404);
2557
+ scheduleAllSync();
2558
+ notifyChange();
2559
+ return c.json(notes);
2560
+ });
2561
+ apiRoutes.delete("/tickets/:id/notes/:noteId", async (c) => {
2562
+ const id = parseInt(c.req.param("id"), 10);
2563
+ const noteId = c.req.param("noteId");
2564
+ const notes = await deleteNote(id, noteId);
2565
+ if (!notes) return c.json({ error: "Not found" }, 404);
2566
+ scheduleAllSync();
2567
+ notifyChange();
2568
+ return c.json(notes);
2569
+ });
2126
2570
  apiRoutes.delete("/tickets/:id/hard", async (c) => {
2127
2571
  const id = parseInt(c.req.param("id"), 10);
2128
2572
  const attachments = await getAttachments(id);
@@ -2282,10 +2726,42 @@ apiRoutes.get("/attachments/file/*", async (c) => {
2282
2726
  headers: { "Content-Type": contentType }
2283
2727
  });
2284
2728
  });
2729
+ apiRoutes.post("/tickets/query", async (c) => {
2730
+ const body = await c.req.json();
2731
+ const tickets = await queryTickets(body.logic, body.conditions, body.sort_by, body.sort_dir);
2732
+ return c.json(tickets);
2733
+ });
2734
+ apiRoutes.get("/tags", async (c) => {
2735
+ const tags = await getAllTags();
2736
+ return c.json(tags);
2737
+ });
2738
+ apiRoutes.get("/categories", async (c) => {
2739
+ const categories = await getCategories();
2740
+ return c.json(categories);
2741
+ });
2742
+ apiRoutes.put("/categories", async (c) => {
2743
+ const categories = await c.req.json();
2744
+ await saveCategories(categories);
2745
+ scheduleAllSync();
2746
+ notifyChange();
2747
+ return c.json(categories);
2748
+ });
2749
+ apiRoutes.get("/category-presets", (c) => {
2750
+ return c.json(CATEGORY_PRESETS);
2751
+ });
2285
2752
  apiRoutes.get("/stats", async (c) => {
2286
2753
  const stats = await getTicketStats();
2287
2754
  return c.json(stats);
2288
2755
  });
2756
+ apiRoutes.get("/dashboard", async (c) => {
2757
+ const { getDashboardStats: getDashboardStats2, getSnapshots: getSnapshots2 } = await Promise.resolve().then(() => (init_stats(), stats_exports));
2758
+ const days = parseInt(c.req.query("days") || "30", 10);
2759
+ const [stats, snapshots] = await Promise.all([
2760
+ getDashboardStats2(days),
2761
+ getSnapshots2(days)
2762
+ ]);
2763
+ return c.json({ ...stats, snapshots });
2764
+ });
2289
2765
  apiRoutes.get("/settings", async (c) => {
2290
2766
  const settings = await getSettings();
2291
2767
  return c.json(settings);
@@ -2352,6 +2828,24 @@ apiRoutes.post("/gitignore/add", async (c) => {
2352
2828
  ensureGitignore2(process.cwd());
2353
2829
  return c.json({ ok: true });
2354
2830
  });
2831
+ apiRoutes.post("/print", async (c) => {
2832
+ const { html } = await c.req.json();
2833
+ const { writeFileSync: writeFileSync7 } = await import("fs");
2834
+ const { tmpdir: tmpdir2 } = await import("os");
2835
+ const { join: pathJoin } = await import("path");
2836
+ const { execFile } = await import("child_process");
2837
+ const tmpPath = pathJoin(tmpdir2(), `hotsheet-print-${Date.now()}.html`);
2838
+ writeFileSync7(tmpPath, html, "utf-8");
2839
+ const platform = process.platform;
2840
+ if (platform === "darwin") {
2841
+ execFile("open", [tmpPath]);
2842
+ } else if (platform === "win32") {
2843
+ execFile("start", ["", tmpPath], { shell: true });
2844
+ } else {
2845
+ execFile("xdg-open", [tmpPath]);
2846
+ }
2847
+ return c.json({ ok: true, path: tmpPath });
2848
+ });
2355
2849
 
2356
2850
  // src/routes/backups.ts
2357
2851
  import { Hono as Hono2 } from "hono";
@@ -2424,24 +2918,7 @@ var SafeHtml = class {
2424
2918
  return this.__html;
2425
2919
  }
2426
2920
  };
2427
- function raw(html) {
2428
- return new SafeHtml(html);
2429
- }
2430
- var VOID_TAGS = /* @__PURE__ */ new Set([
2431
- "area",
2432
- "base",
2433
- "br",
2434
- "col",
2435
- "embed",
2436
- "hr",
2437
- "img",
2438
- "input",
2439
- "link",
2440
- "meta",
2441
- "source",
2442
- "track",
2443
- "wbr"
2444
- ]);
2921
+ var VOID_TAGS = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"]);
2445
2922
  function renderChildren(children) {
2446
2923
  if (children == null || typeof children === "boolean") return "";
2447
2924
  if (children instanceof SafeHtml) return children.__html;
@@ -2450,10 +2927,134 @@ function renderChildren(children) {
2450
2927
  if (Array.isArray(children)) return children.map(renderChildren).join("");
2451
2928
  return "";
2452
2929
  }
2930
+ var ATTR_ALIASES = {
2931
+ // HTML attributes
2932
+ className: "class",
2933
+ htmlFor: "for",
2934
+ httpEquiv: "http-equiv",
2935
+ acceptCharset: "accept-charset",
2936
+ accessKey: "accesskey",
2937
+ autoCapitalize: "autocapitalize",
2938
+ autoComplete: "autocomplete",
2939
+ autoFocus: "autofocus",
2940
+ autoPlay: "autoplay",
2941
+ colSpan: "colspan",
2942
+ contentEditable: "contenteditable",
2943
+ crossOrigin: "crossorigin",
2944
+ dateTime: "datetime",
2945
+ defaultChecked: "checked",
2946
+ defaultValue: "value",
2947
+ encType: "enctype",
2948
+ formAction: "formaction",
2949
+ formEncType: "formenctype",
2950
+ formMethod: "formmethod",
2951
+ formNoValidate: "formnovalidate",
2952
+ formTarget: "formtarget",
2953
+ hrefLang: "hreflang",
2954
+ inputMode: "inputmode",
2955
+ maxLength: "maxlength",
2956
+ minLength: "minlength",
2957
+ noModule: "nomodule",
2958
+ noValidate: "novalidate",
2959
+ readOnly: "readonly",
2960
+ referrerPolicy: "referrerpolicy",
2961
+ rowSpan: "rowspan",
2962
+ spellCheck: "spellcheck",
2963
+ srcDoc: "srcdoc",
2964
+ srcLang: "srclang",
2965
+ srcSet: "srcset",
2966
+ tabIndex: "tabindex",
2967
+ useMap: "usemap",
2968
+ // SVG presentation attributes (camelCase → kebab-case)
2969
+ strokeWidth: "stroke-width",
2970
+ strokeLinecap: "stroke-linecap",
2971
+ strokeLinejoin: "stroke-linejoin",
2972
+ strokeDasharray: "stroke-dasharray",
2973
+ strokeDashoffset: "stroke-dashoffset",
2974
+ strokeMiterlimit: "stroke-miterlimit",
2975
+ strokeOpacity: "stroke-opacity",
2976
+ fillOpacity: "fill-opacity",
2977
+ fillRule: "fill-rule",
2978
+ clipPath: "clip-path",
2979
+ clipRule: "clip-rule",
2980
+ colorInterpolation: "color-interpolation",
2981
+ colorInterpolationFilters: "color-interpolation-filters",
2982
+ floodColor: "flood-color",
2983
+ floodOpacity: "flood-opacity",
2984
+ lightingColor: "lighting-color",
2985
+ stopColor: "stop-color",
2986
+ stopOpacity: "stop-opacity",
2987
+ shapeRendering: "shape-rendering",
2988
+ imageRendering: "image-rendering",
2989
+ textRendering: "text-rendering",
2990
+ pointerEvents: "pointer-events",
2991
+ vectorEffect: "vector-effect",
2992
+ paintOrder: "paint-order",
2993
+ // SVG text/font attributes
2994
+ fontFamily: "font-family",
2995
+ fontSize: "font-size",
2996
+ fontStyle: "font-style",
2997
+ fontVariant: "font-variant",
2998
+ fontWeight: "font-weight",
2999
+ fontStretch: "font-stretch",
3000
+ textAnchor: "text-anchor",
3001
+ textDecoration: "text-decoration",
3002
+ dominantBaseline: "dominant-baseline",
3003
+ alignmentBaseline: "alignment-baseline",
3004
+ baselineShift: "baseline-shift",
3005
+ letterSpacing: "letter-spacing",
3006
+ wordSpacing: "word-spacing",
3007
+ writingMode: "writing-mode",
3008
+ glyphOrientationHorizontal: "glyph-orientation-horizontal",
3009
+ glyphOrientationVertical: "glyph-orientation-vertical",
3010
+ // SVG marker/gradient/filter attributes
3011
+ markerStart: "marker-start",
3012
+ markerMid: "marker-mid",
3013
+ markerEnd: "marker-end",
3014
+ gradientUnits: "gradientUnits",
3015
+ gradientTransform: "gradientTransform",
3016
+ spreadMethod: "spreadMethod",
3017
+ patternUnits: "patternUnits",
3018
+ patternContentUnits: "patternContentUnits",
3019
+ patternTransform: "patternTransform",
3020
+ maskUnits: "maskUnits",
3021
+ maskContentUnits: "maskContentUnits",
3022
+ filterUnits: "filterUnits",
3023
+ primitiveUnits: "primitiveUnits",
3024
+ clipPathUnits: "clipPathUnits",
3025
+ // SVG xlink (legacy but still used)
3026
+ xlinkHref: "xlink:href",
3027
+ xlinkShow: "xlink:show",
3028
+ xlinkActuate: "xlink:actuate",
3029
+ xlinkType: "xlink:type",
3030
+ xlinkRole: "xlink:role",
3031
+ xlinkTitle: "xlink:title",
3032
+ xlinkArcrole: "xlink:arcrole",
3033
+ xmlBase: "xml:base",
3034
+ xmlLang: "xml:lang",
3035
+ xmlSpace: "xml:space",
3036
+ xmlns: "xmlns",
3037
+ xmlnsXlink: "xmlns:xlink",
3038
+ // SVG filter primitive attributes
3039
+ stdDeviation: "stdDeviation",
3040
+ baseFrequency: "baseFrequency",
3041
+ numOctaves: "numOctaves",
3042
+ kernelMatrix: "kernelMatrix",
3043
+ surfaceScale: "surfaceScale",
3044
+ specularConstant: "specularConstant",
3045
+ specularExponent: "specularExponent",
3046
+ diffuseConstant: "diffuseConstant",
3047
+ pointsAtX: "pointsAtX",
3048
+ pointsAtY: "pointsAtY",
3049
+ pointsAtZ: "pointsAtZ",
3050
+ limitingConeAngle: "limitingConeAngle",
3051
+ tableValues: "tableValues"
3052
+ // viewBox, preserveAspectRatio stay as-is (already correct casing)
3053
+ };
2453
3054
  function renderAttr(key, value) {
3055
+ const name = ATTR_ALIASES[key] ?? key;
2454
3056
  if (value == null || value === false) return "";
2455
- if (value === true) return ` ${key}`;
2456
- const name = key === "className" ? "class" : key === "htmlFor" ? "for" : key;
3057
+ if (value === true) return ` ${name}`;
2457
3058
  let strValue;
2458
3059
  if (value instanceof SafeHtml) {
2459
3060
  strValue = value.__html;
@@ -2501,8 +3102,19 @@ pageRoutes.get("/", (c) => {
2501
3102
  /* @__PURE__ */ jsx("div", { className: "header-controls", children: [
2502
3103
  /* @__PURE__ */ jsx("div", { className: "search-box", children: /* @__PURE__ */ jsx("input", { type: "text", id: "search-input", placeholder: "Search tickets..." }) }),
2503
3104
  /* @__PURE__ */ jsx("div", { className: "layout-toggle", id: "layout-toggle", children: [
2504
- /* @__PURE__ */ jsx("button", { className: "layout-btn active", "data-layout": "list", title: "List view", children: raw('<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>') }),
2505
- /* @__PURE__ */ jsx("button", { className: "layout-btn", "data-layout": "columns", title: "Column view", children: raw('<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="M15 3v18"/></svg>') })
3105
+ /* @__PURE__ */ jsx("button", { className: "layout-btn active", "data-layout": "list", title: "List view", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3106
+ /* @__PURE__ */ jsx("line", { x1: "8", y1: "6", x2: "21", y2: "6" }),
3107
+ /* @__PURE__ */ jsx("line", { x1: "8", y1: "12", x2: "21", y2: "12" }),
3108
+ /* @__PURE__ */ jsx("line", { x1: "8", y1: "18", x2: "21", y2: "18" }),
3109
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "6", x2: "3.01", y2: "6" }),
3110
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "12", x2: "3.01", y2: "12" }),
3111
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "18", x2: "3.01", y2: "18" })
3112
+ ] }) }),
3113
+ /* @__PURE__ */ jsx("button", { className: "layout-btn", "data-layout": "columns", title: "Column view", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3114
+ /* @__PURE__ */ jsx("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
3115
+ /* @__PURE__ */ jsx("path", { d: "M9 3v18" }),
3116
+ /* @__PURE__ */ jsx("path", { d: "M15 3v18" })
3117
+ ] }) })
2506
3118
  ] }),
2507
3119
  /* @__PURE__ */ jsx("div", { className: "sort-controls", children: /* @__PURE__ */ jsx("select", { id: "sort-select", children: [
2508
3120
  /* @__PURE__ */ jsx("option", { value: "created:desc", children: "Newest First" }),
@@ -2512,11 +3124,25 @@ pageRoutes.get("/", (c) => {
2512
3124
  /* @__PURE__ */ jsx("option", { value: "status:asc", children: "Status" })
2513
3125
  ] }) }),
2514
3126
  /* @__PURE__ */ jsx("div", { className: "layout-toggle", id: "detail-position-toggle", children: [
2515
- /* @__PURE__ */ jsx("button", { className: "layout-btn active", "data-position": "side", title: "Detail panel on side", children: raw('<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="15" y1="3" x2="15" y2="21"/></svg>') }),
2516
- /* @__PURE__ */ jsx("button", { className: "layout-btn", "data-position": "bottom", title: "Detail panel on bottom", children: raw('<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="15" x2="21" y2="15"/></svg>') })
3127
+ /* @__PURE__ */ jsx("button", { className: "layout-btn active", "data-position": "side", title: "Detail panel on side", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: [
3128
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
3129
+ /* @__PURE__ */ jsx("line", { x1: "15", y1: "3", x2: "15", y2: "21" })
3130
+ ] }) }),
3131
+ /* @__PURE__ */ jsx("button", { className: "layout-btn", "data-position": "bottom", title: "Detail panel on bottom", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: [
3132
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
3133
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "15", x2: "21", y2: "15" })
3134
+ ] }) })
2517
3135
  ] }),
2518
- /* @__PURE__ */ jsx("button", { className: "glassbox-btn", id: "glassbox-btn", title: "Open Glassbox", style: "display:none", children: raw('<img id="glassbox-icon" alt="Glassbox" />') }),
2519
- /* @__PURE__ */ jsx("button", { className: "settings-btn", id: "settings-btn", title: "Settings", children: raw('<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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"/><circle cx="12" cy="12" r="3"/></svg>') })
3136
+ /* @__PURE__ */ jsx("button", { className: "glassbox-btn", id: "glassbox-btn", title: "Open Glassbox", style: "display:none", children: /* @__PURE__ */ jsx("img", { id: "glassbox-icon", alt: "Glassbox" }) }),
3137
+ /* @__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: [
3138
+ /* @__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" }),
3139
+ /* @__PURE__ */ jsx("path", { d: "M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6" }),
3140
+ /* @__PURE__ */ jsx("rect", { x: "6", y: "14", width: "12", height: "8", rx: "1" })
3141
+ ] }) }),
3142
+ /* @__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: [
3143
+ /* @__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" }),
3144
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" })
3145
+ ] }) })
2520
3146
  ] })
2521
3147
  ] }),
2522
3148
  /* @__PURE__ */ jsx("div", { id: "backup-preview-banner", className: "backup-preview-banner", style: "display:none", children: [
@@ -2540,17 +3166,28 @@ pageRoutes.get("/", (c) => {
2540
3166
  /* @__PURE__ */ jsx("div", { className: "app-body", children: [
2541
3167
  /* @__PURE__ */ jsx("nav", { className: "sidebar", children: [
2542
3168
  /* @__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: [
2543
- /* @__PURE__ */ jsx("span", { className: "copy-prompt-icon", id: "copy-prompt-icon", children: raw('<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>') }),
3169
+ /* @__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
+ /* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2" }),
3171
+ /* @__PURE__ */ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
3172
+ ] }) }),
2544
3173
  /* @__PURE__ */ jsx("span", { id: "copy-prompt-label", children: "Copy AI prompt" })
2545
3174
  ] }) }),
2546
3175
  /* @__PURE__ */ jsx("div", { className: "sidebar-section", children: [
2547
- /* @__PURE__ */ jsx("div", { className: "sidebar-label", children: "Views" }),
3176
+ /* @__PURE__ */ jsx("div", { className: "sidebar-label", children: [
3177
+ "Views ",
3178
+ /* @__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: [
3179
+ /* @__PURE__ */ jsx("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
3180
+ /* @__PURE__ */ jsx("path", { d: "M8 12h8" }),
3181
+ /* @__PURE__ */ jsx("path", { d: "M12 8v8" })
3182
+ ] }) })
3183
+ ] }),
2548
3184
  /* @__PURE__ */ jsx("button", { className: "sidebar-item active", "data-view": "all", children: "All Tickets" }),
2549
3185
  /* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "non-verified", children: "Non-Verified" }),
2550
3186
  /* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "up-next", children: "Up Next" }),
2551
3187
  /* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "open", children: "Open" }),
2552
3188
  /* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "completed", children: "Completed" }),
2553
3189
  /* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "verified", children: "Verified" }),
3190
+ /* @__PURE__ */ jsx("div", { id: "custom-views-container" }),
2554
3191
  /* @__PURE__ */ jsx("div", { className: "sidebar-divider" }),
2555
3192
  /* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "backlog", children: "Backlog" }),
2556
3193
  /* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "archive", children: "Archive" }),
@@ -2597,84 +3234,51 @@ pageRoutes.get("/", (c) => {
2597
3234
  /* @__PURE__ */ jsx("main", { className: "main-content", children: [
2598
3235
  /* @__PURE__ */ jsx("div", { className: "batch-toolbar", id: "batch-toolbar", children: [
2599
3236
  /* @__PURE__ */ jsx("input", { type: "checkbox", id: "batch-select-all", className: "batch-select-all", title: "Select all / none" }),
2600
- /* @__PURE__ */ jsx("select", { id: "batch-category", title: "Set category", disabled: true, children: [
2601
- /* @__PURE__ */ jsx("option", { value: "", children: "Category..." }),
2602
- /* @__PURE__ */ jsx("option", { value: "issue", children: "Issue" }),
2603
- /* @__PURE__ */ jsx("option", { value: "bug", children: "Bug" }),
2604
- /* @__PURE__ */ jsx("option", { value: "feature", children: "Feature" }),
2605
- /* @__PURE__ */ jsx("option", { value: "requirement_change", children: "Req Change" }),
2606
- /* @__PURE__ */ jsx("option", { value: "task", children: "Task" }),
2607
- /* @__PURE__ */ jsx("option", { value: "investigation", children: "Investigation" })
2608
- ] }),
2609
- /* @__PURE__ */ jsx("select", { id: "batch-priority", title: "Set priority", disabled: true, children: [
2610
- /* @__PURE__ */ jsx("option", { value: "", children: "Priority..." }),
2611
- /* @__PURE__ */ jsx("option", { value: "highest", children: "Highest" }),
2612
- /* @__PURE__ */ jsx("option", { value: "high", children: "High" }),
2613
- /* @__PURE__ */ jsx("option", { value: "default", children: "Default" }),
2614
- /* @__PURE__ */ jsx("option", { value: "low", children: "Low" }),
2615
- /* @__PURE__ */ jsx("option", { value: "lowest", children: "Lowest" })
2616
- ] }),
2617
- /* @__PURE__ */ jsx("select", { id: "batch-status", title: "Set status", disabled: true, children: [
2618
- /* @__PURE__ */ jsx("option", { value: "", children: "Status..." }),
2619
- /* @__PURE__ */ jsx("option", { value: "not_started", children: "Not Started" }),
2620
- /* @__PURE__ */ jsx("option", { value: "started", children: "Started" }),
2621
- /* @__PURE__ */ jsx("option", { value: "completed", children: "Completed" }),
2622
- /* @__PURE__ */ jsx("option", { value: "verified", children: "Verified" }),
2623
- /* @__PURE__ */ jsx("option", { value: "backlog", children: "Backlog" }),
2624
- /* @__PURE__ */ jsx("option", { value: "archive", children: "Archive" })
2625
- ] }),
2626
- /* @__PURE__ */ jsx("button", { id: "batch-upnext", className: "batch-star-btn", title: "Toggle Up Next", disabled: true, children: raw('<span class="batch-star-icon">&#9734;</span>') }),
2627
- /* @__PURE__ */ jsx("button", { id: "batch-delete", className: "btn btn-sm btn-danger batch-delete-btn", title: "Delete selected", disabled: true, children: raw('<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>') }),
2628
- /* @__PURE__ */ jsx("button", { id: "batch-more", className: "btn btn-sm batch-more-btn", title: "More actions", disabled: true, children: raw('<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>') }),
3237
+ /* @__PURE__ */ jsx("button", { id: "batch-category", className: "btn btn-sm batch-dropdown-btn", title: "Set category", disabled: true, children: "Category" }),
3238
+ /* @__PURE__ */ jsx("button", { id: "batch-priority", className: "btn btn-sm batch-dropdown-btn", title: "Set priority", disabled: true, children: "Priority" }),
3239
+ /* @__PURE__ */ jsx("button", { id: "batch-status", className: "btn btn-sm batch-dropdown-btn", title: "Set status", disabled: true, children: "Status" }),
3240
+ /* @__PURE__ */ jsx("button", { id: "batch-upnext", className: "batch-star-btn", title: "Toggle Up Next", disabled: true, children: /* @__PURE__ */ jsx("span", { className: "batch-star-icon", children: "\u2606" }) }),
3241
+ /* @__PURE__ */ jsx("button", { id: "batch-delete", className: "btn btn-sm btn-danger batch-delete-btn", title: "Delete selected", disabled: true, children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3242
+ /* @__PURE__ */ jsx("path", { d: "M3 6h18" }),
3243
+ /* @__PURE__ */ jsx("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
3244
+ /* @__PURE__ */ jsx("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" }),
3245
+ /* @__PURE__ */ jsx("line", { x1: "10", y1: "11", x2: "10", y2: "17" }),
3246
+ /* @__PURE__ */ jsx("line", { x1: "14", y1: "11", x2: "14", y2: "17" })
3247
+ ] }) }),
3248
+ /* @__PURE__ */ jsx("button", { id: "batch-more", className: "btn btn-sm batch-more-btn", title: "More actions", disabled: true, children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3249
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "1" }),
3250
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "12", r: "1" }),
3251
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "12", r: "1" })
3252
+ ] }) }),
2629
3253
  /* @__PURE__ */ jsx("span", { className: "batch-count", id: "batch-count" })
2630
3254
  ] }),
2631
- /* @__PURE__ */ jsx("div", { className: "ticket-list", id: "ticket-list", children: raw('<div class="ticket-list-loading">Loading...</div>') })
3255
+ /* @__PURE__ */ jsx("div", { className: "ticket-list", id: "ticket-list", children: /* @__PURE__ */ jsx("div", { className: "ticket-list-loading", children: "Loading..." }) })
2632
3256
  ] }),
2633
3257
  /* @__PURE__ */ jsx("div", { className: "detail-resize-handle", id: "detail-resize-handle" }),
2634
3258
  /* @__PURE__ */ jsx("aside", { className: "detail-panel detail-disabled", id: "detail-panel", children: [
2635
3259
  /* @__PURE__ */ jsx("div", { className: "detail-placeholder", id: "detail-placeholder", children: /* @__PURE__ */ jsx("span", { className: "detail-placeholder-text", id: "detail-placeholder-text", children: "Nothing selected" }) }),
2636
3260
  /* @__PURE__ */ jsx("div", { className: "detail-header", id: "detail-header", style: "display:none", children: [
2637
3261
  /* @__PURE__ */ jsx("span", { className: "detail-ticket-number", id: "detail-ticket-number" }),
2638
- /* @__PURE__ */ jsx("button", { className: "detail-close", id: "detail-close", title: "Close", children: raw("&times;") })
3262
+ /* @__PURE__ */ jsx("button", { className: "detail-close", id: "detail-close", title: "Close", children: "\xD7" })
2639
3263
  ] }),
2640
3264
  /* @__PURE__ */ jsx("div", { className: "detail-body", id: "detail-body", style: "display:none", children: [
2641
3265
  /* @__PURE__ */ jsx("div", { className: "detail-fields-row", children: [
2642
3266
  /* @__PURE__ */ jsx("div", { className: "detail-field", children: [
2643
3267
  /* @__PURE__ */ jsx("label", { children: "Category" }),
2644
- /* @__PURE__ */ jsx("select", { id: "detail-category", children: [
2645
- /* @__PURE__ */ jsx("option", { value: "issue", children: "Issue" }),
2646
- /* @__PURE__ */ jsx("option", { value: "bug", children: "Bug" }),
2647
- /* @__PURE__ */ jsx("option", { value: "feature", children: "Feature" }),
2648
- /* @__PURE__ */ jsx("option", { value: "requirement_change", children: "Req Change" }),
2649
- /* @__PURE__ */ jsx("option", { value: "task", children: "Task" }),
2650
- /* @__PURE__ */ jsx("option", { value: "investigation", children: "Investigation" })
2651
- ] })
3268
+ /* @__PURE__ */ jsx("button", { id: "detail-category", className: "detail-dropdown-btn", "data-value": "issue", children: "Issue" })
2652
3269
  ] }),
2653
3270
  /* @__PURE__ */ jsx("div", { className: "detail-field", children: [
2654
3271
  /* @__PURE__ */ jsx("label", { children: "Priority" }),
2655
- /* @__PURE__ */ jsx("select", { id: "detail-priority", children: [
2656
- /* @__PURE__ */ jsx("option", { value: "highest", children: "Highest" }),
2657
- /* @__PURE__ */ jsx("option", { value: "high", children: "High" }),
2658
- /* @__PURE__ */ jsx("option", { value: "default", children: "Default" }),
2659
- /* @__PURE__ */ jsx("option", { value: "low", children: "Low" }),
2660
- /* @__PURE__ */ jsx("option", { value: "lowest", children: "Lowest" })
2661
- ] })
3272
+ /* @__PURE__ */ jsx("button", { id: "detail-priority", className: "detail-dropdown-btn", "data-value": "default", children: "Default" })
2662
3273
  ] }),
2663
3274
  /* @__PURE__ */ jsx("div", { className: "detail-field", children: [
2664
3275
  /* @__PURE__ */ jsx("label", { children: "Status" }),
2665
- /* @__PURE__ */ jsx("select", { id: "detail-status", children: [
2666
- /* @__PURE__ */ jsx("option", { value: "not_started", children: "Not Started" }),
2667
- /* @__PURE__ */ jsx("option", { value: "started", children: "Started" }),
2668
- /* @__PURE__ */ jsx("option", { value: "completed", children: "Completed" }),
2669
- /* @__PURE__ */ jsx("option", { value: "verified", children: "Verified" }),
2670
- /* @__PURE__ */ jsx("option", { value: "backlog", children: "Backlog" }),
2671
- /* @__PURE__ */ jsx("option", { value: "archive", children: "Archive" })
2672
- ] })
3276
+ /* @__PURE__ */ jsx("button", { id: "detail-status", className: "detail-dropdown-btn", "data-value": "not_started", children: "Not Started" })
2673
3277
  ] }),
2674
- /* @__PURE__ */ jsx("div", { className: "detail-field", children: /* @__PURE__ */ jsx("label", { className: "detail-upnext-label", children: [
2675
- /* @__PURE__ */ jsx("input", { type: "checkbox", id: "detail-upnext" }),
2676
- "Up Next"
2677
- ] }) })
3278
+ /* @__PURE__ */ jsx("div", { className: "detail-field", children: [
3279
+ /* @__PURE__ */ jsx("label", { children: "Up Next" }),
3280
+ /* @__PURE__ */ jsx("button", { className: "ticket-star detail-upnext-star", id: "detail-upnext", type: "button", children: "\u2606" })
3281
+ ] })
2678
3282
  ] }),
2679
3283
  /* @__PURE__ */ jsx("div", { className: "detail-field detail-field-full", children: [
2680
3284
  /* @__PURE__ */ jsx("label", { children: "Title" }),
@@ -2684,6 +3288,11 @@ pageRoutes.get("/", (c) => {
2684
3288
  /* @__PURE__ */ jsx("label", { children: "Details" }),
2685
3289
  /* @__PURE__ */ jsx("textarea", { id: "detail-details", rows: 6, placeholder: "Add details..." })
2686
3290
  ] }),
3291
+ /* @__PURE__ */ jsx("div", { className: "detail-field detail-field-full", children: [
3292
+ /* @__PURE__ */ jsx("label", { children: "Tags" }),
3293
+ /* @__PURE__ */ jsx("div", { id: "detail-tags", className: "detail-tags" }),
3294
+ /* @__PURE__ */ jsx("input", { type: "text", id: "detail-tag-input", className: "detail-tag-input", placeholder: "Add tag..." })
3295
+ ] }),
2687
3296
  /* @__PURE__ */ jsx("div", { className: "detail-field detail-field-full", children: [
2688
3297
  /* @__PURE__ */ jsx("label", { children: "Attachments" }),
2689
3298
  /* @__PURE__ */ jsx("div", { id: "detail-attachments", className: "detail-attachments" }),
@@ -2692,8 +3301,15 @@ pageRoutes.get("/", (c) => {
2692
3301
  /* @__PURE__ */ jsx("input", { type: "file", id: "detail-file-input", style: "display:none" })
2693
3302
  ] })
2694
3303
  ] }),
2695
- /* @__PURE__ */ jsx("div", { className: "detail-field detail-field-full", id: "detail-notes-section", style: "display:none", children: [
2696
- /* @__PURE__ */ jsx("label", { children: "Notes" }),
3304
+ /* @__PURE__ */ jsx("div", { className: "detail-field detail-field-full", id: "detail-notes-section", children: [
3305
+ /* @__PURE__ */ jsx("div", { className: "detail-notes-label", children: [
3306
+ /* @__PURE__ */ jsx("span", { children: "Notes" }),
3307
+ " ",
3308
+ /* @__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: [
3309
+ /* @__PURE__ */ jsx("path", { d: "M5 12h14" }),
3310
+ /* @__PURE__ */ jsx("path", { d: "M12 5v14" })
3311
+ ] }) })
3312
+ ] }),
2697
3313
  /* @__PURE__ */ jsx("div", { id: "detail-notes", className: "detail-notes" })
2698
3314
  ] }),
2699
3315
  /* @__PURE__ */ jsx("div", { className: "detail-meta detail-field-full", id: "detail-meta" })
@@ -2709,7 +3325,7 @@ pageRoutes.get("/", (c) => {
2709
3325
  ] }),
2710
3326
  /* @__PURE__ */ jsx("span", { children: [
2711
3327
  /* @__PURE__ */ jsx("kbd", { children: [
2712
- raw("&#8984;"),
3328
+ "\u2318",
2713
3329
  "I/B/F/R/K/G"
2714
3330
  ] }),
2715
3331
  " category"
@@ -2720,7 +3336,7 @@ pageRoutes.get("/", (c) => {
2720
3336
  ] }),
2721
3337
  /* @__PURE__ */ jsx("span", { children: [
2722
3338
  /* @__PURE__ */ jsx("kbd", { children: [
2723
- raw("&#8984;"),
3339
+ "\u2318",
2724
3340
  "D"
2725
3341
  ] }),
2726
3342
  " up next"
@@ -2736,23 +3352,73 @@ pageRoutes.get("/", (c) => {
2736
3352
  /* @__PURE__ */ jsx("div", { className: "settings-overlay", id: "settings-overlay", style: "display:none", children: /* @__PURE__ */ jsx("div", { className: "settings-dialog", children: [
2737
3353
  /* @__PURE__ */ jsx("div", { className: "settings-header", children: [
2738
3354
  /* @__PURE__ */ jsx("h2", { children: "Settings" }),
2739
- /* @__PURE__ */ jsx("button", { className: "detail-close", id: "settings-close", children: raw("&times;") })
3355
+ /* @__PURE__ */ jsx("button", { className: "detail-close", id: "settings-close", children: "\xD7" })
2740
3356
  ] }),
2741
- /* @__PURE__ */ jsx("div", { className: "settings-body", children: [
2742
- /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
2743
- /* @__PURE__ */ jsx("label", { children: "App name" }),
2744
- /* @__PURE__ */ jsx("input", { type: "text", id: "settings-app-name", placeholder: "Hot Sheet" }),
2745
- /* @__PURE__ */ jsx("span", { className: "settings-hint", id: "settings-app-name-hint", children: "Custom name shown in the title bar. Leave empty for default." })
3357
+ /* @__PURE__ */ jsx("div", { className: "settings-tabs", id: "settings-tabs", children: [
3358
+ /* @__PURE__ */ jsx("button", { className: "settings-tab active", "data-tab": "general", children: [
3359
+ /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3360
+ /* @__PURE__ */ jsx("line", { x1: "21", x2: "14", y1: "4", y2: "4" }),
3361
+ /* @__PURE__ */ jsx("line", { x1: "10", x2: "3", y1: "4", y2: "4" }),
3362
+ /* @__PURE__ */ jsx("line", { x1: "21", x2: "12", y1: "12", y2: "12" }),
3363
+ /* @__PURE__ */ jsx("line", { x1: "8", x2: "3", y1: "12", y2: "12" }),
3364
+ /* @__PURE__ */ jsx("line", { x1: "21", x2: "16", y1: "20", y2: "20" }),
3365
+ /* @__PURE__ */ jsx("line", { x1: "12", x2: "3", y1: "20", y2: "20" }),
3366
+ /* @__PURE__ */ jsx("line", { x1: "14", x2: "14", y1: "2", y2: "6" }),
3367
+ /* @__PURE__ */ jsx("line", { x1: "8", x2: "8", y1: "10", y2: "14" }),
3368
+ /* @__PURE__ */ jsx("line", { x1: "16", x2: "16", y1: "18", y2: "22" })
3369
+ ] }),
3370
+ /* @__PURE__ */ jsx("span", { children: "General" })
2746
3371
  ] }),
2747
- /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
2748
- /* @__PURE__ */ jsx("label", { children: "Auto-clear trash after (days)" }),
2749
- /* @__PURE__ */ jsx("input", { type: "number", id: "settings-trash-days", min: "1", value: "3" })
3372
+ /* @__PURE__ */ jsx("button", { className: "settings-tab", "data-tab": "categories", children: [
3373
+ /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3374
+ /* @__PURE__ */ jsx("path", { d: "M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z" }),
3375
+ /* @__PURE__ */ jsx("circle", { cx: "7.5", cy: "7.5", r: ".5", fill: "currentColor" })
3376
+ ] }),
3377
+ /* @__PURE__ */ jsx("span", { children: "Categories" })
2750
3378
  ] }),
2751
- /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
2752
- /* @__PURE__ */ jsx("label", { children: "Auto-clear verified after (days)" }),
2753
- /* @__PURE__ */ jsx("input", { type: "number", id: "settings-verified-days", min: "1", value: "30" })
3379
+ /* @__PURE__ */ jsx("button", { className: "settings-tab", "data-tab": "backups", children: [
3380
+ /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3381
+ /* @__PURE__ */ jsx("line", { x1: "22", x2: "2", y1: "12", y2: "12" }),
3382
+ /* @__PURE__ */ jsx("path", { d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" }),
3383
+ /* @__PURE__ */ jsx("line", { x1: "6", x2: "6.01", y1: "16", y2: "16" }),
3384
+ /* @__PURE__ */ jsx("line", { x1: "10", x2: "10.01", y1: "16", y2: "16" })
3385
+ ] }),
3386
+ /* @__PURE__ */ jsx("span", { children: "Backups" })
2754
3387
  ] }),
2755
- /* @__PURE__ */ jsx("div", { className: "settings-section", children: [
3388
+ /* @__PURE__ */ jsx("button", { className: "settings-tab", "data-tab": "updates", id: "settings-tab-updates", style: "display:none", children: [
3389
+ /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3390
+ /* @__PURE__ */ jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
3391
+ /* @__PURE__ */ jsx("polyline", { points: "7 10 12 15 17 10" }),
3392
+ /* @__PURE__ */ jsx("line", { x1: "12", x2: "12", y1: "15", y2: "3" })
3393
+ ] }),
3394
+ /* @__PURE__ */ jsx("span", { children: "Updates" })
3395
+ ] })
3396
+ ] }),
3397
+ /* @__PURE__ */ jsx("div", { className: "settings-body", children: [
3398
+ /* @__PURE__ */ jsx("div", { className: "settings-tab-panel active", "data-panel": "general", children: [
3399
+ /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
3400
+ /* @__PURE__ */ jsx("label", { children: "App name" }),
3401
+ /* @__PURE__ */ jsx("input", { type: "text", id: "settings-app-name", placeholder: "Hot Sheet" }),
3402
+ /* @__PURE__ */ jsx("span", { className: "settings-hint", id: "settings-app-name-hint", children: "Custom name shown in the title bar. Leave empty for default." })
3403
+ ] }),
3404
+ /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
3405
+ /* @__PURE__ */ jsx("label", { children: "Auto-clear trash after (days)" }),
3406
+ /* @__PURE__ */ jsx("input", { type: "number", id: "settings-trash-days", min: "1", value: "3" })
3407
+ ] }),
3408
+ /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
3409
+ /* @__PURE__ */ jsx("label", { children: "Auto-clear verified after (days)" }),
3410
+ /* @__PURE__ */ jsx("input", { type: "number", id: "settings-verified-days", min: "1", value: "30" })
3411
+ ] })
3412
+ ] }),
3413
+ /* @__PURE__ */ jsx("div", { className: "settings-tab-panel", "data-panel": "categories", children: [
3414
+ /* @__PURE__ */ jsx("div", { className: "settings-section-header", children: [
3415
+ /* @__PURE__ */ jsx("h3", { children: "Categories" }),
3416
+ /* @__PURE__ */ jsx("div", { className: "category-preset-controls", children: /* @__PURE__ */ jsx("select", { id: "category-preset-select", className: "btn btn-sm", children: /* @__PURE__ */ jsx("option", { value: "", children: "Load preset..." }) }) })
3417
+ ] }),
3418
+ /* @__PURE__ */ jsx("div", { id: "category-list", className: "category-list" }),
3419
+ /* @__PURE__ */ jsx("button", { id: "category-add-btn", className: "btn btn-sm", style: "margin-top:8px", children: "Add Category" })
3420
+ ] }),
3421
+ /* @__PURE__ */ jsx("div", { className: "settings-tab-panel", "data-panel": "backups", children: [
2756
3422
  /* @__PURE__ */ jsx("div", { className: "settings-section-header", children: [
2757
3423
  /* @__PURE__ */ jsx("h3", { children: "Database Backups" }),
2758
3424
  /* @__PURE__ */ jsx("button", { className: "btn btn-sm", id: "backup-now-btn", children: "Backup Now" })
@@ -2764,7 +3430,7 @@ pageRoutes.get("/", (c) => {
2764
3430
  ] }),
2765
3431
  /* @__PURE__ */ jsx("div", { id: "backup-list", className: "backup-list", children: "Loading backups..." })
2766
3432
  ] }),
2767
- /* @__PURE__ */ jsx("div", { className: "settings-section", id: "settings-updates-section", style: "display:none", children: [
3433
+ /* @__PURE__ */ jsx("div", { className: "settings-tab-panel", "data-panel": "updates", id: "settings-updates-section", style: "display:none", children: [
2768
3434
  /* @__PURE__ */ jsx("div", { className: "settings-section-header", children: [
2769
3435
  /* @__PURE__ */ jsx("h3", { children: "Software Updates" }),
2770
3436
  /* @__PURE__ */ jsx("button", { className: "btn btn-sm", id: "check-updates-btn", children: "Check for Updates" })
@@ -3076,12 +3742,18 @@ async function main() {
3076
3742
  initMarkdownSync(dataDir2, actualPort);
3077
3743
  scheduleAllSync();
3078
3744
  initSkills(actualPort, dataDir2);
3745
+ setSkillCategories(await getCategories());
3079
3746
  const updatedPlatforms = ensureSkills();
3080
3747
  if (updatedPlatforms.length > 0) {
3081
3748
  console.log(`
3082
3749
  AI tool skills created/updated for: ${updatedPlatforms.join(", ")}`);
3083
3750
  console.log(" Restart your AI tool to pick up the new ticket creation skills.\n");
3084
3751
  }
3752
+ Promise.resolve().then(() => (init_stats(), stats_exports)).then(async ({ recordDailySnapshot: recordDailySnapshot2, backfillSnapshots: backfillSnapshots2 }) => {
3753
+ await backfillSnapshots2();
3754
+ await recordDailySnapshot2();
3755
+ }).catch(() => {
3756
+ });
3085
3757
  if (demo === null) {
3086
3758
  initBackupScheduler(dataDir2);
3087
3759
  }