hotsheet 0.10.4 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -8
- package/dist/cli.js +359 -40
- package/dist/client/app.global.js +61 -47
- package/dist/client/styles.css +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,20 +107,21 @@ The loop stays tight because the AI always knows what to work on next.
|
|
|
107
107
|
**Also includes:**
|
|
108
108
|
- **Tags** — free-form tags on tickets, with autocomplete and a batch tag dialog for multi-select
|
|
109
109
|
- **Custom views** — create filtered views with an interactive query builder (field + operator + value conditions, AND/OR logic)
|
|
110
|
-
- **Five priority levels** — Highest to Lowest, sortable and filterable
|
|
110
|
+
- **Five priority levels** — Highest to Lowest, with Lucide chevron icons, sortable and filterable
|
|
111
111
|
- **Up Next flag** — star tickets to add them to the AI worklist
|
|
112
|
-
- **Drag and drop** — drag tickets onto sidebar views to change category, priority, or status; reorder custom views
|
|
113
|
-
- **Right-click context menus** — full context menu on tickets with category/priority/status submenus, tags, duplicate, delete
|
|
112
|
+
- **Drag and drop** — drag tickets onto sidebar views to change category, priority, or status; drop files onto the detail panel to attach; reorder custom views
|
|
113
|
+
- **Right-click context menus** — full context menu on tickets with category/priority/status submenus, tags, duplicate, backlog, archive, delete — all with Lucide icons
|
|
114
114
|
- **Search** — full-text search across ticket titles, details, and ticket numbers
|
|
115
115
|
- **Print** — print the dashboard, all tickets, selected tickets, or individual tickets in checklist, summary, or full-detail format
|
|
116
116
|
- **Keyboard-driven** — `Enter` to create, `Cmd+I/B/F/R/K/G` for categories, `Alt+1-5` for priority, `Cmd+D` for Up Next, `Delete` to trash, `Cmd+P` to print, `Cmd+Z/Shift+Z` for undo/redo
|
|
117
117
|
- **Undo/redo** — `Cmd+Z` and `Cmd+Shift+Z` for all operations including notes, batch changes, and deletions
|
|
118
118
|
- **Animated transitions** — smooth FLIP animations when tickets reorder after property changes
|
|
119
119
|
- **Copy for commits** — `Cmd+C` copies selected ticket info (number + title + details + notes) for use in commit messages
|
|
120
|
-
- **File attachments** — attach files
|
|
120
|
+
- **File attachments** — attach files via file picker or drag-and-drop onto the detail panel, reveal in file manager
|
|
121
121
|
- **Markdown sync** — `worklist.md` and `open-tickets.md` auto-generated on every change
|
|
122
122
|
- **Automatic backups** — tiered snapshots (every 5 min, hourly, daily) with preview-before-restore recovery
|
|
123
123
|
- **Auto-cleanup** — configurable auto-deletion of old trash and verified items
|
|
124
|
+
- **App icon variants** — 9 icon variants to choose from in Settings, applied instantly to the dock icon
|
|
124
125
|
- **Fully local** — embedded PostgreSQL (PGLite), no network calls, no accounts, no telemetry
|
|
125
126
|
|
|
126
127
|
---
|
|
@@ -156,9 +157,12 @@ Hot Sheet automatically generates skill files for Claude Code (as well as Cursor
|
|
|
156
157
|
Hot Sheet can push events directly to a running Claude Code session via MCP channels. Enable it in Settings → Experimental (the tab only appears when Claude Code is detected on your system):
|
|
157
158
|
|
|
158
159
|
- **Play button** — appears in the sidebar. Single-click sends the worklist to Claude on demand.
|
|
159
|
-
- **Auto mode** — double-click the play button to enable automatic mode. When you star a ticket for Up Next, Claude is notified after a 5-second debounce and picks up the work automatically.
|
|
160
|
-
- **
|
|
161
|
-
- **
|
|
160
|
+
- **Auto mode** — double-click the play button to enable automatic mode. When you star a ticket for Up Next, Claude is notified after a 5-second debounce and picks up the work automatically. Exponential backoff prevents runaway retries.
|
|
161
|
+
- **Auto-prioritize** — when no tickets are flagged as Up Next, Claude automatically evaluates open tickets and picks the most important ones to work on.
|
|
162
|
+
- **Custom commands** — create named buttons that send custom prompts to Claude **or run shell commands** directly. Toggle between "Claude Code" and "Shell" targets per command. Shell commands execute server-side with stdout/stderr captured to the commands log.
|
|
163
|
+
- **Permission relay** — when Claude needs tool approval (Bash, Edit, etc.), a full-screen overlay shows the tool name and command preview with Allow/Deny/Dismiss buttons — no need to switch to the terminal.
|
|
164
|
+
- **Commands log** — a resizable bottom panel that records all communication: triggers, completions, permission requests, and shell command output. Filter by type, search, and copy entries. Shell commands show a stop button for running processes.
|
|
165
|
+
- **Status indicator** — shows "Claude working" / "Shell running" / idle in the footer.
|
|
162
166
|
|
|
163
167
|
Requires Claude Code v2.1.80+ with channel support. See [docs/12-claude-channel.md](docs/12-claude-channel.md) for setup details.
|
|
164
168
|
|
|
@@ -293,7 +297,10 @@ hotsheet --browser
|
|
|
293
297
|
|------|-------------|
|
|
294
298
|
| `--port <number>` | Port to run on (default: 4174) |
|
|
295
299
|
| `--data-dir <path>` | Data directory (default: `.hotsheet/`) |
|
|
300
|
+
| `--no-open` | Don't open the browser on startup |
|
|
301
|
+
| `--strict-port` | Fail if the requested port is in use |
|
|
296
302
|
| `--browser` | Open in browser instead of desktop window |
|
|
303
|
+
| `--check-for-updates` | Check for new versions |
|
|
297
304
|
| `--help` | Show help |
|
|
298
305
|
|
|
299
306
|
### Settings file
|
|
@@ -311,8 +318,9 @@ Create `.hotsheet/settings.json` to configure per-project options:
|
|
|
311
318
|
|-----|-------------|
|
|
312
319
|
| `appName` | Custom window title (defaults to the project folder name) |
|
|
313
320
|
| `backupDir` | Backup storage path (defaults to `.hotsheet/backups/`) |
|
|
321
|
+
| `appIcon` | Icon variant (`default`, `variant-1` through `variant-9`) |
|
|
314
322
|
|
|
315
|
-
|
|
323
|
+
All settings can also be changed from the settings panel UI.
|
|
316
324
|
|
|
317
325
|
### Keyboard shortcuts
|
|
318
326
|
|
|
@@ -360,10 +368,16 @@ npm install
|
|
|
360
368
|
|
|
361
369
|
npm run dev # Build client assets, then run via tsx
|
|
362
370
|
npm run build # Build to dist/cli.js
|
|
371
|
+
npm test # Unit tests with coverage (446 tests)
|
|
372
|
+
npm run test:e2e # E2E browser tests (55 tests)
|
|
373
|
+
npm run test:all # Merged coverage report (unit + E2E)
|
|
374
|
+
npm run lint # ESLint
|
|
363
375
|
npm run clean # Remove dist and caches
|
|
364
376
|
npm link # Symlink for global 'hotsheet' command
|
|
365
377
|
```
|
|
366
378
|
|
|
379
|
+
The project has comprehensive test coverage with 446 unit tests (vitest) and 55 Playwright E2E browser tests, plus 12 smoke tests for production install verification.
|
|
380
|
+
|
|
367
381
|
---
|
|
368
382
|
|
|
369
383
|
## See Also
|
package/dist/cli.js
CHANGED
|
@@ -74,10 +74,10 @@ async function initSchema(db2) {
|
|
|
74
74
|
priority TEXT NOT NULL DEFAULT 'default',
|
|
75
75
|
status TEXT NOT NULL DEFAULT 'not_started',
|
|
76
76
|
up_next BOOLEAN NOT NULL DEFAULT FALSE,
|
|
77
|
-
created_at
|
|
78
|
-
updated_at
|
|
79
|
-
completed_at
|
|
80
|
-
deleted_at
|
|
77
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
78
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
79
|
+
completed_at TIMESTAMPTZ,
|
|
80
|
+
deleted_at TIMESTAMPTZ
|
|
81
81
|
);
|
|
82
82
|
|
|
83
83
|
CREATE TABLE IF NOT EXISTS attachments (
|
|
@@ -85,7 +85,7 @@ async function initSchema(db2) {
|
|
|
85
85
|
ticket_id INTEGER NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
|
|
86
86
|
original_filename TEXT NOT NULL,
|
|
87
87
|
stored_path TEXT NOT NULL,
|
|
88
|
-
created_at
|
|
88
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
89
89
|
);
|
|
90
90
|
|
|
91
91
|
CREATE INDEX IF NOT EXISTS idx_attachments_ticket ON attachments(ticket_id);
|
|
@@ -110,9 +110,29 @@ async function initSchema(db2) {
|
|
|
110
110
|
data TEXT NOT NULL DEFAULT '{}'
|
|
111
111
|
);
|
|
112
112
|
`);
|
|
113
|
+
await db2.exec(`
|
|
114
|
+
CREATE TABLE IF NOT EXISTS command_log (
|
|
115
|
+
id SERIAL PRIMARY KEY,
|
|
116
|
+
event_type TEXT NOT NULL,
|
|
117
|
+
direction TEXT NOT NULL DEFAULT 'system',
|
|
118
|
+
summary TEXT NOT NULL DEFAULT '',
|
|
119
|
+
detail TEXT NOT NULL DEFAULT '',
|
|
120
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
121
|
+
);
|
|
122
|
+
CREATE INDEX IF NOT EXISTS idx_command_log_created ON command_log(created_at);
|
|
123
|
+
`);
|
|
124
|
+
await db2.exec(`
|
|
125
|
+
ALTER TABLE tickets ALTER COLUMN created_at TYPE TIMESTAMPTZ;
|
|
126
|
+
ALTER TABLE tickets ALTER COLUMN updated_at TYPE TIMESTAMPTZ;
|
|
127
|
+
ALTER TABLE tickets ALTER COLUMN completed_at TYPE TIMESTAMPTZ;
|
|
128
|
+
ALTER TABLE tickets ALTER COLUMN deleted_at TYPE TIMESTAMPTZ;
|
|
129
|
+
ALTER TABLE attachments ALTER COLUMN created_at TYPE TIMESTAMPTZ;
|
|
130
|
+
ALTER TABLE command_log ALTER COLUMN created_at TYPE TIMESTAMPTZ;
|
|
131
|
+
`).catch(() => {
|
|
132
|
+
});
|
|
113
133
|
await db2.exec(`
|
|
114
134
|
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS notes TEXT NOT NULL DEFAULT '';
|
|
115
|
-
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS verified_at
|
|
135
|
+
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS verified_at TIMESTAMPTZ;
|
|
116
136
|
ALTER TABLE tickets ADD COLUMN IF NOT EXISTS tags TEXT NOT NULL DEFAULT '[]';
|
|
117
137
|
`).catch(() => {
|
|
118
138
|
});
|
|
@@ -184,6 +204,108 @@ var init_file_settings = __esm({
|
|
|
184
204
|
}
|
|
185
205
|
});
|
|
186
206
|
|
|
207
|
+
// src/db/commandLog.ts
|
|
208
|
+
var commandLog_exports = {};
|
|
209
|
+
__export(commandLog_exports, {
|
|
210
|
+
addLogEntry: () => addLogEntry,
|
|
211
|
+
clearLog: () => clearLog,
|
|
212
|
+
getLogCount: () => getLogCount,
|
|
213
|
+
getLogEntries: () => getLogEntries,
|
|
214
|
+
pruneLog: () => pruneLog,
|
|
215
|
+
updateLogEntry: () => updateLogEntry
|
|
216
|
+
});
|
|
217
|
+
async function addLogEntry(eventType, direction, summary, detail) {
|
|
218
|
+
const db2 = await getDb();
|
|
219
|
+
const result = await db2.query(
|
|
220
|
+
`INSERT INTO command_log (event_type, direction, summary, detail) VALUES ($1, $2, $3, $4) RETURNING *`,
|
|
221
|
+
[eventType, direction, summary, detail]
|
|
222
|
+
);
|
|
223
|
+
return result.rows[0];
|
|
224
|
+
}
|
|
225
|
+
async function getLogEntries(options) {
|
|
226
|
+
const db2 = await getDb();
|
|
227
|
+
const conditions = [];
|
|
228
|
+
const params = [];
|
|
229
|
+
let paramIdx = 1;
|
|
230
|
+
if (options?.eventType !== void 0 && options.eventType !== "") {
|
|
231
|
+
conditions.push(`event_type = $${paramIdx++}`);
|
|
232
|
+
params.push(options.eventType);
|
|
233
|
+
}
|
|
234
|
+
if (options?.search !== void 0 && options.search !== "") {
|
|
235
|
+
conditions.push(`(summary ILIKE $${paramIdx} OR detail ILIKE $${paramIdx})`);
|
|
236
|
+
params.push(`%${options.search}%`);
|
|
237
|
+
paramIdx++;
|
|
238
|
+
}
|
|
239
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
240
|
+
const limit = options?.limit ?? 100;
|
|
241
|
+
const offset = options?.offset ?? 0;
|
|
242
|
+
const result = await db2.query(
|
|
243
|
+
`SELECT id, event_type, direction, summary, detail, created_at
|
|
244
|
+
FROM command_log ${where}
|
|
245
|
+
ORDER BY created_at DESC, id DESC
|
|
246
|
+
LIMIT $${paramIdx++} OFFSET $${paramIdx}`,
|
|
247
|
+
[...params, limit, offset]
|
|
248
|
+
);
|
|
249
|
+
return result.rows;
|
|
250
|
+
}
|
|
251
|
+
async function getLogCount(options) {
|
|
252
|
+
const db2 = await getDb();
|
|
253
|
+
const conditions = [];
|
|
254
|
+
const params = [];
|
|
255
|
+
let paramIdx = 1;
|
|
256
|
+
if (options?.eventType !== void 0 && options.eventType !== "") {
|
|
257
|
+
conditions.push(`event_type = $${paramIdx++}`);
|
|
258
|
+
params.push(options.eventType);
|
|
259
|
+
}
|
|
260
|
+
if (options?.search !== void 0 && options.search !== "") {
|
|
261
|
+
conditions.push(`(summary ILIKE $${paramIdx} OR detail ILIKE $${paramIdx})`);
|
|
262
|
+
params.push(`%${options.search}%`);
|
|
263
|
+
paramIdx++;
|
|
264
|
+
}
|
|
265
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
266
|
+
const result = await db2.query(
|
|
267
|
+
`SELECT COUNT(*) as count FROM command_log ${where}`,
|
|
268
|
+
params
|
|
269
|
+
);
|
|
270
|
+
return parseInt(result.rows[0].count, 10);
|
|
271
|
+
}
|
|
272
|
+
async function updateLogEntry(id, updates) {
|
|
273
|
+
const db2 = await getDb();
|
|
274
|
+
const sets = [];
|
|
275
|
+
const params = [];
|
|
276
|
+
let paramIdx = 1;
|
|
277
|
+
if (updates.summary !== void 0) {
|
|
278
|
+
sets.push(`summary = $${paramIdx++}`);
|
|
279
|
+
params.push(updates.summary);
|
|
280
|
+
}
|
|
281
|
+
if (updates.detail !== void 0) {
|
|
282
|
+
sets.push(`detail = $${paramIdx++}`);
|
|
283
|
+
params.push(updates.detail);
|
|
284
|
+
}
|
|
285
|
+
if (sets.length === 0) return;
|
|
286
|
+
params.push(id);
|
|
287
|
+
await db2.query(`UPDATE command_log SET ${sets.join(", ")} WHERE id = $${paramIdx}`, params);
|
|
288
|
+
}
|
|
289
|
+
async function clearLog() {
|
|
290
|
+
const db2 = await getDb();
|
|
291
|
+
await db2.query(`DELETE FROM command_log`);
|
|
292
|
+
}
|
|
293
|
+
async function pruneLog(maxEntries = 1e3) {
|
|
294
|
+
const db2 = await getDb();
|
|
295
|
+
await db2.query(
|
|
296
|
+
`DELETE FROM command_log WHERE id NOT IN (
|
|
297
|
+
SELECT id FROM command_log ORDER BY created_at DESC, id DESC LIMIT $1
|
|
298
|
+
)`,
|
|
299
|
+
[maxEntries]
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
var init_commandLog = __esm({
|
|
303
|
+
"src/db/commandLog.ts"() {
|
|
304
|
+
"use strict";
|
|
305
|
+
init_connection();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
187
309
|
// src/db/stats.ts
|
|
188
310
|
var stats_exports = {};
|
|
189
311
|
__export(stats_exports, {
|
|
@@ -441,6 +563,36 @@ var init_gitignore = __esm({
|
|
|
441
563
|
}
|
|
442
564
|
});
|
|
443
565
|
|
|
566
|
+
// src/routes/notify.ts
|
|
567
|
+
var notify_exports = {};
|
|
568
|
+
__export(notify_exports, {
|
|
569
|
+
addPollWaiter: () => addPollWaiter,
|
|
570
|
+
getChangeVersion: () => getChangeVersion,
|
|
571
|
+
notifyChange: () => notifyChange
|
|
572
|
+
});
|
|
573
|
+
function notifyChange() {
|
|
574
|
+
changeVersion++;
|
|
575
|
+
const waiters = pollWaiters;
|
|
576
|
+
pollWaiters = [];
|
|
577
|
+
for (const resolve5 of waiters) {
|
|
578
|
+
resolve5(changeVersion);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function getChangeVersion() {
|
|
582
|
+
return changeVersion;
|
|
583
|
+
}
|
|
584
|
+
function addPollWaiter(resolve5) {
|
|
585
|
+
pollWaiters.push(resolve5);
|
|
586
|
+
}
|
|
587
|
+
var changeVersion, pollWaiters;
|
|
588
|
+
var init_notify = __esm({
|
|
589
|
+
"src/routes/notify.ts"() {
|
|
590
|
+
"use strict";
|
|
591
|
+
changeVersion = 0;
|
|
592
|
+
pollWaiters = [];
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
444
596
|
// src/channel-config.ts
|
|
445
597
|
var channel_config_exports = {};
|
|
446
598
|
__export(channel_config_exports, {
|
|
@@ -788,6 +940,9 @@ async function deleteAttachment(id) {
|
|
|
788
940
|
return result.rows[0] ?? null;
|
|
789
941
|
}
|
|
790
942
|
|
|
943
|
+
// src/db/queries.ts
|
|
944
|
+
init_commandLog();
|
|
945
|
+
|
|
791
946
|
// src/db/notes.ts
|
|
792
947
|
init_connection();
|
|
793
948
|
var noteCounter = 0;
|
|
@@ -2404,12 +2559,12 @@ init_file_settings();
|
|
|
2404
2559
|
import { serve } from "@hono/node-server";
|
|
2405
2560
|
import { execFile } from "child_process";
|
|
2406
2561
|
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
2407
|
-
import { Hono as
|
|
2562
|
+
import { Hono as Hono11 } from "hono";
|
|
2408
2563
|
import { dirname as dirname3, join as join11 } from "path";
|
|
2409
2564
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2410
2565
|
|
|
2411
2566
|
// src/routes/api.ts
|
|
2412
|
-
import { Hono as
|
|
2567
|
+
import { Hono as Hono8 } from "hono";
|
|
2413
2568
|
|
|
2414
2569
|
// src/routes/attachments.ts
|
|
2415
2570
|
import { existsSync as existsSync5, mkdirSync as mkdirSync3, rmSync as rmSync5 } from "fs";
|
|
@@ -2652,25 +2807,8 @@ async function syncOpenTickets() {
|
|
|
2652
2807
|
}
|
|
2653
2808
|
}
|
|
2654
2809
|
|
|
2655
|
-
// src/routes/notify.ts
|
|
2656
|
-
var changeVersion = 0;
|
|
2657
|
-
var pollWaiters = [];
|
|
2658
|
-
function notifyChange() {
|
|
2659
|
-
changeVersion++;
|
|
2660
|
-
const waiters = pollWaiters;
|
|
2661
|
-
pollWaiters = [];
|
|
2662
|
-
for (const resolve5 of waiters) {
|
|
2663
|
-
resolve5(changeVersion);
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2666
|
-
function getChangeVersion() {
|
|
2667
|
-
return changeVersion;
|
|
2668
|
-
}
|
|
2669
|
-
function addPollWaiter(resolve5) {
|
|
2670
|
-
pollWaiters.push(resolve5);
|
|
2671
|
-
}
|
|
2672
|
-
|
|
2673
2810
|
// src/routes/attachments.ts
|
|
2811
|
+
init_notify();
|
|
2674
2812
|
var attachmentRoutes = new Hono();
|
|
2675
2813
|
attachmentRoutes.post("/tickets/:id/attachments", async (c) => {
|
|
2676
2814
|
const id = parseInt(c.req.param("id"), 10);
|
|
@@ -2758,8 +2896,10 @@ attachmentRoutes.get("/attachments/file/*", async (c) => {
|
|
|
2758
2896
|
|
|
2759
2897
|
// src/routes/channel.ts
|
|
2760
2898
|
import { Hono as Hono2 } from "hono";
|
|
2899
|
+
init_notify();
|
|
2761
2900
|
var channelRoutes = new Hono2();
|
|
2762
2901
|
var channelDoneFlag = false;
|
|
2902
|
+
var loggedPermissionRequests = /* @__PURE__ */ new Map();
|
|
2763
2903
|
channelRoutes.get("/channel/claude-check", async (c) => {
|
|
2764
2904
|
const { execFileSync } = await import("child_process");
|
|
2765
2905
|
try {
|
|
@@ -2790,7 +2930,11 @@ channelRoutes.post("/channel/trigger", async (c) => {
|
|
|
2790
2930
|
const serverPort = parseInt(new URL(c.req.url).port || "4174", 10);
|
|
2791
2931
|
const body = await c.req.json().catch(() => ({ message: void 0 }));
|
|
2792
2932
|
channelDoneFlag = false;
|
|
2933
|
+
loggedPermissionRequests.clear();
|
|
2793
2934
|
const ok = await triggerChannel2(dataDir2, serverPort, body.message);
|
|
2935
|
+
const summary = body.message !== void 0 && body.message !== "" ? body.message.slice(0, 200) : "Worklist trigger";
|
|
2936
|
+
addLogEntry("trigger", "outgoing", summary, body.message ?? "").catch(() => {
|
|
2937
|
+
});
|
|
2794
2938
|
return c.json({ ok });
|
|
2795
2939
|
});
|
|
2796
2940
|
channelRoutes.get("/channel/permission", async (c) => {
|
|
@@ -2801,6 +2945,20 @@ channelRoutes.get("/channel/permission", async (c) => {
|
|
|
2801
2945
|
try {
|
|
2802
2946
|
const res = await fetch(`http://127.0.0.1:${port2}/permission`);
|
|
2803
2947
|
const data = await res.json();
|
|
2948
|
+
if (data.pending !== null) {
|
|
2949
|
+
const reqId = data.pending.request_id ?? "";
|
|
2950
|
+
if (reqId !== "" && !loggedPermissionRequests.has(reqId)) {
|
|
2951
|
+
const toolName = data.pending.tool_name ?? "unknown tool";
|
|
2952
|
+
const description = data.pending.description ?? "";
|
|
2953
|
+
const inputPreview = data.pending.input_preview ?? (data.pending.tool_input !== void 0 ? JSON.stringify(data.pending.tool_input, null, 2).slice(0, 2e3) : "");
|
|
2954
|
+
const detail = (description !== "" ? description + "\n\n" : "") + inputPreview;
|
|
2955
|
+
addLogEntry("permission_request", "incoming", `Permission: ${toolName}`, detail).then((entry) => {
|
|
2956
|
+
loggedPermissionRequests.set(reqId, entry.id);
|
|
2957
|
+
}).catch(() => {
|
|
2958
|
+
loggedPermissionRequests.set(reqId, 0);
|
|
2959
|
+
});
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2804
2962
|
return c.json(data);
|
|
2805
2963
|
} catch {
|
|
2806
2964
|
return c.json({ pending: null });
|
|
@@ -2812,6 +2970,16 @@ channelRoutes.post("/channel/permission/respond", async (c) => {
|
|
|
2812
2970
|
const port2 = getChannelPort2(dataDir2);
|
|
2813
2971
|
if (port2 === null) return c.json({ error: "Channel not available" }, 503);
|
|
2814
2972
|
const body = await c.req.json();
|
|
2973
|
+
const action = body.behavior === "allow" ? "Allowed" : "Denied";
|
|
2974
|
+
const toolName = body.tool_name ?? "tool";
|
|
2975
|
+
const logId = loggedPermissionRequests.get(body.request_id);
|
|
2976
|
+
if (logId !== void 0 && logId > 0) {
|
|
2977
|
+
updateLogEntry(logId, { summary: `Permission: ${toolName} \u2014 ${action}` }).catch(() => {
|
|
2978
|
+
});
|
|
2979
|
+
} else {
|
|
2980
|
+
addLogEntry("permission_request", "incoming", `Permission: ${toolName} \u2014 ${action}`, JSON.stringify(body)).catch(() => {
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2815
2983
|
try {
|
|
2816
2984
|
const res = await fetch(`http://127.0.0.1:${port2}/permission/respond`, {
|
|
2817
2985
|
method: "POST",
|
|
@@ -2836,6 +3004,8 @@ channelRoutes.post("/channel/permission/dismiss", async (c) => {
|
|
|
2836
3004
|
});
|
|
2837
3005
|
channelRoutes.post("/channel/done", (_c) => {
|
|
2838
3006
|
channelDoneFlag = true;
|
|
3007
|
+
addLogEntry("done", "incoming", "Claude finished", "").catch(() => {
|
|
3008
|
+
});
|
|
2839
3009
|
notifyChange();
|
|
2840
3010
|
return _c.json({ ok: true });
|
|
2841
3011
|
});
|
|
@@ -2855,9 +3025,33 @@ channelRoutes.post("/channel/disable", async (c) => {
|
|
|
2855
3025
|
return c.json({ ok: true });
|
|
2856
3026
|
});
|
|
2857
3027
|
|
|
3028
|
+
// src/routes/commandLog.ts
|
|
3029
|
+
import { Hono as Hono3 } from "hono";
|
|
3030
|
+
var commandLogRoutes = new Hono3();
|
|
3031
|
+
commandLogRoutes.get("/command-log", async (c) => {
|
|
3032
|
+
const url = new URL(c.req.url);
|
|
3033
|
+
const limit = parseInt(url.searchParams.get("limit") ?? "100", 10);
|
|
3034
|
+
const offset = parseInt(url.searchParams.get("offset") ?? "0", 10);
|
|
3035
|
+
const eventType = url.searchParams.get("event_type") ?? void 0;
|
|
3036
|
+
const search = url.searchParams.get("search") ?? void 0;
|
|
3037
|
+
const entries = await getLogEntries({ limit, offset, eventType, search });
|
|
3038
|
+
return c.json(entries);
|
|
3039
|
+
});
|
|
3040
|
+
commandLogRoutes.delete("/command-log", async (c) => {
|
|
3041
|
+
await clearLog();
|
|
3042
|
+
return c.json({ ok: true });
|
|
3043
|
+
});
|
|
3044
|
+
commandLogRoutes.get("/command-log/count", async (c) => {
|
|
3045
|
+
const url = new URL(c.req.url);
|
|
3046
|
+
const eventType = url.searchParams.get("event_type") ?? void 0;
|
|
3047
|
+
const search = url.searchParams.get("search") ?? void 0;
|
|
3048
|
+
const count = await getLogCount({ eventType, search });
|
|
3049
|
+
return c.json({ count });
|
|
3050
|
+
});
|
|
3051
|
+
|
|
2858
3052
|
// src/routes/dashboard.ts
|
|
2859
3053
|
import { writeFileSync as writeFileSync7 } from "fs";
|
|
2860
|
-
import { Hono as
|
|
3054
|
+
import { Hono as Hono4 } from "hono";
|
|
2861
3055
|
import { tmpdir } from "os";
|
|
2862
3056
|
import { join as join10, relative as relative2 } from "path";
|
|
2863
3057
|
|
|
@@ -3126,7 +3320,8 @@ function consumeSkillsCreatedFlag() {
|
|
|
3126
3320
|
}
|
|
3127
3321
|
|
|
3128
3322
|
// src/routes/dashboard.ts
|
|
3129
|
-
|
|
3323
|
+
init_notify();
|
|
3324
|
+
var dashboardRoutes = new Hono4();
|
|
3130
3325
|
dashboardRoutes.get("/poll", async (c) => {
|
|
3131
3326
|
const clientVersion = parseInt(c.req.query("version") ?? "0", 10);
|
|
3132
3327
|
const changeVersion2 = getChangeVersion();
|
|
@@ -3216,8 +3411,9 @@ dashboardRoutes.post("/print", async (c) => {
|
|
|
3216
3411
|
});
|
|
3217
3412
|
|
|
3218
3413
|
// src/routes/settings.ts
|
|
3219
|
-
import { Hono as
|
|
3220
|
-
|
|
3414
|
+
import { Hono as Hono5 } from "hono";
|
|
3415
|
+
init_notify();
|
|
3416
|
+
var settingsRoutes = new Hono5();
|
|
3221
3417
|
settingsRoutes.get("/tags", async (c) => {
|
|
3222
3418
|
const tags = await getAllTags();
|
|
3223
3419
|
return c.json(tags);
|
|
@@ -3265,10 +3461,93 @@ settingsRoutes.patch("/file-settings", async (c) => {
|
|
|
3265
3461
|
return c.json(updated);
|
|
3266
3462
|
});
|
|
3267
3463
|
|
|
3464
|
+
// src/routes/shell.ts
|
|
3465
|
+
init_commandLog();
|
|
3466
|
+
import { Hono as Hono6 } from "hono";
|
|
3467
|
+
var shellRoutes = new Hono6();
|
|
3468
|
+
var runningProcesses = /* @__PURE__ */ new Map();
|
|
3469
|
+
var killedProcesses = /* @__PURE__ */ new Set();
|
|
3470
|
+
shellRoutes.post("/shell/exec", async (c) => {
|
|
3471
|
+
const { spawn } = await import("child_process");
|
|
3472
|
+
const dataDir2 = c.get("dataDir");
|
|
3473
|
+
const body = await c.req.json();
|
|
3474
|
+
const command = body.command;
|
|
3475
|
+
const name = body.name;
|
|
3476
|
+
if (!command || command.trim() === "") {
|
|
3477
|
+
return c.json({ error: "No command provided" }, 400);
|
|
3478
|
+
}
|
|
3479
|
+
const cwd = dataDir2 + "/..";
|
|
3480
|
+
const summary = name !== void 0 && name !== "" ? name : command.slice(0, 200);
|
|
3481
|
+
const logEntry = await addLogEntry("shell_command", "outgoing", summary, command);
|
|
3482
|
+
const logId = logEntry.id;
|
|
3483
|
+
const child = spawn(command, { shell: true, cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
3484
|
+
runningProcesses.set(logId, child);
|
|
3485
|
+
let stdout = "";
|
|
3486
|
+
let stderr = "";
|
|
3487
|
+
child.stdout.on("data", (data) => {
|
|
3488
|
+
stdout += data.toString();
|
|
3489
|
+
});
|
|
3490
|
+
child.stderr.on("data", (data) => {
|
|
3491
|
+
stderr += data.toString();
|
|
3492
|
+
});
|
|
3493
|
+
child.on("close", (code, signal) => {
|
|
3494
|
+
runningProcesses.delete(logId);
|
|
3495
|
+
const wasCanceled = killedProcesses.has(logId);
|
|
3496
|
+
killedProcesses.delete(logId);
|
|
3497
|
+
const output = (stdout + (stderr ? "\n--- stderr ---\n" + stderr : "")).trim();
|
|
3498
|
+
const exitSummary = wasCanceled ? "Canceled" : code === 0 ? "Completed (exit 0)" : signal !== null ? `Killed by ${signal}` : `Exited with code ${code ?? "unknown"}`;
|
|
3499
|
+
const combinedDetail = command + "\n---SHELL_OUTPUT---\n" + output;
|
|
3500
|
+
if (logId > 0) {
|
|
3501
|
+
const label = name !== void 0 && name !== "" ? name : command.slice(0, 100);
|
|
3502
|
+
updateLogEntry(logId, { summary: `${label} \u2014 ${exitSummary}`, detail: combinedDetail }).catch(() => {
|
|
3503
|
+
});
|
|
3504
|
+
} else {
|
|
3505
|
+
addLogEntry("shell_command", "outgoing", exitSummary, combinedDetail).catch(() => {
|
|
3506
|
+
});
|
|
3507
|
+
}
|
|
3508
|
+
Promise.resolve().then(() => (init_notify(), notify_exports)).then(({ notifyChange: notifyChange2 }) => notifyChange2()).catch(() => {
|
|
3509
|
+
});
|
|
3510
|
+
});
|
|
3511
|
+
child.on("error", (err) => {
|
|
3512
|
+
runningProcesses.delete(logId);
|
|
3513
|
+
const combinedDetail = command + "\n---SHELL_OUTPUT---\n" + (err.stack ?? err.message);
|
|
3514
|
+
if (logId > 0) {
|
|
3515
|
+
updateLogEntry(logId, { summary: `${command.slice(0, 100)} \u2014 Error: ${err.message}`, detail: combinedDetail }).catch(() => {
|
|
3516
|
+
});
|
|
3517
|
+
} else {
|
|
3518
|
+
addLogEntry("shell_command", "outgoing", `Error: ${err.message}`, combinedDetail).catch(() => {
|
|
3519
|
+
});
|
|
3520
|
+
}
|
|
3521
|
+
Promise.resolve().then(() => (init_notify(), notify_exports)).then(({ notifyChange: notifyChange2 }) => notifyChange2()).catch(() => {
|
|
3522
|
+
});
|
|
3523
|
+
});
|
|
3524
|
+
return c.json({ id: logId });
|
|
3525
|
+
});
|
|
3526
|
+
shellRoutes.post("/shell/kill", async (c) => {
|
|
3527
|
+
const body = await c.req.json();
|
|
3528
|
+
const child = runningProcesses.get(body.id);
|
|
3529
|
+
if (!child) {
|
|
3530
|
+
return c.json({ error: "Process not found or already finished" }, 404);
|
|
3531
|
+
}
|
|
3532
|
+
killedProcesses.add(body.id);
|
|
3533
|
+
child.kill("SIGTERM");
|
|
3534
|
+
setTimeout(() => {
|
|
3535
|
+
if (runningProcesses.has(body.id)) {
|
|
3536
|
+
child.kill("SIGKILL");
|
|
3537
|
+
}
|
|
3538
|
+
}, 3e3);
|
|
3539
|
+
return c.json({ ok: true });
|
|
3540
|
+
});
|
|
3541
|
+
shellRoutes.get("/shell/running", (c) => {
|
|
3542
|
+
const ids = Array.from(runningProcesses.keys());
|
|
3543
|
+
return c.json({ ids });
|
|
3544
|
+
});
|
|
3545
|
+
|
|
3268
3546
|
// src/routes/tickets.ts
|
|
3269
3547
|
import { rmSync as rmSync6 } from "fs";
|
|
3270
|
-
import { Hono as
|
|
3271
|
-
|
|
3548
|
+
import { Hono as Hono7 } from "hono";
|
|
3549
|
+
init_notify();
|
|
3550
|
+
var ticketRoutes = new Hono7();
|
|
3272
3551
|
ticketRoutes.get("/tickets", async (c) => {
|
|
3273
3552
|
const filters = {};
|
|
3274
3553
|
const category = c.req.query("category");
|
|
@@ -3455,16 +3734,18 @@ ticketRoutes.post("/tickets/query", async (c) => {
|
|
|
3455
3734
|
});
|
|
3456
3735
|
|
|
3457
3736
|
// src/routes/api.ts
|
|
3458
|
-
var apiRoutes = new
|
|
3737
|
+
var apiRoutes = new Hono8();
|
|
3459
3738
|
apiRoutes.route("/", ticketRoutes);
|
|
3460
3739
|
apiRoutes.route("/", attachmentRoutes);
|
|
3461
3740
|
apiRoutes.route("/", channelRoutes);
|
|
3741
|
+
apiRoutes.route("/", commandLogRoutes);
|
|
3462
3742
|
apiRoutes.route("/", settingsRoutes);
|
|
3463
3743
|
apiRoutes.route("/", dashboardRoutes);
|
|
3744
|
+
apiRoutes.route("/", shellRoutes);
|
|
3464
3745
|
|
|
3465
3746
|
// src/routes/backups.ts
|
|
3466
|
-
import { Hono as
|
|
3467
|
-
var backupRoutes = new
|
|
3747
|
+
import { Hono as Hono9 } from "hono";
|
|
3748
|
+
var backupRoutes = new Hono9();
|
|
3468
3749
|
backupRoutes.get("/", (c) => {
|
|
3469
3750
|
const dataDir2 = c.get("dataDir");
|
|
3470
3751
|
const backups = listBackups(dataDir2);
|
|
@@ -3513,7 +3794,7 @@ backupRoutes.post("/restore", async (c) => {
|
|
|
3513
3794
|
});
|
|
3514
3795
|
|
|
3515
3796
|
// src/routes/pages.tsx
|
|
3516
|
-
import { Hono as
|
|
3797
|
+
import { Hono as Hono10 } from "hono";
|
|
3517
3798
|
|
|
3518
3799
|
// src/utils/escapeHtml.ts
|
|
3519
3800
|
function escapeHtml(str) {
|
|
@@ -3708,7 +3989,7 @@ function Layout({ title, children }) {
|
|
|
3708
3989
|
}
|
|
3709
3990
|
|
|
3710
3991
|
// src/routes/pages.tsx
|
|
3711
|
-
var pageRoutes = new
|
|
3992
|
+
var pageRoutes = new Hono10();
|
|
3712
3993
|
pageRoutes.get("/", (c) => {
|
|
3713
3994
|
const html = /* @__PURE__ */ jsx(Layout, { title: "Hot Sheet", children: [
|
|
3714
3995
|
/* @__PURE__ */ jsx("div", { className: "app", children: [
|
|
@@ -4050,8 +4331,44 @@ pageRoutes.get("/", (c) => {
|
|
|
4050
4331
|
] }),
|
|
4051
4332
|
/* @__PURE__ */ jsx("div", { className: "status-bar-right", children: [
|
|
4052
4333
|
/* @__PURE__ */ jsx("div", { id: "status-bar", className: "status-bar" }),
|
|
4334
|
+
/* @__PURE__ */ jsx("button", { id: "command-log-btn", className: "command-log-btn", title: "Commands Log", children: [
|
|
4335
|
+
/* @__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: [
|
|
4336
|
+
/* @__PURE__ */ jsx("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
|
|
4337
|
+
/* @__PURE__ */ jsx("path", { d: "M3 15h18" }),
|
|
4338
|
+
/* @__PURE__ */ jsx("path", { d: "m9 10 3-3 3 3" })
|
|
4339
|
+
] }),
|
|
4340
|
+
/* @__PURE__ */ jsx("span", { id: "command-log-badge", className: "command-log-badge", style: "display:none" })
|
|
4341
|
+
] }),
|
|
4053
4342
|
/* @__PURE__ */ jsx("span", { id: "channel-status-indicator", className: "channel-status-indicator", style: "display:none" })
|
|
4054
4343
|
] })
|
|
4344
|
+
] }),
|
|
4345
|
+
/* @__PURE__ */ jsx("div", { id: "command-log-panel", className: "command-log-panel", style: "display:none", children: [
|
|
4346
|
+
/* @__PURE__ */ jsx("div", { className: "command-log-resize-handle", id: "command-log-resize" }),
|
|
4347
|
+
/* @__PURE__ */ jsx("div", { className: "command-log-header", children: [
|
|
4348
|
+
/* @__PURE__ */ jsx("span", { className: "command-log-title", children: "Commands Log" }),
|
|
4349
|
+
/* @__PURE__ */ jsx("div", { className: "command-log-search-box", children: [
|
|
4350
|
+
/* @__PURE__ */ jsx("svg", { className: "command-log-search-icon", xmlns: "http://www.w3.org/2000/svg", width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
4351
|
+
/* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "8" }),
|
|
4352
|
+
/* @__PURE__ */ jsx("path", { d: "m21 21-4.3-4.3" })
|
|
4353
|
+
] }),
|
|
4354
|
+
/* @__PURE__ */ jsx("input", { type: "text", id: "command-log-search", placeholder: "Search...", className: "command-log-search" })
|
|
4355
|
+
] }),
|
|
4356
|
+
/* @__PURE__ */ jsx("button", { id: "command-log-filter-btn", className: "command-log-filter-btn", title: "Filter by type", children: [
|
|
4357
|
+
/* @__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", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("polygon", { points: "22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" }) }),
|
|
4358
|
+
/* @__PURE__ */ jsx("span", { children: "All types" })
|
|
4359
|
+
] }),
|
|
4360
|
+
/* @__PURE__ */ jsx("button", { id: "command-log-clear", className: "command-log-clear-btn", title: "Clear log", 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: [
|
|
4361
|
+
/* @__PURE__ */ jsx("path", { d: "M3 6h18" }),
|
|
4362
|
+
/* @__PURE__ */ jsx("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
|
|
4363
|
+
/* @__PURE__ */ jsx("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" })
|
|
4364
|
+
] }) }),
|
|
4365
|
+
/* @__PURE__ */ jsx("button", { id: "command-log-close", className: "command-log-close-btn", title: "Close", 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: [
|
|
4366
|
+
/* @__PURE__ */ jsx("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
|
|
4367
|
+
/* @__PURE__ */ jsx("path", { d: "M3 15h18" }),
|
|
4368
|
+
/* @__PURE__ */ jsx("path", { d: "m15 8-3 3-3-3" })
|
|
4369
|
+
] }) })
|
|
4370
|
+
] }),
|
|
4371
|
+
/* @__PURE__ */ jsx("div", { id: "command-log-entries", className: "command-log-entries" })
|
|
4055
4372
|
] })
|
|
4056
4373
|
] }),
|
|
4057
4374
|
/* @__PURE__ */ jsx("div", { className: "settings-overlay", id: "settings-overlay", style: "display:none", children: /* @__PURE__ */ jsx("div", { className: "settings-dialog", children: [
|
|
@@ -4100,7 +4417,7 @@ pageRoutes.get("/", (c) => {
|
|
|
4100
4417
|
] }),
|
|
4101
4418
|
/* @__PURE__ */ jsx("span", { children: "Context" })
|
|
4102
4419
|
] }),
|
|
4103
|
-
/* @__PURE__ */ jsx("button", { className: "settings-tab", "data-tab": "experimental", id: "settings-tab-experimental",
|
|
4420
|
+
/* @__PURE__ */ jsx("button", { className: "settings-tab", "data-tab": "experimental", id: "settings-tab-experimental", children: [
|
|
4104
4421
|
/* @__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: [
|
|
4105
4422
|
/* @__PURE__ */ jsx("path", { d: "M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2" }),
|
|
4106
4423
|
/* @__PURE__ */ jsx("path", { d: "M8.5 2h7" }),
|
|
@@ -4187,7 +4504,7 @@ pageRoutes.get("/", (c) => {
|
|
|
4187
4504
|
/* @__PURE__ */ jsx("span", { className: "settings-hint", style: "margin-bottom:12px;display:block", children: "Automatically prepend instructions to ticket details in the worklist, based on category or tag. Category context appears first, then tag context in alphabetical order." }),
|
|
4188
4505
|
/* @__PURE__ */ jsx("div", { id: "auto-context-list" })
|
|
4189
4506
|
] }),
|
|
4190
|
-
/* @__PURE__ */ jsx("div", { className: "settings-tab-panel", "data-panel": "experimental", id: "settings-experimental-panel",
|
|
4507
|
+
/* @__PURE__ */ jsx("div", { className: "settings-tab-panel", "data-panel": "experimental", id: "settings-experimental-panel", children: [
|
|
4191
4508
|
/* @__PURE__ */ jsx("div", { className: "settings-field", children: [
|
|
4192
4509
|
/* @__PURE__ */ jsx("label", { className: "settings-checkbox-label", children: [
|
|
4193
4510
|
/* @__PURE__ */ jsx("input", { type: "checkbox", id: "settings-channel-enabled" }),
|
|
@@ -4246,7 +4563,7 @@ function tryServe(fetch2, port2) {
|
|
|
4246
4563
|
});
|
|
4247
4564
|
}
|
|
4248
4565
|
async function startServer(port2, dataDir2, options) {
|
|
4249
|
-
const app = new
|
|
4566
|
+
const app = new Hono11();
|
|
4250
4567
|
app.use("*", async (c, next) => {
|
|
4251
4568
|
c.set("dataDir", dataDir2);
|
|
4252
4569
|
await next();
|
|
@@ -4573,6 +4890,8 @@ async function main() {
|
|
|
4573
4890
|
AI tool skills created/updated for: ${updatedPlatforms.join(", ")}`);
|
|
4574
4891
|
console.log(" Restart your AI tool to pick up the new ticket creation skills.\n");
|
|
4575
4892
|
}
|
|
4893
|
+
Promise.resolve().then(() => (init_commandLog(), commandLog_exports)).then(({ pruneLog: pruneLog2 }) => pruneLog2(1e3)).catch(() => {
|
|
4894
|
+
});
|
|
4576
4895
|
Promise.resolve().then(() => (init_stats(), stats_exports)).then(async ({ recordDailySnapshot: recordDailySnapshot2, backfillSnapshots: backfillSnapshots2 }) => {
|
|
4577
4896
|
await backfillSnapshots2();
|
|
4578
4897
|
await recordDailySnapshot2();
|