bashbros 0.1.2 → 0.1.4
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 +727 -265
- package/dist/adapters-JAZGGNVP.js +9 -0
- package/dist/chunk-4XZ64P4V.js +47 -0
- package/dist/chunk-4XZ64P4V.js.map +1 -0
- package/dist/{chunk-XCZMQRSX.js → chunk-7OEWYFN3.js} +745 -541
- package/dist/chunk-7OEWYFN3.js.map +1 -0
- package/dist/{chunk-SQCP6IYB.js → chunk-CG6VEHJM.js} +3 -2
- package/dist/chunk-CG6VEHJM.js.map +1 -0
- package/dist/{chunk-DLP2O6PN.js → chunk-EMLEJVJZ.js} +102 -1
- package/dist/chunk-EMLEJVJZ.js.map +1 -0
- package/dist/chunk-IUUBCPMV.js +166 -0
- package/dist/chunk-IUUBCPMV.js.map +1 -0
- package/dist/chunk-J6ONXY6N.js +146 -0
- package/dist/chunk-J6ONXY6N.js.map +1 -0
- package/dist/chunk-KYDMPE4N.js +224 -0
- package/dist/chunk-KYDMPE4N.js.map +1 -0
- package/dist/chunk-LJE4EPIU.js +56 -0
- package/dist/chunk-LJE4EPIU.js.map +1 -0
- package/dist/chunk-LZYW7XQO.js +339 -0
- package/dist/chunk-LZYW7XQO.js.map +1 -0
- package/dist/{chunk-YUMNBQAY.js → chunk-RDNSS3ME.js} +587 -12
- package/dist/chunk-RDNSS3ME.js.map +1 -0
- package/dist/{chunk-BW6XCOJH.js → chunk-RTZ4QWG2.js} +2 -2
- package/dist/chunk-RTZ4QWG2.js.map +1 -0
- package/dist/chunk-SDN6TAGD.js +157 -0
- package/dist/chunk-SDN6TAGD.js.map +1 -0
- package/dist/chunk-T5ONCUHZ.js +198 -0
- package/dist/chunk-T5ONCUHZ.js.map +1 -0
- package/dist/cli.js +1182 -251
- package/dist/cli.js.map +1 -1
- package/dist/{config-JLLOTFLI.js → config-I5NCK3RJ.js} +2 -2
- package/dist/copilot-cli-5WJWK5YT.js +9 -0
- package/dist/{db-OBKEXRTP.js → db-ETWTBXAE.js} +2 -2
- package/dist/db-checks-2YOVECD4.js +133 -0
- package/dist/db-checks-2YOVECD4.js.map +1 -0
- package/dist/{display-6LZ2HBCU.js → display-UH7KEHOW.js} +3 -3
- package/dist/display-UH7KEHOW.js.map +1 -0
- package/dist/gemini-cli-3563EELZ.js +9 -0
- package/dist/gemini-cli-3563EELZ.js.map +1 -0
- package/dist/index.d.ts +195 -72
- package/dist/index.js +119 -398
- package/dist/index.js.map +1 -1
- package/dist/{ollama-HY35OHW4.js → ollama-5JVKNFOV.js} +2 -2
- package/dist/ollama-5JVKNFOV.js.map +1 -0
- package/dist/opencode-DRCY275R.js +9 -0
- package/dist/opencode-DRCY275R.js.map +1 -0
- package/dist/profiles-7CLN6TAT.js +9 -0
- package/dist/profiles-7CLN6TAT.js.map +1 -0
- package/dist/setup-YS27MOPE.js +124 -0
- package/dist/setup-YS27MOPE.js.map +1 -0
- package/dist/static/index.html +4815 -2007
- package/dist/store-WJ5Y7MOE.js +9 -0
- package/dist/store-WJ5Y7MOE.js.map +1 -0
- package/dist/writer-3NAVABN6.js +12 -0
- package/dist/writer-3NAVABN6.js.map +1 -0
- package/package.json +77 -68
- package/dist/chunk-BW6XCOJH.js.map +0 -1
- package/dist/chunk-DLP2O6PN.js.map +0 -1
- package/dist/chunk-SQCP6IYB.js.map +0 -1
- package/dist/chunk-XCZMQRSX.js.map +0 -1
- package/dist/chunk-YUMNBQAY.js.map +0 -1
- /package/dist/{config-JLLOTFLI.js.map → adapters-JAZGGNVP.js.map} +0 -0
- /package/dist/{db-OBKEXRTP.js.map → config-I5NCK3RJ.js.map} +0 -0
- /package/dist/{display-6LZ2HBCU.js.map → copilot-cli-5WJWK5YT.js.map} +0 -0
- /package/dist/{ollama-HY35OHW4.js.map → db-ETWTBXAE.js.map} +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/dashboard/db.ts
|
|
4
4
|
import Database from "better-sqlite3";
|
|
5
5
|
import { randomUUID } from "crypto";
|
|
6
|
-
var DashboardDB = class {
|
|
6
|
+
var DashboardDB = class _DashboardDB {
|
|
7
7
|
db;
|
|
8
8
|
constructor(dbPath = ".bashbros.db") {
|
|
9
9
|
this.db = new Database(dbPath);
|
|
@@ -85,7 +85,7 @@ var DashboardDB = class {
|
|
|
85
85
|
this.db.exec(`
|
|
86
86
|
CREATE TABLE IF NOT EXISTS commands (
|
|
87
87
|
id TEXT PRIMARY KEY,
|
|
88
|
-
session_id TEXT
|
|
88
|
+
session_id TEXT,
|
|
89
89
|
timestamp TEXT NOT NULL,
|
|
90
90
|
command TEXT NOT NULL,
|
|
91
91
|
allowed INTEGER NOT NULL,
|
|
@@ -97,6 +97,7 @@ var DashboardDB = class {
|
|
|
97
97
|
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
98
98
|
)
|
|
99
99
|
`);
|
|
100
|
+
this.migrateCommandsNullableSessionId();
|
|
100
101
|
this.db.exec(`
|
|
101
102
|
CREATE TABLE IF NOT EXISTS bro_events (
|
|
102
103
|
id TEXT PRIMARY KEY,
|
|
@@ -122,6 +123,34 @@ var DashboardDB = class {
|
|
|
122
123
|
project_type TEXT
|
|
123
124
|
)
|
|
124
125
|
`);
|
|
126
|
+
this.db.exec(`
|
|
127
|
+
CREATE TABLE IF NOT EXISTS tool_uses (
|
|
128
|
+
id TEXT PRIMARY KEY,
|
|
129
|
+
timestamp TEXT NOT NULL,
|
|
130
|
+
tool_name TEXT NOT NULL,
|
|
131
|
+
tool_input TEXT NOT NULL,
|
|
132
|
+
tool_output TEXT NOT NULL,
|
|
133
|
+
exit_code INTEGER,
|
|
134
|
+
success INTEGER,
|
|
135
|
+
cwd TEXT NOT NULL,
|
|
136
|
+
repo_name TEXT,
|
|
137
|
+
repo_path TEXT
|
|
138
|
+
)
|
|
139
|
+
`);
|
|
140
|
+
this.db.exec(`
|
|
141
|
+
CREATE TABLE IF NOT EXISTS adapter_events (
|
|
142
|
+
id TEXT PRIMARY KEY,
|
|
143
|
+
timestamp TEXT NOT NULL,
|
|
144
|
+
adapter_name TEXT NOT NULL,
|
|
145
|
+
base_model TEXT NOT NULL,
|
|
146
|
+
purpose TEXT NOT NULL,
|
|
147
|
+
action TEXT NOT NULL,
|
|
148
|
+
success INTEGER NOT NULL
|
|
149
|
+
)
|
|
150
|
+
`);
|
|
151
|
+
this.migrateToolUsesAddSessionId();
|
|
152
|
+
this.migrateSessionsAddMode();
|
|
153
|
+
this.migrateSessionsAddRepoName();
|
|
125
154
|
this.db.exec(`
|
|
126
155
|
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
127
156
|
CREATE INDEX IF NOT EXISTS idx_sessions_start_time ON sessions(start_time);
|
|
@@ -131,6 +160,10 @@ var DashboardDB = class {
|
|
|
131
160
|
CREATE INDEX IF NOT EXISTS idx_bro_events_session_id ON bro_events(session_id);
|
|
132
161
|
CREATE INDEX IF NOT EXISTS idx_bro_events_timestamp ON bro_events(timestamp);
|
|
133
162
|
CREATE INDEX IF NOT EXISTS idx_bro_status_timestamp ON bro_status(timestamp);
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_tool_uses_timestamp ON tool_uses(timestamp);
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_tool_uses_tool_name ON tool_uses(tool_name);
|
|
165
|
+
CREATE INDEX IF NOT EXISTS idx_tool_uses_session_id ON tool_uses(session_id);
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_adapter_events_timestamp ON adapter_events(timestamp);
|
|
134
167
|
`);
|
|
135
168
|
}
|
|
136
169
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -361,10 +394,10 @@ var DashboardDB = class {
|
|
|
361
394
|
const id = randomUUID();
|
|
362
395
|
const startTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
363
396
|
const stmt = this.db.prepare(`
|
|
364
|
-
INSERT INTO sessions (id, agent, pid, start_time, status, command_count, blocked_count, avg_risk_score, working_dir)
|
|
365
|
-
VALUES (?, ?, ?, ?, 'active', 0, 0, 0, ?)
|
|
397
|
+
INSERT INTO sessions (id, agent, pid, start_time, status, command_count, blocked_count, avg_risk_score, working_dir, repo_name)
|
|
398
|
+
VALUES (?, ?, ?, ?, 'active', 0, 0, 0, ?, ?)
|
|
366
399
|
`);
|
|
367
|
-
stmt.run(id, input.agent, input.pid, startTime, input.workingDir);
|
|
400
|
+
stmt.run(id, input.agent, input.pid, startTime, input.workingDir, input.repoName ?? null);
|
|
368
401
|
return id;
|
|
369
402
|
}
|
|
370
403
|
updateSession(id, updates) {
|
|
@@ -443,6 +476,44 @@ var DashboardDB = class {
|
|
|
443
476
|
if (!row) return null;
|
|
444
477
|
return this.rowToSession(row);
|
|
445
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* Insert a session with an externally-provided ID (for hook-mode sessions).
|
|
481
|
+
* Uses INSERT OR IGNORE for race-safe concurrent hook calls.
|
|
482
|
+
*/
|
|
483
|
+
insertSessionWithId(id, input) {
|
|
484
|
+
const startTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
485
|
+
const stmt = this.db.prepare(`
|
|
486
|
+
INSERT OR IGNORE INTO sessions (id, agent, pid, start_time, status, command_count, blocked_count, avg_risk_score, working_dir, mode, repo_name)
|
|
487
|
+
VALUES (?, ?, ?, ?, 'active', 0, 0, 0, ?, ?, ?)
|
|
488
|
+
`);
|
|
489
|
+
stmt.run(id, input.agent, input.pid, startTime, input.workingDir, input.mode ?? "hook", input.repoName ?? null);
|
|
490
|
+
return id;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Get ALL active sessions, ordered by start_time DESC.
|
|
494
|
+
* Used by the multi-session dashboard.
|
|
495
|
+
*/
|
|
496
|
+
getActiveSessions() {
|
|
497
|
+
const stmt = this.db.prepare(`
|
|
498
|
+
SELECT * FROM sessions WHERE status = 'active' ORDER BY start_time DESC
|
|
499
|
+
`);
|
|
500
|
+
const rows = stmt.all();
|
|
501
|
+
return rows.map((row) => this.rowToSession(row));
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Atomically increment session counters in a single UPDATE.
|
|
505
|
+
* Race-safe for concurrent hook processes (SQLite serializes writes).
|
|
506
|
+
*/
|
|
507
|
+
incrementSessionCommand(id, blocked, riskScore) {
|
|
508
|
+
const stmt = this.db.prepare(`
|
|
509
|
+
UPDATE sessions SET
|
|
510
|
+
command_count = command_count + 1,
|
|
511
|
+
blocked_count = blocked_count + ?,
|
|
512
|
+
avg_risk_score = (avg_risk_score * command_count + ?) / (command_count + 1)
|
|
513
|
+
WHERE id = ?
|
|
514
|
+
`);
|
|
515
|
+
stmt.run(blocked ? 1 : 0, riskScore, id);
|
|
516
|
+
}
|
|
446
517
|
rowToSession(row) {
|
|
447
518
|
return {
|
|
448
519
|
id: row.id,
|
|
@@ -454,7 +525,8 @@ var DashboardDB = class {
|
|
|
454
525
|
commandCount: row.command_count,
|
|
455
526
|
blockedCount: row.blocked_count,
|
|
456
527
|
avgRiskScore: row.avg_risk_score,
|
|
457
|
-
workingDir: row.working_dir
|
|
528
|
+
workingDir: row.working_dir,
|
|
529
|
+
repoName: row.repo_name ?? null
|
|
458
530
|
};
|
|
459
531
|
}
|
|
460
532
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -469,7 +541,7 @@ var DashboardDB = class {
|
|
|
469
541
|
`);
|
|
470
542
|
stmt.run(
|
|
471
543
|
id,
|
|
472
|
-
input.sessionId,
|
|
544
|
+
input.sessionId ?? null,
|
|
473
545
|
timestamp,
|
|
474
546
|
input.command,
|
|
475
547
|
input.allowed ? 1 : 0,
|
|
@@ -522,12 +594,17 @@ var DashboardDB = class {
|
|
|
522
594
|
}
|
|
523
595
|
getLiveCommands(limit = 20) {
|
|
524
596
|
const stmt = this.db.prepare(`
|
|
525
|
-
SELECT
|
|
526
|
-
|
|
597
|
+
SELECT c.*, s.repo_name, s.working_dir
|
|
598
|
+
FROM commands c
|
|
599
|
+
LEFT JOIN sessions s ON c.session_id = s.id
|
|
600
|
+
ORDER BY c.timestamp DESC
|
|
527
601
|
LIMIT ?
|
|
528
602
|
`);
|
|
529
603
|
const rows = stmt.all(limit);
|
|
530
|
-
return rows.map((row) =>
|
|
604
|
+
return rows.map((row) => ({
|
|
605
|
+
...this.rowToCommand(row),
|
|
606
|
+
repoName: row.repo_name ?? (row.working_dir ? row.working_dir.split(/[/\\]/).pop() ?? null : null)
|
|
607
|
+
}));
|
|
531
608
|
}
|
|
532
609
|
rowToCommand(row) {
|
|
533
610
|
return {
|
|
@@ -637,6 +714,132 @@ var DashboardDB = class {
|
|
|
637
714
|
};
|
|
638
715
|
}
|
|
639
716
|
// ─────────────────────────────────────────────────────────────
|
|
717
|
+
// Adapter Events
|
|
718
|
+
// ─────────────────────────────────────────────────────────────
|
|
719
|
+
insertAdapterEvent(input) {
|
|
720
|
+
const id = randomUUID();
|
|
721
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
722
|
+
const stmt = this.db.prepare(`
|
|
723
|
+
INSERT INTO adapter_events (id, timestamp, adapter_name, base_model, purpose, action, success)
|
|
724
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
725
|
+
`);
|
|
726
|
+
stmt.run(
|
|
727
|
+
id,
|
|
728
|
+
timestamp,
|
|
729
|
+
input.adapterName,
|
|
730
|
+
input.baseModel,
|
|
731
|
+
input.purpose,
|
|
732
|
+
input.action,
|
|
733
|
+
input.success ? 1 : 0
|
|
734
|
+
);
|
|
735
|
+
return id;
|
|
736
|
+
}
|
|
737
|
+
getAdapterEvents(limit = 50) {
|
|
738
|
+
const stmt = this.db.prepare(`
|
|
739
|
+
SELECT * FROM adapter_events
|
|
740
|
+
ORDER BY timestamp DESC
|
|
741
|
+
LIMIT ?
|
|
742
|
+
`);
|
|
743
|
+
const rows = stmt.all(limit);
|
|
744
|
+
return rows.map((row) => ({
|
|
745
|
+
id: row.id,
|
|
746
|
+
timestamp: new Date(row.timestamp),
|
|
747
|
+
adapterName: row.adapter_name,
|
|
748
|
+
baseModel: row.base_model,
|
|
749
|
+
purpose: row.purpose,
|
|
750
|
+
action: row.action,
|
|
751
|
+
success: row.success === 1
|
|
752
|
+
}));
|
|
753
|
+
}
|
|
754
|
+
// ─────────────────────────────────────────────────────────────
|
|
755
|
+
// Tool Uses
|
|
756
|
+
// ─────────────────────────────────────────────────────────────
|
|
757
|
+
insertToolUse(input) {
|
|
758
|
+
const id = randomUUID();
|
|
759
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
760
|
+
const stmt = this.db.prepare(`
|
|
761
|
+
INSERT INTO tool_uses (id, timestamp, tool_name, tool_input, tool_output, exit_code, success, cwd, repo_name, repo_path, session_id)
|
|
762
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
763
|
+
`);
|
|
764
|
+
stmt.run(
|
|
765
|
+
id,
|
|
766
|
+
timestamp,
|
|
767
|
+
input.toolName,
|
|
768
|
+
input.toolInput.substring(0, 5e4),
|
|
769
|
+
// Truncate very long inputs
|
|
770
|
+
input.toolOutput.substring(0, 5e4),
|
|
771
|
+
// Truncate very long outputs
|
|
772
|
+
input.exitCode ?? null,
|
|
773
|
+
input.success === void 0 ? null : input.success ? 1 : 0,
|
|
774
|
+
input.cwd,
|
|
775
|
+
input.repoName ?? null,
|
|
776
|
+
input.repoPath ?? null,
|
|
777
|
+
input.sessionId ?? null
|
|
778
|
+
);
|
|
779
|
+
return id;
|
|
780
|
+
}
|
|
781
|
+
getToolUses(filter = {}) {
|
|
782
|
+
const conditions = [];
|
|
783
|
+
const params = [];
|
|
784
|
+
if (filter.toolName) {
|
|
785
|
+
conditions.push("tool_name = ?");
|
|
786
|
+
params.push(filter.toolName);
|
|
787
|
+
}
|
|
788
|
+
if (filter.sessionId) {
|
|
789
|
+
conditions.push("session_id = ?");
|
|
790
|
+
params.push(filter.sessionId);
|
|
791
|
+
}
|
|
792
|
+
if (filter.since) {
|
|
793
|
+
conditions.push("timestamp >= ?");
|
|
794
|
+
params.push(filter.since.toISOString());
|
|
795
|
+
}
|
|
796
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
797
|
+
const limit = filter.limit ?? 100;
|
|
798
|
+
const offset = filter.offset ?? 0;
|
|
799
|
+
const stmt = this.db.prepare(`
|
|
800
|
+
SELECT * FROM tool_uses
|
|
801
|
+
${whereClause}
|
|
802
|
+
ORDER BY timestamp DESC
|
|
803
|
+
LIMIT ? OFFSET ?
|
|
804
|
+
`);
|
|
805
|
+
params.push(limit, offset);
|
|
806
|
+
const rows = stmt.all(...params);
|
|
807
|
+
return rows.map((row) => ({
|
|
808
|
+
id: row.id,
|
|
809
|
+
timestamp: new Date(row.timestamp),
|
|
810
|
+
toolName: row.tool_name,
|
|
811
|
+
toolInput: row.tool_input,
|
|
812
|
+
toolOutput: row.tool_output,
|
|
813
|
+
exitCode: row.exit_code,
|
|
814
|
+
success: row.success === null ? null : row.success === 1,
|
|
815
|
+
cwd: row.cwd,
|
|
816
|
+
repoName: row.repo_name,
|
|
817
|
+
repoPath: row.repo_path
|
|
818
|
+
}));
|
|
819
|
+
}
|
|
820
|
+
getLiveToolUses(limit = 50) {
|
|
821
|
+
return this.getToolUses({ limit });
|
|
822
|
+
}
|
|
823
|
+
getToolUseStats() {
|
|
824
|
+
const totalRow = this.db.prepare("SELECT COUNT(*) as count FROM tool_uses").get();
|
|
825
|
+
const toolRows = this.db.prepare(`
|
|
826
|
+
SELECT tool_name, COUNT(*) as count FROM tool_uses GROUP BY tool_name ORDER BY count DESC
|
|
827
|
+
`).all();
|
|
828
|
+
const byTool = {};
|
|
829
|
+
for (const row of toolRows) {
|
|
830
|
+
byTool[row.tool_name] = row.count;
|
|
831
|
+
}
|
|
832
|
+
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
|
|
833
|
+
const last24hRow = this.db.prepare(`
|
|
834
|
+
SELECT COUNT(*) as count FROM tool_uses WHERE timestamp >= ?
|
|
835
|
+
`).get(oneDayAgo);
|
|
836
|
+
return {
|
|
837
|
+
totalUses: totalRow.count,
|
|
838
|
+
byTool,
|
|
839
|
+
last24h: last24hRow.count
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
// ─────────────────────────────────────────────────────────────
|
|
640
843
|
// Session Metrics
|
|
641
844
|
// ─────────────────────────────────────────────────────────────
|
|
642
845
|
getSessionMetrics(sessionId) {
|
|
@@ -673,6 +876,61 @@ var DashboardDB = class {
|
|
|
673
876
|
};
|
|
674
877
|
}
|
|
675
878
|
// ─────────────────────────────────────────────────────────────
|
|
879
|
+
// Security Summary
|
|
880
|
+
// ─────────────────────────────────────────────────────────────
|
|
881
|
+
getSecuritySummary() {
|
|
882
|
+
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
|
|
883
|
+
const totalRow = this.db.prepare(`
|
|
884
|
+
SELECT COUNT(*) as count FROM commands WHERE timestamp >= ?
|
|
885
|
+
`).get(oneDayAgo);
|
|
886
|
+
const blockedRow = this.db.prepare(`
|
|
887
|
+
SELECT COUNT(*) as count FROM commands WHERE timestamp >= ? AND allowed = 0
|
|
888
|
+
`).get(oneDayAgo);
|
|
889
|
+
const avgRow = this.db.prepare(`
|
|
890
|
+
SELECT AVG(risk_score) as avg FROM commands WHERE timestamp >= ?
|
|
891
|
+
`).get(oneDayAgo);
|
|
892
|
+
const riskRows = this.db.prepare(`
|
|
893
|
+
SELECT risk_level, COUNT(*) as count FROM commands WHERE timestamp >= ? GROUP BY risk_level
|
|
894
|
+
`).all(oneDayAgo);
|
|
895
|
+
const riskDistribution = {};
|
|
896
|
+
for (const row of riskRows) {
|
|
897
|
+
riskDistribution[row.risk_level] = row.count;
|
|
898
|
+
}
|
|
899
|
+
const highRiskRow = this.db.prepare(`
|
|
900
|
+
SELECT COUNT(*) as count FROM commands WHERE timestamp >= ? AND risk_level IN ('dangerous', 'critical')
|
|
901
|
+
`).get(oneDayAgo);
|
|
902
|
+
const violationRows = this.db.prepare(`
|
|
903
|
+
SELECT violations FROM commands WHERE timestamp >= ? AND allowed = 0 AND violations != '[]'
|
|
904
|
+
`).all(oneDayAgo);
|
|
905
|
+
const typeCounts = {};
|
|
906
|
+
for (const row of violationRows) {
|
|
907
|
+
try {
|
|
908
|
+
const violations = JSON.parse(row.violations);
|
|
909
|
+
for (const v of violations) {
|
|
910
|
+
const type = v.includes(":") ? v.split(":")[0] : v;
|
|
911
|
+
typeCounts[type] = (typeCounts[type] || 0) + 1;
|
|
912
|
+
}
|
|
913
|
+
} catch {
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const violationTypes = Object.entries(typeCounts).map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count);
|
|
917
|
+
return {
|
|
918
|
+
totalCommands24h: totalRow.count,
|
|
919
|
+
blockedCount24h: blockedRow.count,
|
|
920
|
+
avgRiskScore24h: avgRow.avg ?? 0,
|
|
921
|
+
riskDistribution,
|
|
922
|
+
violationTypes,
|
|
923
|
+
highRiskCount24h: highRiskRow.count
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
getBlockedCommandsRecent(limit = 25) {
|
|
927
|
+
const stmt = this.db.prepare(`
|
|
928
|
+
SELECT * FROM commands WHERE allowed = 0 ORDER BY timestamp DESC LIMIT ?
|
|
929
|
+
`);
|
|
930
|
+
const rows = stmt.all(limit);
|
|
931
|
+
return rows.map((row) => this.rowToCommand(row));
|
|
932
|
+
}
|
|
933
|
+
// ─────────────────────────────────────────────────────────────
|
|
676
934
|
// Stats
|
|
677
935
|
// ─────────────────────────────────────────────────────────────
|
|
678
936
|
getStats() {
|
|
@@ -735,6 +993,27 @@ var DashboardDB = class {
|
|
|
735
993
|
};
|
|
736
994
|
}
|
|
737
995
|
// ─────────────────────────────────────────────────────────────
|
|
996
|
+
// Cross-process Query Helpers (for db-checks)
|
|
997
|
+
// ─────────────────────────────────────────────────────────────
|
|
998
|
+
getRecentCommandTexts(windowSize) {
|
|
999
|
+
const stmt = this.db.prepare(`
|
|
1000
|
+
SELECT command, timestamp FROM commands
|
|
1001
|
+
ORDER BY timestamp DESC
|
|
1002
|
+
LIMIT ?
|
|
1003
|
+
`);
|
|
1004
|
+
return stmt.all(windowSize);
|
|
1005
|
+
}
|
|
1006
|
+
getCommandCountSince(sinceISO) {
|
|
1007
|
+
const row = this.db.prepare(`
|
|
1008
|
+
SELECT COUNT(*) as count FROM commands WHERE timestamp >= ?
|
|
1009
|
+
`).get(sinceISO);
|
|
1010
|
+
return row.count;
|
|
1011
|
+
}
|
|
1012
|
+
getTotalCommandCount() {
|
|
1013
|
+
const row = this.db.prepare("SELECT COUNT(*) as count FROM commands").get();
|
|
1014
|
+
return row.count;
|
|
1015
|
+
}
|
|
1016
|
+
// ─────────────────────────────────────────────────────────────
|
|
738
1017
|
// Maintenance
|
|
739
1018
|
// ─────────────────────────────────────────────────────────────
|
|
740
1019
|
cleanup(olderThanDays = 30) {
|
|
@@ -751,16 +1030,312 @@ var DashboardDB = class {
|
|
|
751
1030
|
`).run(cutoff).changes;
|
|
752
1031
|
const broEventsDeleted = this.db.prepare("DELETE FROM bro_events WHERE timestamp < ?").run(cutoff).changes;
|
|
753
1032
|
const broStatusDeleted = this.db.prepare("DELETE FROM bro_status WHERE timestamp < ?").run(cutoff).changes;
|
|
754
|
-
|
|
1033
|
+
const toolUsesDeleted = this.db.prepare("DELETE FROM tool_uses WHERE timestamp < ?").run(cutoff).changes;
|
|
1034
|
+
return eventsDeleted + connectorDeleted + blocksDeleted + exposuresDeleted + commandsDeleted + sessionsDeleted + broEventsDeleted + broStatusDeleted + toolUsesDeleted;
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Migration: add session_id column to tool_uses table for session tracking
|
|
1038
|
+
*/
|
|
1039
|
+
migrateToolUsesAddSessionId() {
|
|
1040
|
+
try {
|
|
1041
|
+
const tableInfo = this.db.pragma("table_info(tool_uses)");
|
|
1042
|
+
const hasSessionId = tableInfo.some((col) => col.name === "session_id");
|
|
1043
|
+
if (!hasSessionId) {
|
|
1044
|
+
this.db.exec("ALTER TABLE tool_uses ADD COLUMN session_id TEXT");
|
|
1045
|
+
}
|
|
1046
|
+
} catch {
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Migration: add mode column to sessions table to distinguish watch vs hook sessions
|
|
1051
|
+
*/
|
|
1052
|
+
migrateSessionsAddMode() {
|
|
1053
|
+
try {
|
|
1054
|
+
const tableInfo = this.db.pragma("table_info(sessions)");
|
|
1055
|
+
const hasMode = tableInfo.some((col) => col.name === "mode");
|
|
1056
|
+
if (!hasMode) {
|
|
1057
|
+
this.db.exec("ALTER TABLE sessions ADD COLUMN mode TEXT DEFAULT 'watch'");
|
|
1058
|
+
}
|
|
1059
|
+
} catch {
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Migration: add repo_name column to sessions table
|
|
1064
|
+
*/
|
|
1065
|
+
migrateSessionsAddRepoName() {
|
|
1066
|
+
try {
|
|
1067
|
+
const tableInfo = this.db.pragma("table_info(sessions)");
|
|
1068
|
+
const hasRepoName = tableInfo.some((col) => col.name === "repo_name");
|
|
1069
|
+
if (!hasRepoName) {
|
|
1070
|
+
this.db.exec("ALTER TABLE sessions ADD COLUMN repo_name TEXT");
|
|
1071
|
+
}
|
|
1072
|
+
} catch {
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Migration: allow NULL session_id in commands table for hook-mode recording
|
|
1077
|
+
* (each hook invocation is a separate process with no long-running session)
|
|
1078
|
+
*/
|
|
1079
|
+
migrateCommandsNullableSessionId() {
|
|
1080
|
+
try {
|
|
1081
|
+
const tableInfo = this.db.pragma("table_info(commands)");
|
|
1082
|
+
const sessionCol = tableInfo.find((col) => col.name === "session_id");
|
|
1083
|
+
if (sessionCol && sessionCol.notnull === 1) {
|
|
1084
|
+
this.db.exec(`
|
|
1085
|
+
CREATE TABLE commands_mig (
|
|
1086
|
+
id TEXT PRIMARY KEY,
|
|
1087
|
+
session_id TEXT,
|
|
1088
|
+
timestamp TEXT NOT NULL,
|
|
1089
|
+
command TEXT NOT NULL,
|
|
1090
|
+
allowed INTEGER NOT NULL,
|
|
1091
|
+
risk_score INTEGER NOT NULL,
|
|
1092
|
+
risk_level TEXT NOT NULL,
|
|
1093
|
+
risk_factors TEXT NOT NULL,
|
|
1094
|
+
duration_ms INTEGER NOT NULL,
|
|
1095
|
+
violations TEXT NOT NULL,
|
|
1096
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
1097
|
+
);
|
|
1098
|
+
INSERT INTO commands_mig SELECT * FROM commands;
|
|
1099
|
+
DROP TABLE commands;
|
|
1100
|
+
ALTER TABLE commands_mig RENAME TO commands;
|
|
1101
|
+
CREATE INDEX IF NOT EXISTS idx_commands_session_id ON commands(session_id);
|
|
1102
|
+
CREATE INDEX IF NOT EXISTS idx_commands_timestamp ON commands(timestamp);
|
|
1103
|
+
CREATE INDEX IF NOT EXISTS idx_commands_allowed ON commands(allowed);
|
|
1104
|
+
`);
|
|
1105
|
+
}
|
|
1106
|
+
} catch {
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
// ─────────────────────────────────────────────────────────────
|
|
1110
|
+
// Achievement System
|
|
1111
|
+
// ─────────────────────────────────────────────────────────────
|
|
1112
|
+
static BADGE_DEFINITIONS = [
|
|
1113
|
+
// Volume
|
|
1114
|
+
{ id: "first_blood", name: "First Blood", description: "Execute commands", category: "volume", icon: "\u2694", stat: "totalCommands", tiers: [1, 100, 1e3, 1e4, 1e5] },
|
|
1115
|
+
{ id: "marathon_runner", name: "Marathon Runner", description: "Complete sessions", category: "volume", icon: "\u{1F3C3}", stat: "totalSessions", tiers: [1, 10, 50, 200, 1e3] },
|
|
1116
|
+
{ id: "watchdog", name: "Watchdog", description: "Time under watch", category: "volume", icon: "\u23F1", stat: "totalWatchTimeMinutes", tiers: [60, 1440, 10080, 43200, 525600] },
|
|
1117
|
+
// Security
|
|
1118
|
+
{ id: "shield_bearer", name: "Shield Bearer", description: "Threats blocked", category: "security", icon: "\u{1F6E1}", stat: "totalBlocked", tiers: [1, 25, 100, 500, 2e3] },
|
|
1119
|
+
{ id: "clean_hands", name: "Clean Hands", description: "Consecutive clean commands", category: "security", icon: "\u2728", stat: "cleanestStreak", tiers: [10, 50, 200, 1e3, 5e3] },
|
|
1120
|
+
{ id: "risk_taker", name: "Risk Taker", description: "High risk commands executed", category: "security", icon: "\u{1F3B2}", stat: "highRiskCount", tiers: [1, 5, 10, 25, 50] },
|
|
1121
|
+
// Agents
|
|
1122
|
+
{ id: "buddy_system", name: "Buddy System", description: "Use 2+ agents", category: "agents", icon: "\u{1F91D}", stat: "uniqueAgents", tiers: [2, 2, 2, 2, 2] },
|
|
1123
|
+
{ id: "squad_up", name: "Squad Up", description: "Use multiple agents", category: "agents", icon: "\u{1F46B}", stat: "uniqueAgents", tiers: [2, 3, 4, 5, 5] },
|
|
1124
|
+
{ id: "loyal", name: "Loyal", description: "1000 commands from one agent", category: "agents", icon: "\u{1F451}", stat: "maxAgentCommands", tiers: [100, 250, 500, 1e3, 5e3] },
|
|
1125
|
+
{ id: "polyglot", name: "Polyglot", description: "100+ commands from multiple agents", category: "agents", icon: "\u{1F30D}", stat: "agentsWith100", tiers: [1, 2, 3, 4, 5] },
|
|
1126
|
+
// Behavioral
|
|
1127
|
+
{ id: "night_owl", name: "Night Owl", description: "Commands after midnight", category: "behavioral", icon: "\u{1F989}", stat: "lateNightCount", tiers: [10, 100, 500, 2e3, 1e4] },
|
|
1128
|
+
{ id: "speed_demon", name: "Speed Demon", description: "Commands in a single hour", category: "behavioral", icon: "\u26A1", stat: "peakHourCount", tiers: [60, 100, 150, 200, 300] },
|
|
1129
|
+
{ id: "one_liner", name: "One-Liner", description: "Longest command (chars)", category: "behavioral", icon: "\u{1F4DD}", stat: "longestCommandLength", tiers: [500, 1e3, 2e3, 5e3, 1e4] },
|
|
1130
|
+
{ id: "creature_of_habit", name: "Creature of Habit", description: "Same command repeated", category: "behavioral", icon: "\u{1F501}", stat: "mostUsedCommandCount", tiers: [50, 200, 500, 1e3, 5e3] },
|
|
1131
|
+
{ id: "explorer", name: "Explorer", description: "Unique commands used", category: "behavioral", icon: "\u{1F9ED}", stat: "uniqueCommands", tiers: [25, 50, 100, 250, 500] },
|
|
1132
|
+
// Repo
|
|
1133
|
+
{ id: "home_base", name: "Home Base", description: "Protect a repo", category: "repo", icon: "\u{1F3E0}", stat: "uniqueRepos", tiers: [1, 1, 1, 1, 1] },
|
|
1134
|
+
{ id: "empire", name: "Empire", description: "Protect multiple repos", category: "repo", icon: "\u{1F3F0}", stat: "uniqueRepos", tiers: [3, 5, 10, 25, 50] }
|
|
1135
|
+
];
|
|
1136
|
+
getAchievementStats() {
|
|
1137
|
+
const totalCommands = this.db.prepare("SELECT COUNT(*) as c FROM commands").get().c;
|
|
1138
|
+
const totalBlocked = this.db.prepare("SELECT COUNT(*) as c FROM commands WHERE allowed = 0").get().c;
|
|
1139
|
+
const totalSessions = this.db.prepare("SELECT COUNT(*) as c FROM sessions").get().c;
|
|
1140
|
+
const totalCharacters = this.db.prepare("SELECT COALESCE(SUM(LENGTH(command)), 0) as c FROM commands").get().c;
|
|
1141
|
+
const watchTimeRow = this.db.prepare(`
|
|
1142
|
+
SELECT COALESCE(SUM(
|
|
1143
|
+
(julianday(COALESCE(end_time, datetime('now'))) - julianday(start_time)) * 1440
|
|
1144
|
+
), 0) as minutes FROM sessions
|
|
1145
|
+
`).get();
|
|
1146
|
+
const totalWatchTimeMinutes = Math.round(watchTimeRow.minutes);
|
|
1147
|
+
const uniqueRepos = this.db.prepare(`
|
|
1148
|
+
SELECT COUNT(DISTINCT repo_name) as c FROM sessions WHERE repo_name IS NOT NULL AND repo_name != ''
|
|
1149
|
+
`).get().c;
|
|
1150
|
+
const firstCommandRow = this.db.prepare("SELECT MIN(timestamp) as t FROM commands").get();
|
|
1151
|
+
const memberSince = firstCommandRow.t || null;
|
|
1152
|
+
const agentRows = this.db.prepare(`
|
|
1153
|
+
SELECT s.agent, COUNT(*) as count
|
|
1154
|
+
FROM commands c
|
|
1155
|
+
JOIN sessions s ON c.session_id = s.id
|
|
1156
|
+
GROUP BY s.agent
|
|
1157
|
+
ORDER BY count DESC
|
|
1158
|
+
`).all();
|
|
1159
|
+
const agentBreakdown = {};
|
|
1160
|
+
for (const row of agentRows) {
|
|
1161
|
+
agentBreakdown[row.agent] = row.count;
|
|
1162
|
+
}
|
|
1163
|
+
const uniqueAgents = Object.keys(agentBreakdown).length;
|
|
1164
|
+
const maxAgentCommands = agentRows.length > 0 ? agentRows[0].count : 0;
|
|
1165
|
+
const favoriteAgent = agentRows.length > 0 ? agentRows[0].agent : null;
|
|
1166
|
+
const agentsWith100 = agentRows.filter((r) => r.count >= 100).length;
|
|
1167
|
+
const mostUsedRow = this.db.prepare(`
|
|
1168
|
+
SELECT command, COUNT(*) as count FROM commands
|
|
1169
|
+
GROUP BY command ORDER BY count DESC LIMIT 1
|
|
1170
|
+
`).get();
|
|
1171
|
+
const mostUsedCommand = mostUsedRow?.command || null;
|
|
1172
|
+
const mostUsedCommandCount = mostUsedRow?.count || 0;
|
|
1173
|
+
const uniqueCommands = this.db.prepare("SELECT COUNT(DISTINCT command) as c FROM commands").get().c;
|
|
1174
|
+
const longestCommandLength = this.db.prepare("SELECT COALESCE(MAX(LENGTH(command)), 0) as c FROM commands").get().c;
|
|
1175
|
+
const peakHourRow = this.db.prepare(`
|
|
1176
|
+
SELECT CAST(strftime('%H', timestamp) AS INTEGER) as hour, COUNT(*) as count
|
|
1177
|
+
FROM commands GROUP BY hour ORDER BY count DESC LIMIT 1
|
|
1178
|
+
`).get();
|
|
1179
|
+
const peakHour = peakHourRow?.hour ?? null;
|
|
1180
|
+
const peakHourCount = peakHourRow?.count ?? 0;
|
|
1181
|
+
const peakDayRow = this.db.prepare(`
|
|
1182
|
+
SELECT CAST(strftime('%w', timestamp) AS INTEGER) as day, COUNT(*) as count
|
|
1183
|
+
FROM commands GROUP BY day ORDER BY count DESC LIMIT 1
|
|
1184
|
+
`).get();
|
|
1185
|
+
const peakDay = peakDayRow?.day ?? null;
|
|
1186
|
+
const busiestDayRow = this.db.prepare(`
|
|
1187
|
+
SELECT DATE(timestamp) as day, COUNT(*) as count
|
|
1188
|
+
FROM commands GROUP BY day ORDER BY count DESC LIMIT 1
|
|
1189
|
+
`).get();
|
|
1190
|
+
const busiestDay = busiestDayRow?.day || null;
|
|
1191
|
+
const busiestDayCount = busiestDayRow?.count || 0;
|
|
1192
|
+
const avgCommandsPerSession = totalSessions > 0 ? Math.round(totalCommands / totalSessions) : 0;
|
|
1193
|
+
const longestSessionRow = this.db.prepare(`
|
|
1194
|
+
SELECT MAX(
|
|
1195
|
+
(julianday(COALESCE(end_time, datetime('now'))) - julianday(start_time)) * 1440
|
|
1196
|
+
) as minutes FROM sessions
|
|
1197
|
+
`).get();
|
|
1198
|
+
const longestSessionMinutes = Math.round(longestSessionRow.minutes || 0);
|
|
1199
|
+
const lateNightCount = this.db.prepare(`
|
|
1200
|
+
SELECT COUNT(*) as c FROM commands
|
|
1201
|
+
WHERE CAST(strftime('%H', timestamp) AS INTEGER) < 5
|
|
1202
|
+
`).get().c;
|
|
1203
|
+
const avgRiskRow = this.db.prepare("SELECT AVG(risk_score) as avg FROM commands").get();
|
|
1204
|
+
const lifetimeAvgRisk = avgRiskRow.avg ?? 0;
|
|
1205
|
+
const allAllowed = this.db.prepare(
|
|
1206
|
+
"SELECT allowed FROM commands ORDER BY timestamp ASC"
|
|
1207
|
+
).all();
|
|
1208
|
+
let cleanestStreak = 0;
|
|
1209
|
+
let currentStreak = 0;
|
|
1210
|
+
for (const row of allAllowed) {
|
|
1211
|
+
if (row.allowed === 1) {
|
|
1212
|
+
currentStreak++;
|
|
1213
|
+
if (currentStreak > cleanestStreak) cleanestStreak = currentStreak;
|
|
1214
|
+
} else {
|
|
1215
|
+
currentStreak = 0;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
const highestRiskRow = this.db.prepare(
|
|
1219
|
+
"SELECT command, risk_score FROM commands ORDER BY risk_score DESC LIMIT 1"
|
|
1220
|
+
).get();
|
|
1221
|
+
const highestRiskCommand = highestRiskRow?.command || null;
|
|
1222
|
+
const highestRiskScore = highestRiskRow?.risk_score || 0;
|
|
1223
|
+
const highRiskCount = this.db.prepare(
|
|
1224
|
+
"SELECT COUNT(*) as c FROM commands WHERE risk_score >= 8"
|
|
1225
|
+
).get().c;
|
|
1226
|
+
return {
|
|
1227
|
+
totalCommands,
|
|
1228
|
+
totalBlocked,
|
|
1229
|
+
totalSessions,
|
|
1230
|
+
totalCharacters,
|
|
1231
|
+
totalWatchTimeMinutes,
|
|
1232
|
+
uniqueRepos,
|
|
1233
|
+
memberSince,
|
|
1234
|
+
agentBreakdown,
|
|
1235
|
+
uniqueAgents,
|
|
1236
|
+
maxAgentCommands,
|
|
1237
|
+
favoriteAgent,
|
|
1238
|
+
agentsWith100,
|
|
1239
|
+
mostUsedCommand,
|
|
1240
|
+
mostUsedCommandCount,
|
|
1241
|
+
uniqueCommands,
|
|
1242
|
+
longestCommandLength,
|
|
1243
|
+
peakHour,
|
|
1244
|
+
peakHourCount,
|
|
1245
|
+
peakDay,
|
|
1246
|
+
busiestDay,
|
|
1247
|
+
busiestDayCount,
|
|
1248
|
+
avgCommandsPerSession,
|
|
1249
|
+
longestSessionMinutes,
|
|
1250
|
+
lateNightCount,
|
|
1251
|
+
lifetimeAvgRisk,
|
|
1252
|
+
cleanestStreak,
|
|
1253
|
+
currentCleanStreak: currentStreak,
|
|
1254
|
+
highestRiskCommand,
|
|
1255
|
+
highestRiskScore,
|
|
1256
|
+
highRiskCount
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
computeAchievements(stats) {
|
|
1260
|
+
return _DashboardDB.BADGE_DEFINITIONS.map((badge) => {
|
|
1261
|
+
const value = stats[badge.stat] ?? 0;
|
|
1262
|
+
let currentTier = 0;
|
|
1263
|
+
for (let i = 0; i < badge.tiers.length; i++) {
|
|
1264
|
+
if (value >= badge.tiers[i]) currentTier = i + 1;
|
|
1265
|
+
}
|
|
1266
|
+
const nextThreshold = currentTier < badge.tiers.length ? badge.tiers[currentTier] : badge.tiers[badge.tiers.length - 1];
|
|
1267
|
+
const prevThreshold = currentTier > 0 ? badge.tiers[currentTier - 1] : 0;
|
|
1268
|
+
const progress = currentTier >= badge.tiers.length ? 1 : (value - prevThreshold) / Math.max(1, nextThreshold - prevThreshold);
|
|
1269
|
+
return {
|
|
1270
|
+
id: badge.id,
|
|
1271
|
+
name: badge.name,
|
|
1272
|
+
description: badge.description,
|
|
1273
|
+
category: badge.category,
|
|
1274
|
+
icon: badge.icon,
|
|
1275
|
+
tier: currentTier,
|
|
1276
|
+
tierName: TIER_NAMES[currentTier],
|
|
1277
|
+
value,
|
|
1278
|
+
nextThreshold,
|
|
1279
|
+
progress: Math.min(1, Math.max(0, progress)),
|
|
1280
|
+
maxed: currentTier >= 5
|
|
1281
|
+
};
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
computeXP(stats, badges) {
|
|
1285
|
+
const TIER_XP = [0, 50, 100, 200, 500, 1e3];
|
|
1286
|
+
const RANK_THRESHOLDS = [
|
|
1287
|
+
{ rank: "Obsidian", xp: 1e5 },
|
|
1288
|
+
{ rank: "Diamond", xp: 25e3 },
|
|
1289
|
+
{ rank: "Gold", xp: 5e3 },
|
|
1290
|
+
{ rank: "Silver", xp: 1e3 },
|
|
1291
|
+
{ rank: "Bronze", xp: 0 }
|
|
1292
|
+
];
|
|
1293
|
+
let totalXP = 0;
|
|
1294
|
+
totalXP += stats.totalCommands;
|
|
1295
|
+
totalXP += stats.totalBlocked * 3;
|
|
1296
|
+
totalXP += stats.totalSessions * 10;
|
|
1297
|
+
totalXP += stats.lateNightCount * 2;
|
|
1298
|
+
totalXP += Math.floor(stats.cleanestStreak / 100) * 25;
|
|
1299
|
+
for (const badge of badges) {
|
|
1300
|
+
if (badge.tier > 0) {
|
|
1301
|
+
totalXP += TIER_XP[badge.tier];
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
let rank = "Bronze";
|
|
1305
|
+
let nextRankXP = 1e3;
|
|
1306
|
+
for (const t of RANK_THRESHOLDS) {
|
|
1307
|
+
if (totalXP >= t.xp) {
|
|
1308
|
+
rank = t.rank;
|
|
1309
|
+
const idx = RANK_THRESHOLDS.indexOf(t);
|
|
1310
|
+
nextRankXP = idx > 0 ? RANK_THRESHOLDS[idx - 1].xp : totalXP;
|
|
1311
|
+
break;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
const currentRankXP = RANK_THRESHOLDS.find((t) => t.rank === rank).xp;
|
|
1315
|
+
const progress = rank === "Obsidian" ? 1 : (totalXP - currentRankXP) / Math.max(1, nextRankXP - currentRankXP);
|
|
1316
|
+
return {
|
|
1317
|
+
totalXP,
|
|
1318
|
+
rank,
|
|
1319
|
+
nextRankXP,
|
|
1320
|
+
progress: Math.min(1, Math.max(0, progress))
|
|
1321
|
+
};
|
|
755
1322
|
}
|
|
756
1323
|
close() {
|
|
757
1324
|
this.db.close();
|
|
758
1325
|
}
|
|
759
1326
|
};
|
|
1327
|
+
var TIER_NAMES = {
|
|
1328
|
+
0: "Locked",
|
|
1329
|
+
1: "Bronze",
|
|
1330
|
+
2: "Silver",
|
|
1331
|
+
3: "Gold",
|
|
1332
|
+
4: "Diamond",
|
|
1333
|
+
5: "Obsidian"
|
|
1334
|
+
};
|
|
760
1335
|
var db_default = DashboardDB;
|
|
761
1336
|
|
|
762
1337
|
export {
|
|
763
1338
|
DashboardDB,
|
|
764
1339
|
db_default
|
|
765
1340
|
};
|
|
766
|
-
//# sourceMappingURL=chunk-
|
|
1341
|
+
//# sourceMappingURL=chunk-RDNSS3ME.js.map
|