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 +824 -152
- package/dist/client/app.global.js +99 -3
- package/dist/client/styles.css +1 -1
- package/package.json +2 -2
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
|
-
|
|
426
|
-
|
|
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(
|
|
429
|
-
if (Array.isArray(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:
|
|
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 '
|
|
582
|
-
WHEN '
|
|
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
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
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. ${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
1897
|
-
if (!
|
|
2309
|
+
function parseTicketNotes(raw) {
|
|
2310
|
+
if (!raw || raw === "") return [];
|
|
1898
2311
|
try {
|
|
1899
|
-
const parsed = JSON.parse(
|
|
2312
|
+
const parsed = JSON.parse(raw);
|
|
1900
2313
|
if (Array.isArray(parsed)) return parsed;
|
|
1901
2314
|
} catch {
|
|
1902
2315
|
}
|
|
1903
|
-
if (
|
|
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(
|
|
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
|
|
1941
|
-
lines.push(`- ${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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ` ${
|
|
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:
|
|
2505
|
-
|
|
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:
|
|
2516
|
-
|
|
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:
|
|
2519
|
-
/* @__PURE__ */ jsx("button", { className: "settings-btn", id: "
|
|
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:
|
|
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:
|
|
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("
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
/* @__PURE__ */ jsx("
|
|
2606
|
-
/* @__PURE__ */ jsx("
|
|
2607
|
-
/* @__PURE__ */ jsx("
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
/* @__PURE__ */ jsx("
|
|
2613
|
-
/* @__PURE__ */ jsx("
|
|
2614
|
-
/* @__PURE__ */ jsx("
|
|
2615
|
-
|
|
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">☆</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:
|
|
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:
|
|
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("
|
|
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("
|
|
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("
|
|
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:
|
|
2675
|
-
/* @__PURE__ */ jsx("
|
|
2676
|
-
"
|
|
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",
|
|
2696
|
-
/* @__PURE__ */ jsx("
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
3355
|
+
/* @__PURE__ */ jsx("button", { className: "detail-close", id: "settings-close", children: "\xD7" })
|
|
2740
3356
|
] }),
|
|
2741
|
-
/* @__PURE__ */ jsx("div", { className: "settings-
|
|
2742
|
-
/* @__PURE__ */ jsx("
|
|
2743
|
-
/* @__PURE__ */ jsx("
|
|
2744
|
-
|
|
2745
|
-
|
|
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("
|
|
2748
|
-
/* @__PURE__ */ jsx("
|
|
2749
|
-
|
|
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("
|
|
2752
|
-
/* @__PURE__ */ jsx("
|
|
2753
|
-
|
|
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("
|
|
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-
|
|
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
|
}
|