happenin 0.3.0 → 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/README.md +9 -2
- package/assets/help.md +1 -1
- package/dist/bin.js +439 -121
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](docs/CODE_OF_CONDUCT.md)
|
|
6
6
|
[](http://makeapullrequest.com)
|
|
7
7
|
|
|
8
|
-
A
|
|
8
|
+
A CLI that records Cursor and Claude Code agent events to a local SQLite database and serves a live browser dashboard.
|
|
9
9
|
|
|
10
10
|

|
|
11
11
|
|
|
@@ -29,7 +29,7 @@ Cursor and Claude Code can emit local hooks for each session, tool use, prompt,
|
|
|
29
29
|
|
|
30
30
|
## Requirements
|
|
31
31
|
|
|
32
|
-
- macOS
|
|
32
|
+
- macOS, Linux, or Windows
|
|
33
33
|
- Node.js `>= 22.13.0` (uses the built-in `node:sqlite` module, available since Node 22.13)
|
|
34
34
|
- Zero runtime dependencies
|
|
35
35
|
- The dashboard loads htmx and htmx-ext-sse from a CDN
|
|
@@ -142,6 +142,13 @@ Clicking a tool, skill, or file jumps to a list of every session that used it. T
|
|
|
142
142
|
chart can be scoped to one directory with the "md files directory" filter (the common path prefix
|
|
143
143
|
is stripped from the dropdown labels, and paths under `/private` are excluded).
|
|
144
144
|
|
|
145
|
+
The persistent filter bar also has multi-select "directories", "skills", and "integrations (MCP)"
|
|
146
|
+
controls. Each lets you pick several values at once — matching sessions with _any_ of the selected
|
|
147
|
+
values in that category (OR) — and the categories combine with every other filter, including each
|
|
148
|
+
other, with AND (e.g. directory X or Y, AND skill A or B). The "integrations (MCP)" list is derived
|
|
149
|
+
from the data itself: it parses the `mcp__<server>__<tool>` naming convention on recorded tool
|
|
150
|
+
calls and lists every distinct MCP server actually seen, with no hardcoded set of servers.
|
|
151
|
+
|
|
145
152
|
A context breakdown widget splits recorded payload size (and, for imported Claude sessions, token
|
|
146
153
|
usage) into four buckets: MCP server calls, markdown file reads, bloatware (skill loads and
|
|
147
154
|
lifecycle/hook noise), and actual value (real tool calls and conversation turns). It appears on the
|
package/assets/help.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# happenin
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A CLI that records Cursor and Claude Code agent events to a local SQLite database and serves a live browser dashboard.
|
|
4
4
|
|
|
5
5
|
## Commands
|
|
6
6
|
|
package/dist/bin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameSync as i,statSync as a,writeFileSync as o}from"node:fs";import s from"node:path";import c from"node:process";import{fileURLToPath as l}from"node:url";import{homedir as u}from"node:os";import{DatabaseSync as d}from"node:sqlite";import f from"node:http";import{execFile as p}from"node:child_process";const m=[`sessionStart`,`sessionEnd`,`beforeSubmitPrompt`,`preToolUse`,`postToolUse`,`postToolUseFailure`,`subagentStart`,`subagentStop`,`beforeShellExecution`,`afterShellExecution`,`beforeMCPExecution`,`afterMCPExecution`,`beforeReadFile`,`afterFileEdit`,`afterAgentResponse`,`afterAgentThought`,`preCompact`,`stop`],h=`SessionStart.SessionEnd.Setup.UserPromptSubmit.UserPromptExpansion.PreToolUse.PermissionRequest.PermissionDenied.PostToolUse.PostToolUseFailure.PostToolBatch.Notification.MessageDisplay.SubagentStart.SubagentStop.TaskCreated.TaskCompleted.Stop.StopFailure.TeammateIdle.InstructionsLoaded.ConfigChange.CwdChanged.DirectoryAdded.FileChanged.WorktreeCreate.WorktreeRemove.PreCompact.PostCompact.Elicitation.ElicitationResult`.split(`.`),g=new Set([`beforeSubmitPrompt`]),_=new Set([`preToolUse`,`beforeShellExecution`,`beforeMCPExecution`,`beforeReadFile`,`subagentStart`]),v=new Set([`UserPromptSubmit`,`UserPromptExpansion`]),y=new Set([`PreToolUse`,`PermissionRequest`,`SubagentStart`,`SubagentStop`,`TaskCreated`,`TaskCompleted`,`Stop`,`WorktreeCreate`,`WorktreeRemove`]),
|
|
3
|
-
`,`utf8`),i(n,e)},
|
|
4
|
-
`),t&&c.stdout.write(` backup: ${t}\n`)}async function
|
|
2
|
+
import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameSync as i,statSync as a,writeFileSync as o}from"node:fs";import s from"node:path";import c from"node:process";import{fileURLToPath as l}from"node:url";import{homedir as u}from"node:os";import{DatabaseSync as d}from"node:sqlite";import f from"node:http";import{execFile as p}from"node:child_process";const m=[`sessionStart`,`sessionEnd`,`beforeSubmitPrompt`,`preToolUse`,`postToolUse`,`postToolUseFailure`,`subagentStart`,`subagentStop`,`beforeShellExecution`,`afterShellExecution`,`beforeMCPExecution`,`afterMCPExecution`,`beforeReadFile`,`afterFileEdit`,`afterAgentResponse`,`afterAgentThought`,`preCompact`,`stop`],h=`SessionStart.SessionEnd.Setup.UserPromptSubmit.UserPromptExpansion.PreToolUse.PermissionRequest.PermissionDenied.PostToolUse.PostToolUseFailure.PostToolBatch.Notification.MessageDisplay.SubagentStart.SubagentStop.TaskCreated.TaskCompleted.Stop.StopFailure.TeammateIdle.InstructionsLoaded.ConfigChange.CwdChanged.DirectoryAdded.FileChanged.WorktreeCreate.WorktreeRemove.PreCompact.PostCompact.Elicitation.ElicitationResult`.split(`.`),g=new Set([`beforeSubmitPrompt`]),_=new Set([`preToolUse`,`beforeShellExecution`,`beforeMCPExecution`,`beforeReadFile`,`subagentStart`]),v=new Set([`UserPromptSubmit`,`UserPromptExpansion`]),y=new Set([`PreToolUse`,`PermissionRequest`,`SubagentStart`,`SubagentStop`,`TaskCreated`,`TaskCompleted`,`Stop`,`WorktreeCreate`,`WorktreeRemove`]),b=(e,t)=>{if(e===`cursor`)return g.has(t)?JSON.stringify({continue:!0}):_.has(t)?JSON.stringify({permission:`allow`}):void 0;if(e===`claude`)return v.has(t)?JSON.stringify({continue:!0}):y.has(t)?JSON.stringify({decision:`approve`}):void 0},x=()=>c.env.HOME||u(),S=e=>typeof e==`object`&&!!e&&!Array.isArray(e),ee=e=>e instanceof Error?e.message:String(e),te=()=>{let e=c.argv[1];if(e&&s.isAbsolute(e)){if(e.split(s.sep).includes(`_npx`))return`npx -y happenin`;if(c.platform!==`win32`)return e}return`happenin`},ne=(e,t)=>e.includes(t),re=e=>{t(e,{recursive:!0})},C=t=>e(t),ie=e=>{let t=n(e,`utf8`);return JSON.parse(t)},ae=(e,t)=>{let n=`${e}.${Date.now()}.tmp`;re(s.dirname(e)),o(n,JSON.stringify(t,null,2)+`
|
|
3
|
+
`,`utf8`),i(n,e)},oe=(e,t)=>{if(re(t),!C(e))return;let r=s.join(t,`${s.basename(e)}.${Date.now()}.bak`);return o(r,n(e,`utf8`),`utf8`),r},se=e=>{if(!C(e))return{version:1,hooks:{}};let t=ie(e);if(!S(t))throw Error(`Invalid JSON object in ${e}`);let n=S(t.hooks)?t.hooks:{};return{...t,version:typeof t.version==`number`?t.version:1,hooks:n}},ce=e=>{if(!C(e))return{hooks:{}};let t=ie(e);if(!S(t))throw Error(`Invalid JSON object in ${e}`);let n=S(t.hooks)?t.hooks:{};return{...t,hooks:n}},le=(e,t,n)=>{e[t]=[...(Array.isArray(e[t])?e[t]:[]).filter(e=>!S(e)||typeof e.command!=`string`||!ne(e.command,`happenin record cursor`)),{command:`${n} record cursor`}]},ue=(e,t,n)=>{let r=`happenin record claude ${t}`;e[t]=[...(Array.isArray(e[t])?e[t]:[]).map(e=>S(e)&&Array.isArray(e.hooks)?{...e,hooks:e.hooks.filter(e=>!S(e)||typeof e.command!=`string`||!ne(e.command,r))}:e).filter(e=>!(S(e)&&Array.isArray(e.hooks)&&e.hooks.length===0)),{matcher:``,hooks:[{type:`command`,command:`${n} record claude ${t}`}]}]},de=e=>{let t=x(),n=s.join(t,`.cursor`,`hooks.json`),r=s.join(t,`.happenin`,`backups`,`cursor`),i=oe(n,r),a=se(n);for(let t of m)le(a.hooks,t,e);return ae(n,a),{target:n,backup:i}},fe=e=>{let t=x(),n=s.join(t,`.claude`,`settings.json`),r=s.join(t,`.happenin`,`backups`,`claude`),i=oe(n,r),a=ce(n);for(let t of h)ue(a.hooks,t,e);return ae(n,a),{target:n,backup:i}},pe=e=>{let t=!1,n=!1;for(let r of e)r===`--cursor`&&(t=!0),r===`--claude`&&(n=!0);return!t&&!n&&(t=!0,n=!0),{cursor:t,claude:n}};async function me(e={}){let{cursor:t=!0,claude:n=!0}=e,r=te(),i=[];t&&i.push(de(r)),n&&i.push(fe(r));for(let{target:e,backup:t}of i)c.stdout.write(`${e} written
|
|
4
|
+
`),t&&c.stdout.write(` backup: ${t}\n`)}async function he(e=[]){try{let{cursor:t,claude:n}=pe(e);await me({cursor:t,claude:n})}catch(e){c.stderr.write(`install failed: ${ee(e)}\n`),c.exitCode=1}}const ge=[`CREATE INDEX IF NOT EXISTS idx_events_source ON events(source);`,`CREATE INDEX IF NOT EXISTS idx_events_event ON events(event);`,`CREATE INDEX IF NOT EXISTS idx_events_session_id ON events(session_id);`,`CREATE INDEX IF NOT EXISTS idx_events_happened_at ON events(happened_at);`,`CREATE INDEX IF NOT EXISTS idx_events_received_at ON events(received_at);`,`CREATE INDEX IF NOT EXISTS idx_events_source_path ON events(source_path);`,`CREATE INDEX IF NOT EXISTS idx_events_subagent_id ON events(subagent_id);`];function w(e){let t=e.prepare(`PRAGMA user_version`).get();return Number(t?.user_version??0)}function _e(e){let t=w(e);if(!(t>=1)){if(e.exec(`BEGIN IMMEDIATE;`),t=w(e),t>=1){e.exec(`ROLLBACK;`);return}try{let t=e.prepare(`PRAGMA table_info(events)`).all(),n=new Set(t.map(e=>e.name)),r=t=>{n.has(t)||e.exec(`ALTER TABLE events ADD COLUMN ${t} TEXT;`)};r(`subagent_id`),r(`subagent_type`),r(`transcript_path`),e.exec(`PRAGMA user_version = 1;`),e.exec(`COMMIT;`)}catch(t){try{e.exec(`ROLLBACK;`)}catch{}throw t}}}function ve(e){let t=w(e);if(!(t>=3)){if(e.exec(`BEGIN IMMEDIATE;`),t=w(e),t>=3){e.exec(`ROLLBACK;`);return}try{e.prepare(`PRAGMA table_info(events)`).all().some(e=>e.name===`skill_name`)||e.exec(`ALTER TABLE events ADD COLUMN skill_name TEXT;`),e.exec(`PRAGMA user_version = 3;`),e.exec(`COMMIT;`)}catch(t){try{e.exec(`ROLLBACK;`)}catch{}throw t}}}const ye=e=>{let t=w(e);if(!(t>=2)){if(e.exec(`BEGIN IMMEDIATE;`),t=w(e),t>=2){e.exec(`ROLLBACK;`);return}try{let t=e.prepare(`SELECT id, payload FROM events WHERE happened_at IS NULL AND json_valid(payload) = 1`).all(),n=e.prepare(`UPDATE events SET happened_at = ? WHERE id = ?`);for(let e of t){let t=Ce(JSON.parse(e.payload));t!==void 0&&n.run(t,e.id)}e.exec(`
|
|
5
5
|
UPDATE events
|
|
6
6
|
SET happened_at = strftime('%Y-%m-%dT%H:%M:%fZ', received_at / 1000.0, 'unixepoch')
|
|
7
7
|
WHERE happened_at IS NULL;
|
|
@@ -24,7 +24,7 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
24
24
|
WHERE
|
|
25
25
|
project_path IS NULL
|
|
26
26
|
AND json_valid(payload) = 1;
|
|
27
|
-
`),e.exec(`PRAGMA user_version = 2;`),e.exec(`COMMIT;`)}catch(t){try{e.exec(`ROLLBACK;`)}catch{}throw t}}},
|
|
27
|
+
`),e.exec(`PRAGMA user_version = 2;`),e.exec(`COMMIT;`)}catch(t){try{e.exec(`ROLLBACK;`)}catch{}throw t}}},be=e=>{let t=w(e);if(t<1)throw Error(`Database schema must be migrated before backfilling subagent metadata`);if(e.exec(`BEGIN IMMEDIATE;`),t=w(e),t<1){e.exec(`ROLLBACK;`);return}try{e.exec(`
|
|
28
28
|
UPDATE events
|
|
29
29
|
SET
|
|
30
30
|
subagent_id = NULLIF(trim(json_extract(payload, '$.subagent_id')), ''),
|
|
@@ -64,7 +64,7 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
64
64
|
WHERE s.event = 'subagentStart'
|
|
65
65
|
AND s.subagent_id = json_extract(events.payload, '$.tool_use_id')
|
|
66
66
|
);
|
|
67
|
-
`),e.exec(`COMMIT;`)}catch(t){try{e.exec(`ROLLBACK;`)}catch{}throw t}},
|
|
67
|
+
`),e.exec(`COMMIT;`)}catch(t){try{e.exec(`ROLLBACK;`)}catch{}throw t}},T=()=>c.env.HAPPENIN_DB?c.env.HAPPENIN_DB:s.join(x(),`.happenin`,`happenin.db`),E=(e,n=5e3)=>{let r=e??T();t(s.dirname(r),{recursive:!0});let i=new d(r);try{i.exec(`PRAGMA busy_timeout = ${n};`),i.exec(`PRAGMA journal_mode = WAL;`),i.exec(`CREATE TABLE IF NOT EXISTS events (
|
|
68
68
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
69
69
|
source TEXT NOT NULL,
|
|
70
70
|
client TEXT,
|
|
@@ -87,7 +87,7 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
87
87
|
mtime INTEGER NOT NULL,
|
|
88
88
|
imported_at INTEGER NOT NULL
|
|
89
89
|
);
|
|
90
|
-
`),
|
|
90
|
+
`),_e(i),ye(i),ve(i);for(let e of ge)i.exec(e);return i}catch(e){try{i.close()}catch{}throw e}},xe=[`happenedAt`,`timestamp`,`happened_at`,`time`,`ts`,`createdAt`,`created_at`];function Se(e){if(e!=null){if(typeof e==`number`){if(!Number.isFinite(e))return;try{return new Date(e).toISOString()}catch{return}}if(typeof e==`string`){let t=e.trim();if(t.length===0)return;if(/^\d+$/.test(t)){let e=Number(t);if(!Number.isFinite(e))return;try{return new Date(e).toISOString()}catch{return}}return Number.isNaN(Date.parse(t))?void 0:t}}}function Ce(e){if(typeof e!=`object`||!e||Array.isArray(e))return;let t=e;for(let e of xe){let n=Se(Reflect.get(t,e));if(n!==void 0)return n}}const D=(e,t)=>{let n=e.prepare(`
|
|
91
91
|
INSERT INTO events (
|
|
92
92
|
source,
|
|
93
93
|
client,
|
|
@@ -105,7 +105,7 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
105
105
|
subagent_type,
|
|
106
106
|
transcript_path
|
|
107
107
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
108
|
-
`),r=Date.now(),i=
|
|
108
|
+
`),r=Date.now(),i=Se(t.happenedAt)??new Date(r).toISOString();n.run(t.source,t.client??null,t.event??null,t.sessionId??null,i,r,t.projectPath??null,t.filePath??null,t.toolName??null,t.skillName??null,t.payload,t.sourcePath??null,t.subagentId??null,t.subagentType??null,t.transcriptPath??null)},O=`COALESCE(strftime('%Y-%m-%dT%H:%M:%fZ', happened_at), strftime('%Y-%m-%dT%H:%M:%fZ', received_at / 1000.0, 'unixepoch'))`;function we(e){return(e===`7d`?168:e===`30d`?720:24)*60*60*1e3}function Te(e){return e.replace(/\\/g,`\\\\`).replace(/%/g,`\\%`).replace(/_/g,`\\_`)}function Ee(e){return`mcp\\_\\_${Te(e)}\\_\\_%`}function k(e,t=Date.now()){let n=[],r=[];if(e.since!==void 0&&(n.push(`id > ?`),r.push(e.since)),e.source!==void 0&&(n.push(`source = ?`),r.push(e.source)),e.event!==void 0&&(n.push(`event = ?`),r.push(e.event)),e.tool!==void 0&&e.tool!==``&&(n.push(`tool_name = ?`),r.push(e.tool)),e.skill!==void 0&&e.skill!==``&&(n.push(`skill_name = ?`),r.push(e.skill)),e.file!==void 0&&e.file!==``&&(n.push(`file_path = ?`),r.push(e.file)),e.skills!==void 0&&e.skills.length>0){let t=e.skills.map(()=>`?`).join(`,`);n.push(`skill_name IN (${t})`);for(let t of e.skills)r.push(t)}if(e.projectPaths!==void 0&&e.projectPaths.length>0){let t=e.projectPaths.map(()=>`?`).join(`,`);n.push(`project_path IN (${t})`);for(let t of e.projectPaths)r.push(t)}if(e.mcpServers!==void 0&&e.mcpServers.length>0){let t=e.mcpServers.map(()=>`tool_name LIKE ? ESCAPE '\\'`);n.push(t.length===1?t[0]:`(${t.join(` OR `)})`);for(let t of e.mcpServers)r.push(Ee(t))}if(e.sessionId!==void 0&&e.sessionId!==``&&(e.sessionIdExact?(n.push(`session_id = ?`),r.push(e.sessionId)):(n.push(`session_id LIKE ?`),r.push(`%${e.sessionId}%`))),e.sessionIds!==void 0&&e.sessionIds.length>0){let t=e.sessionIds.filter(e=>e!==null),i=e.sessionIds.some(e=>e===null),a=[];if(t.length>0){let e=t.map(()=>`?`).join(`,`);a.push(`session_id IN (${e})`);for(let e of t)r.push(e)}i&&a.push(`session_id IS NULL`),n.push(a.length===1?a[0]:`(${a.join(` OR `)})`)}return e.subagentId!==void 0&&e.subagentId!==``&&(n.push(`subagent_id = ?`),r.push(e.subagentId)),e.q!==void 0&&e.q!==``&&(n.push(`(payload LIKE ? OR session_id LIKE ?)`),r.push(`%${e.q}%`),r.push(`%${e.q}%`)),e.range!==void 0&&e.range!==`all`&&(n.push(`${O} >= ?`),r.push(new Date(t-we(e.range)).toISOString())),{clause:n.length>0?`WHERE ${n.join(` AND `)}`:``,params:r}}const De=(e,t)=>{let{clause:n,params:r}=k(t),i=t.limit??100,a=t.offset??0,o=`
|
|
109
109
|
SELECT
|
|
110
110
|
id,
|
|
111
111
|
source,
|
|
@@ -124,7 +124,7 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
124
124
|
subagent_type AS subagentType,
|
|
125
125
|
transcript_path AS transcriptPath
|
|
126
126
|
FROM events
|
|
127
|
-
${n} ORDER BY id DESC LIMIT ?${a>0?` OFFSET ?`:``}`;return r.push(i),a>0&&r.push(a),e.prepare(o).all(...r)},A=(e,t)=>{let{clause:n,params:r}=
|
|
127
|
+
${n} ORDER BY id DESC LIMIT ?${a>0?` OFFSET ?`:``}`;return r.push(i),a>0&&r.push(a),e.prepare(o).all(...r)},A=(e,t)=>{let{clause:n,params:r}=k(t),i=`SELECT COUNT(*) AS count FROM events ${n}`,a=e.prepare(i).get(...r);return a?Number(a.count):0},Oe=(e,t)=>{let{clause:n,params:r}=k(t);return{total:A(e,t),bySource:e.prepare(`SELECT source, COUNT(*) AS count FROM events ${n} GROUP BY source ORDER BY count DESC`).all(...r),byEvent:e.prepare(`SELECT event, COUNT(*) AS count FROM events ${n} GROUP BY event ORDER BY count DESC`).all(...r),bySession:e.prepare(`SELECT session_id AS sessionId, COUNT(*) AS count, MIN(happened_at) AS firstAt, MAX(happened_at) AS lastAt FROM events ${n} GROUP BY session_id ORDER BY count DESC`).all(...r)}};function ke(e){let t=Number(e.firstReceivedAt),n=Number(e.lastReceivedAt),r=JSON.parse(e.projectPaths),i=JSON.parse(e.toolNames),a=JSON.parse(e.skillNames),o=JSON.parse(e.filePaths),s=e.firstAt?Date.parse(e.firstAt):NaN,c=e.lastAt?Date.parse(e.lastAt):NaN,l=!Number.isNaN(s)&&!Number.isNaN(c)&&c>=s?c-s:n-t;return{sessionId:e.sessionId,subagentId:e.subagentId,subagentType:e.subagentType,firstAt:e.firstAt,lastAt:e.lastAt,firstReceivedAt:t,lastReceivedAt:n,durationMs:l,eventCount:Number(e.eventCount),projectPath:r[0]??null,projectPaths:r,tools:i,skills:a,files:o,failureCount:Number(e.failureCount)}}const Ae=(e,t,n=Date.now())=>{let{clause:r,params:i}=k(t,n),a=t.limit??100,o=t.offset??0,s=`
|
|
128
128
|
SELECT
|
|
129
129
|
session_id AS sessionId,
|
|
130
130
|
COUNT(*) AS eventCount,
|
|
@@ -142,7 +142,7 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
142
142
|
GROUP BY session_id
|
|
143
143
|
ORDER BY lastReceivedAt DESC, sessionId
|
|
144
144
|
LIMIT ?${o>0?` OFFSET ?`:``}
|
|
145
|
-
`;return i.push(a),o>0&&i.push(o),e.prepare(s).all(...i).map(
|
|
145
|
+
`;return i.push(a),o>0&&i.push(o),e.prepare(s).all(...i).map(ke)},je=(e,t={},n=10,r=Date.now())=>{let{clause:i,params:a}=k(t,r),o=`tool_name IS NOT NULL AND tool_name <> ''`,s=`SELECT tool_name AS tool, COUNT(*) AS count FROM events ${i?`${i} AND ${o}`:`WHERE ${o}`} GROUP BY tool_name ORDER BY count DESC, tool_name ASC LIMIT ?`;return e.prepare(s).all(...a,n)},Me=(e,t={},n=10,r=Date.now())=>{let{clause:i,params:a}=k(t,r),o=`skill_name IS NOT NULL AND skill_name <> ''`,s=`SELECT skill_name AS skill, COUNT(*) AS count FROM events ${i?`${i} AND ${o}`:`WHERE ${o}`} GROUP BY skill_name ORDER BY count DESC, skill_name ASC LIMIT ?`;return e.prepare(s).all(...a,n)},Ne=(e,t={},n=10,r=Date.now())=>{let{clause:i,params:a}=k(t,r),o=[`file_path IS NOT NULL`,`file_path <> ''`,`LOWER(file_path) LIKE '%.md'`];t.mdDir&&o.push(`project_path = ?`);let s=o.join(` AND `),c=`SELECT file_path AS file, COUNT(*) AS count FROM events ${i?`${i} AND ${s}`:`WHERE ${s}`} GROUP BY file_path ORDER BY count DESC, file_path ASC LIMIT ?`,l=e.prepare(c),u=t.mdDir?[...a,t.mdDir,n]:[...a,n];return l.all(...u)},Pe=`
|
|
146
146
|
CASE
|
|
147
147
|
WHEN tool_name LIKE 'mcp\\_\\_%' ESCAPE '\\' THEN 'mcpServers'
|
|
148
148
|
WHEN file_path IS NOT NULL AND file_path <> '' AND LOWER(file_path) LIKE '%.md' THEN 'mdFiles'
|
|
@@ -151,9 +151,9 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
151
151
|
WHEN event IN ('user', 'assistant', 'prompt') THEN 'actualValue'
|
|
152
152
|
ELSE 'bloatware'
|
|
153
153
|
END
|
|
154
|
-
`;function
|
|
154
|
+
`;function Fe(){return{bytes:{mcpServers:0,mdFiles:0,bloatware:0,actualValue:0},tokens:{mcpServers:0,mdFiles:0,bloatware:0,actualValue:0}}}const Ie=(e,t={},n=Date.now())=>{let{clause:r,params:i}=k(t,n),a=`
|
|
155
155
|
SELECT
|
|
156
|
-
${
|
|
156
|
+
${Pe} AS bucket,
|
|
157
157
|
SUM(LENGTH(payload)) AS bytes,
|
|
158
158
|
SUM(
|
|
159
159
|
CASE WHEN json_valid(payload) THEN
|
|
@@ -164,8 +164,8 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
164
164
|
) AS tokens
|
|
165
165
|
FROM events
|
|
166
166
|
${r}
|
|
167
|
-
GROUP BY ${
|
|
168
|
-
`,o=e.prepare(a).all(...i),s=
|
|
167
|
+
GROUP BY ${Pe}
|
|
168
|
+
`,o=e.prepare(a).all(...i),s=Fe();for(let e of o)s.bytes[e.bucket]=Number(e.bytes),s.tokens[e.bucket]=Number(e.tokens);return s};function Le(e){return e===`day`?`strftime('%Y-%m-%dT00:00:00.000Z', ${O})`:`strftime('%Y-%m-%dT%H:00:00.000Z', ${O})`}function Re(e,t=Date.now()){let n=new Date(t);return e===`day`?n.setUTCHours(0,0,0,0):(n.setUTCMinutes(0,0,0),n.setUTCMilliseconds(0)),n}function ze(e){return e===`day`?1440*60*1e3:3600*1e3}const Be=(e,t={},n=24,r=`hour`,i=Date.now())=>{if(n<=0){let n=Le(r),{clause:a,params:o}=k({...t,range:void 0},i),s=`SELECT ${n} AS bucket, COUNT(*) AS count FROM events ${a} GROUP BY bucket ORDER BY bucket`;return e.prepare(s).all(...o)}let a=ze(r),o=Re(r,i),s=Re(r,i-n*60*60*1e3),c=Math.max(1,Math.ceil((o.getTime()-s.getTime())/a)+1),l=s.toISOString(),u=Le(r),{clause:d,params:f}=k({...t,range:void 0},i),p=`${u} >= ?`,m=`SELECT ${u} AS bucket, COUNT(*) AS count FROM events ${d?`${d} AND ${p}`:`WHERE ${p}`} GROUP BY bucket ORDER BY bucket`,h=e.prepare(m).all(...f,l),g=[],_=new Map;for(let e of h)_.set(e.bucket,e.count);for(let e=0;e<c;e++){let e=r===`day`?`${s.toISOString().slice(0,10)}T00:00:00.000Z`:s.toISOString();g.push({bucket:e,count:_.get(e)??0}),s.setTime(s.getTime()+a)}return g},Ve=e=>{let t=e.prepare(`SELECT id FROM events ORDER BY id DESC LIMIT 1`).get();return t?Number(t.id):0},He=(e,t)=>{if(e.failureCount>0)return`failed`;let n=e.lastAt?Date.parse(e.lastAt):NaN;return t-(Number.isNaN(n)?e.lastReceivedAt:Math.min(n,e.lastReceivedAt))<=300*1e3?`active`:`completed`};function Ue(e,t,n){return!(t.status!==void 0&&He(e,n)!==t.status||t.minDuration!==void 0&&e.durationMs<t.minDuration*60*1e3||t.maxDuration!==void 0&&e.durationMs>t.maxDuration*60*1e3)}const We=(e,t,n={})=>{let r=[...new Set(t.filter(e=>e!==null))];if(r.length===0)return[];let{clause:i,params:a}=k({...n,sessionId:void 0,sessionIds:void 0,subagentId:void 0}),o=i?` ${i.replace(/^WHERE/,`AND`)}`:``,s=[];for(let t=0;t<r.length;t+=500){let n=r.slice(t,t+500),i=`
|
|
169
169
|
SELECT
|
|
170
170
|
session_id AS sessionId,
|
|
171
171
|
subagent_id AS subagentId,
|
|
@@ -184,13 +184,22 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
184
184
|
WHERE subagent_id IS NOT NULL AND subagent_id <> '' AND session_id IN (${n.map(()=>`?`).join(`,`)})${o}
|
|
185
185
|
GROUP BY session_id, subagent_id
|
|
186
186
|
ORDER BY lastReceivedAt DESC, subagent_id
|
|
187
|
-
`,c=e.prepare(i);s.push(...c.all(...n,...a))}return s.map(
|
|
187
|
+
`,c=e.prepare(i);s.push(...c.all(...n,...a))}return s.map(ke)},j=(e,t={},n=Date.now())=>{let r=Ae(e,{...t,limit:2**53-1,offset:0},n).filter(e=>Ue(e,t,n)),i=We(e,r.map(e=>e.sessionId),t),a=new Map;for(let e of i){let t=a.get(e.sessionId)??[];t.push(e),a.set(e.sessionId,t)}let o=r.map(e=>({...e,children:a.get(e.sessionId)})),s=t.offset??0,c=t.limit;return s===0&&c===void 0?o:c===void 0?o.slice(s):o.slice(s,s+c)},Ge=e=>{let t=e.prepare(`SELECT DISTINCT source FROM events WHERE source IS NOT NULL AND source <> '' ORDER BY source`).all(),n=e.prepare(`SELECT DISTINCT event FROM events WHERE event IS NOT NULL AND event <> '' ORDER BY event`).all(),r=e.prepare(`SELECT DISTINCT tool_name AS tool FROM events WHERE tool_name IS NOT NULL AND tool_name <> '' ORDER BY tool_name`).all(),i=e.prepare(`SELECT DISTINCT project_path AS dir FROM events WHERE project_path IS NOT NULL AND project_path <> '' AND project_path NOT LIKE '/private/%' ORDER BY project_path`).all(),a=e.prepare(`SELECT DISTINCT skill_name AS skill FROM events WHERE skill_name IS NOT NULL AND skill_name <> '' ORDER BY skill_name`).all(),o=e.prepare(`
|
|
188
|
+
SELECT DISTINCT server FROM (
|
|
189
|
+
SELECT substr(tool_name, 6, instr(substr(tool_name, 6), '__') - 1) AS server
|
|
190
|
+
FROM events
|
|
191
|
+
WHERE tool_name LIKE 'mcp\\_\\_%' ESCAPE '\\'
|
|
192
|
+
AND instr(substr(tool_name, 6), '__') > 0
|
|
193
|
+
)
|
|
194
|
+
WHERE server <> ''
|
|
195
|
+
ORDER BY server
|
|
196
|
+
`).all();return{sources:t.map(e=>e.source),events:n.map(e=>e.event),tools:r.map(e=>e.tool),directories:i.map(e=>e.dir),skills:a.map(e=>e.skill),mcpServers:o.map(e=>e.server)}},M=(e,t,n)=>{e.prepare(`
|
|
188
197
|
INSERT INTO imports (path, mtime, imported_at)
|
|
189
198
|
VALUES (?, ?, ?)
|
|
190
199
|
ON CONFLICT(path) DO UPDATE SET
|
|
191
200
|
mtime = excluded.mtime,
|
|
192
201
|
imported_at = excluded.imported_at
|
|
193
|
-
`).run(t,n,Date.now())},
|
|
202
|
+
`).run(t,n,Date.now())},Ke=(e,t)=>{let n=e.prepare(`SELECT mtime FROM imports WHERE path = ?`).get(t);return n?Number(n.mtime):void 0},qe=e=>{e.exec(`DELETE FROM imports`)},Je=new Set([`Read`,`Edit`,`Write`,`MultiEdit`,`NotebookEdit`]);function N(e){return typeof e==`string`&&e.length>0?e:void 0}function Ye(e,t){if(!e||!Je.has(e)||typeof t!=`object`||!t)return;let n=t;return N(n.file_path)??N(n.notebook_path)??N(n.path)}function Xe(e,t){if(!(e!==`Skill`||typeof t!=`object`||!t))return N(t.skill)}function P(e){if(e==null||typeof e==`object`)return;let t=String(e).trim();return t.length>0?t:void 0}function F(e){let t=P(e);if(t!==void 0)return t;if(Array.isArray(e))for(let t of e){let e=P(t);if(e!==void 0)return e}}function Ze(e){if(typeof e!=`object`||!e)return!1;let t=e,n=t.errcode??0;return t.code===`ERR_SQLITE_ERROR`&&((n&255)==5||(n&255)==6)}function Qe(e,t,n){if(t.length===0)return;let r;try{r=JSON.parse(t)}catch{return}let i=e[0];if(i!==`cursor`&&i!==`claude`)return;let a;a=i===`cursor`?P(r.hook_event_name):e[1];let o=i===`cursor`&&a===`subagentStart`?P(r.parent_conversation_id)??P(r.conversation_id)??P(r.sessionId)??P(r.session_id):P(r.sessionId)??P(r.session_id),s=Ce(r),c=P(r.projectPath)??P(r.cwd)??P(r.project_path)??F(r.workspaceRoot)??F(r.workspaceRoots)??F(r.workspace_roots)??F(r.workspace_root)??P(r.workspace_path),l=P(r.toolName??r.tool_name??r.tool),u=P(r.filePath??r.file_path??r.path)??Ye(l,r.tool_input),d=Xe(l,r.tool_input),f=P(r.client)??(i===`cursor`?`cursor`:`claude_code`),p=P(r.subagent_id)??void 0,m=P(r.subagent_type)??void 0,h=P(r.transcript_path)??void 0,g={source:i,client:f,event:a,sessionId:o,happenedAt:s,projectPath:c,filePath:u,toolName:l,skillName:d,payload:t,subagentId:p,subagentType:m,transcriptPath:h},_=b(i,a??``),v;try{if(v=E(n,500),!g.subagentId){let e=P(r.tool_use_id);if(e){let t=v.prepare(`SELECT subagent_id FROM events WHERE event = 'subagentStart' AND subagent_id = ? LIMIT 1`).get(e);t&&(g.subagentId=t.subagent_id)}}D(v,g)}catch(e){if(Ze(e))console.warn(`happenin: dropping event due to database lock`,e);else throw e}finally{try{v?.close()}catch{}}return _}async function $e(e){let t=[];for await(let e of c.stdin)t.push(Buffer.isBuffer(e)?e:Buffer.from(e,`utf8`));let n=Qe(e,Buffer.concat(t).toString(`utf8`).trim());n!==void 0&&c.stdout.write(`${n}\n`)}const et=`claude-transcript`,I=`cursor-transcript`,tt=`cursor`;function nt(e){if(e.type!==`assistant`||typeof e.message!=`object`||e.message===null)return[];let t=e.message.content;if(!Array.isArray(t))return[];let n=[];for(let e of t)if(typeof e==`object`&&e&&e.type===`tool_use`&&typeof e.name==`string`){let t=e.name,r=e.input;n.push({name:t,filePath:Ye(t,r),skillName:Xe(t,r)})}return n}const L=e=>{try{return r(e,{withFileTypes:!0})}catch{return[]}},rt=e=>{try{return n(e,`utf8`)}catch{return}},it=e=>{try{return Math.floor(a(e).mtimeMs)}catch{return}},R=(e,t)=>{let n=it(t);if(n===void 0||Ke(e,t)===n)return;let r=rt(t);if(r!==void 0)return{content:r,mtime:n}};function at(e){let t=[];for(let n of L(e)){if(n.isSymbolicLink())continue;let r=s.join(e,n.name);n.isDirectory()?t.push(...at(r)):n.isFile()&&n.name.endsWith(`.jsonl`)&&n.name!==`store.db`&&t.push(r)}return t}function ot(e){let t=[];for(let n of L(e)){if(!n.isDirectory()||n.isSymbolicLink())continue;let r=s.join(e,n.name);for(let e of L(r))e.isDirectory()&&!e.isSymbolicLink()&&t.push(s.join(r,e.name))}return t}async function st(e,t){let n=R(e,t);if(n===void 0)return;let{content:r,mtime:i}=n,a=s.basename(t,`.jsonl`),o=[];for(let e of r.split(/\r?\n/)){if(e.trim().length===0)continue;let t;try{t=JSON.parse(e)}catch{continue}typeof t==`object`&&t&&o.push({obj:t,line:e})}e.prepare(`DELETE FROM events WHERE source = ? AND source_path = ?`).run(et,t);for(let{obj:n,line:r}of o){let i={source:et,client:`claude_code`,event:typeof n.type==`string`?n.type:`transcript`,sessionId:typeof n.sessionId==`string`?n.sessionId:a,happenedAt:typeof n.timestamp==`string`?n.timestamp:typeof n.timestamp==`number`?String(n.timestamp):void 0,projectPath:typeof n.cwd==`string`?n.cwd:void 0,payload:r,sourcePath:t},o=nt(n);if(o.length===0){D(e,i);continue}for(let t of o)D(e,{...i,toolName:t.name,filePath:t.filePath,skillName:t.skillName})}M(e,t,i)}async function ct(e){let t=s.resolve(x(),`.claude/projects`);for(let n of at(t))await st(e,n)}async function lt(e,t,n){let r=R(e,t);if(r===void 0)return;let{content:i,mtime:a}=r,o=[],s;try{s=JSON.parse(i)}catch{return}Array.isArray(s)&&(o=s),e.prepare(`DELETE FROM events WHERE source = ? AND source_path = ?`).run(I,t);for(let r of o)typeof r==`string`&&D(e,{source:I,client:tt,event:`prompt`,sessionId:n,payload:JSON.stringify({prompt:r}),sourcePath:t});M(e,t,a)}async function ut(e,t,n){let r=R(e,t);if(r===void 0)return;let{content:i,mtime:a}=r;e.prepare(`DELETE FROM events WHERE source = ? AND source_path = ?`).run(I,t),D(e,{source:I,client:tt,event:`session_meta`,sessionId:n,payload:i,sourcePath:t}),M(e,t,a)}async function dt(e,t){let n=s.basename(t);await lt(e,s.join(t,`prompt_history.json`),n),await ut(e,s.join(t,`meta.json`),n)}async function ft(e){let t=s.resolve(x(),`.cursor/chats`);for(let n of ot(t))await dt(e,n)}async function pt(e){let t=E(T());try{be(t),e?.includes(`--force`)&&qe(t),await mt(t)}finally{t.close()}}async function mt(e){await ct(e),await ft(e)}function z(e){return e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`)}function B(e){return e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`)}function V(e,t=24){return e.length<=t?e:`${e.slice(0,t-1)}…`}function ht(e){if(e.length===0)return``;let t=e[0];for(let n of e.slice(1)){let e=0;for(;e<t.length&&e<n.length&&t.charAt(e)===n.charAt(e);)e++;if(t=t.slice(0,e),t===``)return``}let n=process.platform===`win32`?Math.max(t.lastIndexOf(`/`),t.lastIndexOf(`\\`)):t.lastIndexOf(`/`);return n>0?t.slice(0,n+1):``}function gt(e,t){return e.split(process.platform===`win32`?/[\\/]/:/\//).filter(e=>e.length>0).slice(-t).join(`/`)}function _t(e){let t=Math.max(0,Math.round(e/1e3)),n=Math.floor(t/60),r=t%60;return n>0?`${n}m ${r}s`:`${r}s`}function H(e){if(e==null||e===``)return`-`;let t=new Date(e);return Number.isNaN(t.getTime())?`-`:t.toLocaleString()}function U(e,t,n){if(n!==void 0&&n.length!==0)for(let r of n)e.append(t,r)}function vt(e,t={}){let n={...e,...t},r=new URLSearchParams;return n.source&&r.set(`source`,n.source),n.event&&r.set(`event`,n.event),n.sessionId&&r.set(`session`,n.sessionId),n.q&&r.set(`q`,n.q),n.since!==void 0&&r.set(`since`,String(n.since)),n.range&&r.set(`range`,n.range),n.status&&r.set(`status`,n.status),n.tool&&r.set(`tool`,n.tool),n.skill&&r.set(`skill`,n.skill),n.file&&r.set(`file`,n.file),n.mdDir&&r.set(`mdDir`,n.mdDir),U(r,`dirs`,n.projectPaths),U(r,`skills`,n.skills),U(r,`mcp`,n.mcpServers),n.view&&r.set(`view`,n.view),n.minDuration!==void 0&&r.set(`minDuration`,String(n.minDuration)),n.maxDuration!==void 0&&r.set(`maxDuration`,String(n.maxDuration)),n.limit!==void 0&&r.set(`limit`,String(n.limit)),n.offset!==void 0&&r.set(`offset`,String(n.offset)),r.toString()}function W(e,t={}){return`/fragments/sessions?${vt(e,t)}`}function yt(e){let t=e.totalSessions===0?`-`:`${Math.round(e.successRate)}%`;return`<div class="metric-grid">
|
|
194
203
|
<div class="metric-card metric-sessions">
|
|
195
204
|
<div class="metric-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 18 9 12 13 16 21 6"/><circle cx="21" cy="6" r="2"/></svg></div>
|
|
196
205
|
<div class="metric-label">Total Sessions</div>
|
|
@@ -204,17 +213,17 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
204
213
|
<div class="metric-card metric-duration">
|
|
205
214
|
<div class="metric-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
|
|
206
215
|
<div class="metric-label">Average Duration</div>
|
|
207
|
-
<div class="metric-value">${
|
|
216
|
+
<div class="metric-value">${_t(e.averageDurationMs)}</div>
|
|
208
217
|
</div>
|
|
209
218
|
<div class="metric-card metric-success">
|
|
210
219
|
<div class="metric-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><polyline points="9 12 12 15 16 10"/></svg></div>
|
|
211
220
|
<div class="metric-label">Success Rate</div>
|
|
212
221
|
<div class="metric-value">${t}</div>
|
|
213
222
|
</div>
|
|
214
|
-
</div>`}function
|
|
223
|
+
</div>`}function bt(e,t){return t===`day`?e.slice(5,10):e.slice(11,16)}function xt(e,t=`hour`){if(e.length===0)return`<div class="chart-panel"><h2 class="chart-title">Event Frequency Over Time</h2><div class="chart-empty">No data</div></div>`;let n=Math.max(0,...e.map(e=>e.count)),r=e.length>1?552/(e.length-1):0,i=n>0?172/n:1,a=e.map((e,n)=>({x:44+n*r,y:216-e.count*i,count:e.count,label:bt(e.bucket,t)})),o=a.map(e=>`${e.x},${e.y}`).join(` `),s=`M ${a[0].x} 216 ${a.map(e=>`L ${e.x} ${e.y}`).join(` `)} L ${a[a.length-1].x} 216 Z`,c=e.length<=12?1:Math.ceil(e.length/8),l=a.filter((e,t)=>t%c===0).map(e=>`<text x="${e.x}" y="248" text-anchor="middle">${z(e.label)}</text>`).join(``),u=[0,.5,1].map(e=>{let t=n>0?Math.round(n*e):0;return`<text x="36" y="${216-172*e+4}" text-anchor="end" class="y-label">${t}</text>`}).join(``);return`<div class="chart-panel"><h2 class="chart-title">Event Frequency Over Time</h2>${`<svg class="event-chart" viewBox="0 0 640 260" preserveAspectRatio="xMidYMid meet"><defs><linearGradient id="areaGradient" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="var(--accent)" stop-opacity="0.5"/><stop offset="100%" stop-color="var(--accent-2)" stop-opacity="0.05"/></linearGradient></defs>${[.25,.5,.75].map(e=>{let t=216-172*e;return`<line class="grid" x1="44" y1="${t}" x2="596" y2="${t}"/>`}).join(``)}<line class="axis" x1="44" y1="44" x2="44" y2="216"/><line class="axis" x1="44" y1="216" x2="596" y2="216"/>${u}<path class="area" fill="url(#areaGradient)" d="${s}"/><polyline points="${o}"/>${l}</svg>`}${`<div class="chart-max">max ${n.toLocaleString()}</div>`}</div>`}function St(e,t,n){return e.length<=n?e:t===`start`?`…${e.slice(-(n-1))}`:`${e.slice(0,n-1)}…`}function G(e,t,n={}){let{truncate:r=`end`,labelLimit:i=20,wideLabel:a=!1}=n,o=a?`chart-panel chart-panel--wide-label`:`chart-panel`;if(t.length===0)return`<div class="${o}"><h2 class="chart-title">${z(e)}</h2><div class="chart-empty">No data</div></div>`;let s=Math.max(...t.map(e=>e.count)),c=t.map(e=>{let t=s>0?e.count/s*100:0,n=z(St(e.label,r,i)),a=`<span class="tool-name" title="${B(e.title??e.label)}">${n}</span><div class="tool-bar-bg"><div class="tool-bar" style="width: ${t}%"></div></div><span class="tool-count">${e.count}</span>`;return e.href?`<a class="tool-row" href="${B(e.href)}" hx-get="${B(e.href)}" hx-target="#dashboard-content" hx-swap="innerHTML">${a}</a>`:`<div class="tool-row">${a}</div>`}).join(``);return`<div class="${o}"><h2 class="chart-title">${z(e)}</h2><div class="tool-chart">${c}</div></div>`}function Ct(e,t){return G(`Most Used Tools`,e.map(e=>({label:e.tool,count:e.count,href:t&&W(t,{tool:e.tool,view:`list`,offset:0})})),{labelLimit:36,wideLabel:!0})}function wt(e,t){return G(`Most Used Skills`,e.map(e=>({label:e.skill,count:e.count,href:t&&W(t,{skill:e.skill,view:`list`,offset:0})})),{labelLimit:36,wideLabel:!0})}function Tt(e,t){return G(`Top Markdown Files`,e.map(e=>({label:gt(e.file,2),title:e.file,count:e.count,href:t&&W(t,{file:e.file,view:`list`,offset:0})})),{truncate:`start`,labelLimit:40,wideLabel:!0})}const K=[{key:`actualValue`,label:`Actual value`,color:`var(--success)`},{key:`mdFiles`,label:`Markdown files`,color:`var(--accent-2)`},{key:`mcpServers`,label:`MCP servers`,color:`var(--accent)`},{key:`bloatware`,label:`Bloatware`,color:`var(--warning)`}];function Et(e){return e>=1e6?`${(e/1e6).toFixed(1)}MB`:e>=1e3?`${(e/1e3).toFixed(1)}KB`:`${e}B`}function Dt(e){return e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e3?`${(e/1e3).toFixed(1)}K`:`${e}`}function Ot(e,t,n,r){let i=K.reduce((e,{key:n})=>e+t[n],0);if(i===0)return`<div class="context-bar-block">
|
|
215
224
|
<h3 class="context-bar-title">${z(e)}</h3>
|
|
216
225
|
<div class="chart-empty">${z(r)}</div>
|
|
217
|
-
</div>`;let a=
|
|
226
|
+
</div>`;let a=K.filter(({key:e})=>t[e]>0).map(({key:e,label:r,color:a})=>{let o=t[e],s=o/i*100;return`<div class="context-segment" style="width: ${s}%; background: ${a}" title="${z(`${r}: ${n(o)} (${s.toFixed(1)}%)`)}"></div>`}).join(``),o=K.map(({key:e,label:r,color:a})=>{let o=t[e],s=o/i*100;return`<div class="context-legend-row">
|
|
218
227
|
<span class="context-legend-dot" style="background: ${a}"></span>
|
|
219
228
|
<span class="context-legend-label">${z(r)}</span>
|
|
220
229
|
<span class="context-legend-value">${z(n(o))} · ${s.toFixed(1)}%</span>
|
|
@@ -222,21 +231,21 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
222
231
|
<h3 class="context-bar-title">${z(e)}</h3>
|
|
223
232
|
<div class="context-bar">${a}</div>
|
|
224
233
|
<div class="context-legend">${o}</div>
|
|
225
|
-
</div>`}function
|
|
234
|
+
</div>`}function kt(e){return`<div class="chart-panel context-breakdown">
|
|
226
235
|
<h2 class="chart-title">Context Breakdown</h2>
|
|
227
|
-
<div class="context-bars">${
|
|
228
|
-
</div>`}function
|
|
236
|
+
<div class="context-bars">${Ot(`By payload size (all sources)`,e.bytes,Et,`No data`)}${Ot(`By tokens (imported Claude sessions)`,e.tokens,Dt,"Run `happenin import` to see token-based numbers")}</div>
|
|
237
|
+
</div>`}function At(e,t,n){let r=new URLSearchParams({session:t});if(n&&r.set(`subagent`,n),e?.source&&r.set(`source`,e.source),e?.event&&r.set(`event`,e.event),e?.q&&r.set(`q`,e.q),e?.range&&r.set(`range`,e.range),e?.status&&r.set(`status`,e.status),e?.tool&&r.set(`tool`,e.tool),e?.projectPaths&&e.projectPaths.length>0)for(let t of e.projectPaths)r.append(`dirs`,t);if(e?.skills&&e.skills.length>0)for(let t of e.skills)r.append(`skills`,t);if(e?.mcpServers&&e.mcpServers.length>0)for(let t of e.mcpServers)r.append(`mcp`,t);return e?.minDuration!==void 0&&r.set(`minDuration`,String(e.minDuration)),e?.maxDuration!==void 0&&r.set(`maxDuration`,String(e.maxDuration)),e?.limit!==void 0&&r.set(`limit`,String(e.limit)),e?.offset!==void 0&&r.set(`offset`,String(e.offset)),`/fragments/detail?${r.toString()}`}function q(e,t,n,r,i,a=!1,o=``,s=``){let c=He(e,t),l=a?e.subagentId??`no subagent`:e.sessionId??`no session`,u=z(V(l,28)),d=z(H(e.firstAt??e.firstReceivedAt)),f=z(_t(e.durationMs)),p=(a?e.subagentId===r&&e.sessionId===n:e.sessionId===n)?` active`:``,m=e.sessionId??``,h=a?e.subagentId:void 0,g=!a&&!!e.children?.length,_=m&&!g?` hx-get="${B(At(i,m,h))}" hx-target="#dashboard-content" hx-swap="innerHTML"`:``,v=m&&g?` hx-get="${B(At(i,m))}" hx-target="#dashboard-content" hx-swap="innerHTML"`:``,y=a?` session-subagent`:``,b=!a&&e.children?.length&&e.sessionId===n?` expanded`:``,x=!a&&e.children?.length?` session-parent`:``,S=!a&&!e.sessionId?` session-item-static`:``,ee=a?` data-session="${B(m)}" data-subagent="${B(e.subagentId??``)}"`:` data-session="${B(e.sessionId??``)}"`,te=a&&e.subagentType?` <span class="subagent-type-badge">${z(e.subagentType)}</span>`:``;return`<li class="session-item${x}${b}${y}${p}${S}"${ee}${_}>
|
|
229
238
|
${o}
|
|
230
239
|
<div class="session-main"${v}>
|
|
231
240
|
<div class="session-row">
|
|
232
|
-
<span class="session-id" title="${B(l)}">${u}${
|
|
241
|
+
<span class="session-id" title="${B(l)}">${u}${te}</span>
|
|
233
242
|
<span class="status-badge status-${c}">${c}</span>
|
|
234
243
|
</div>
|
|
235
244
|
<div class="session-meta"><span>${f}</span><span>${e.eventCount} events</span><span>${d}</span></div>
|
|
236
245
|
</div>
|
|
237
246
|
<span class="session-chevron" aria-hidden="true"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg></span>
|
|
238
247
|
${s}
|
|
239
|
-
</li>`}function
|
|
248
|
+
</li>`}function jt(e,t=Date.now(),n,r,i){return e.length===0?`<ul class="session-list sessions-table"><li class="empty">No sessions found.</li></ul>`:`<ul class="session-list sessions-table">${e.map(e=>e.children?.length?q(e,t,n,r,i,!1,`<button type="button" class="session-toggle" aria-label="toggle subagents" onclick="event.stopPropagation(); const li = this.closest('.session-item'); li.classList.toggle('expanded'); this.textContent = li.classList.contains('expanded') ? '▾' : '▸'">${e.sessionId===n?`▾`:`▸`}</button>`,`<ul class="session-children">${e.children.map(e=>q(e,t,n,r,i,!0)).join(``)}</ul>`):q(e,t,n,r,i)).join(``)}</ul>`}function Mt(e){if(e)try{return JSON.parse(e)}catch{return}}function Nt(e){return{id:e.id,source:e.source,client:e.client,event:e.event,sessionId:e.sessionId,happenedAt:e.happenedAt,receivedAt:e.receivedAt,projectPath:e.projectPath,filePath:e.filePath,toolName:e.toolName,skillName:e.skillName,sourcePath:e.sourcePath,subagentId:e.subagentId,subagentType:e.subagentType,transcriptPath:e.transcriptPath,payload:Mt(e.payload??``)??e.payload}}function Pt(e=`Back`){return`<button type="button" class="detail-back" onclick="backToDashboard()" aria-label="back to dashboard"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg><span>${e}</span></button>`}function Ft(e){let t=new Map;for(let n of e){let e=n.subagentId??null;t.has(e)||t.set(e,[]),t.get(e).push(n)}let n=[];for(let[e,r]of t){let t=r.toSorted((e,t)=>t.id-e.id),i=t.findIndex(e=>e.event===`subagentStart`),a=i>=0?t.at(i):t.at(0),o=t.filter(e=>e!==a);n.push({subagentId:e,parent:a,children:o,maxId:t.at(0).id})}return n.toSorted((e,t)=>t.maxId-e.maxId)}function It(e){return`<span class="ts">${z(H(e.happenedAt??e.receivedAt))}</span><span class="source">${z(e.source??``)}</span><span class="event">${z(e.event??``)}</span><span class="tool">${z(e.toolName??``)}</span>`}function Lt(e){let t=z(H(e.happenedAt??e.receivedAt)),n=z(e.source??``),r=z(e.client??``),i=z(e.event??``),a=z(e.toolName??``),o=z(e.subagentId??``),s=z(e.subagentType??``),c=z(e.projectPath??``),l=z(e.filePath??``),u=z(e.skillName??``),d=o?`<div><span>Subagent</span><span>${s||`-`} (${o})</span></div>`:``,f=l?`<div><span>File</span><span>${l}</span></div>`:``,p=u?`<div><span>Skill</span><span>${u}</span></div>`:``;return`<div class="detail-attrs">
|
|
240
249
|
<div><span>Time</span><span>${t}</span></div>
|
|
241
250
|
<div><span>Source</span><span>${n}</span></div>
|
|
242
251
|
<div><span>Client</span><span>${r||`-`}</span></div>
|
|
@@ -246,32 +255,32 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
246
255
|
${d}
|
|
247
256
|
${f}
|
|
248
257
|
${p}
|
|
249
|
-
</div>`}function
|
|
258
|
+
</div>`}function Rt(e){return`<div class="detail-json">
|
|
250
259
|
<details class="json-toggle">
|
|
251
260
|
<summary>JSON</summary>
|
|
252
|
-
<pre>${z(JSON.stringify(
|
|
261
|
+
<pre>${z(JSON.stringify(Nt(e),null,2))}</pre>
|
|
253
262
|
</details>
|
|
254
263
|
<button type="button" class="json-copy" onclick="copyEventJson(this)" title="Copy JSON" aria-label="Copy JSON"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
|
|
255
|
-
</div>`}function
|
|
256
|
-
<summary>${
|
|
257
|
-
<div class="detail-body">${
|
|
258
|
-
${
|
|
264
|
+
</div>`}function J(e,t){if(t.length===0)return``;let n=t.map(e=>`<span class="chip" title="${B(e)}">${z(V(e,40))}</span>`).join(``);return`<div class="summary-group"><span class="summary-label">${e} (${t.length})</span><div class="summary-chips">${n}</div></div>`}function zt(e){if(!e)return``;let t=[J(`Tools`,e.tools),J(`Skills`,e.skills),J(`Files`,e.files)].join(``);return t?`<div class="detail-summary">${t}</div>`:``}function Bt(e){return`<details class="detail-event" data-event-id="${e.id}" open>
|
|
265
|
+
<summary>${It(e)}</summary>
|
|
266
|
+
<div class="detail-body">${Lt(e)}
|
|
267
|
+
${Rt(e)}
|
|
259
268
|
</div>
|
|
260
|
-
</details>`}function
|
|
261
|
-
<summary>${
|
|
269
|
+
</details>`}function Vt(e,t){let n=e.parent,r=e.children.map(Bt).join(``),i=`<span class="subagent-badge">${z(n.subagentType||`subagent`)}${` · ${z(V(t,18))}`} · ${e.children.length}</span>`;return`<details class="detail-event detail-subagent" data-event-id="${n.id}" open>
|
|
270
|
+
<summary>${It(n)}${i}</summary>
|
|
262
271
|
<div class="detail-body">
|
|
263
|
-
${
|
|
264
|
-
${
|
|
272
|
+
${Lt(n)}
|
|
273
|
+
${Rt(n)}
|
|
265
274
|
<div class="detail-children">${r}</div>
|
|
266
275
|
</div>
|
|
267
|
-
</details>`}function
|
|
276
|
+
</details>`}function Ht(e){return Ft(e).map(e=>e.subagentId?Vt(e,e.subagentId):[e.parent,...e.children].map(Bt).join(``)).join(``)}function Ut(e,t,n,r,i,a){if(!e)return`<div class="detail-header"><h2 class="detail-title">Session Details</h2></div><div class="empty">No events.</div>`;let o=r?t.filter(e=>e.subagentId===r):t,s=r?`${e} · ${r}`:e;if(o.length===0)return`<div class="detail-header"><h2 class="detail-title">Session Details - ${z(s)}</h2></div><div class="empty">No events for this subagent.</div>`;let c=Ht(o),l=JSON.stringify(o,null,2),u=n!==void 0&&n>o.length?`<div class="detail-truncated">Showing the ${o.length} most recent of ${n} events — older events are hidden and the JSON copy is partial.</div>`:``;return`<section class="session-detail-view" data-session="${B(e)}" data-subagent="${B(r??``)}">
|
|
268
277
|
<div class="detail-header">
|
|
269
|
-
|
|
278
|
+
${Pt()}
|
|
270
279
|
<h2 class="detail-title">Session Details - ${z(s)}</h2>
|
|
271
280
|
<span class="detail-count">${n??o.length} events</span>
|
|
272
281
|
</div>
|
|
273
|
-
${
|
|
274
|
-
${a?
|
|
282
|
+
${zt(i)}
|
|
283
|
+
${a?kt(a):``}
|
|
275
284
|
${u}
|
|
276
285
|
<div class="detail-toolbar">
|
|
277
286
|
<input type="search" class="detail-search" placeholder="Search attributes..." oninput="filterSessionDetails(this.value)">
|
|
@@ -283,18 +292,21 @@ import{existsSync as e,mkdirSync as t,readFileSync as n,readdirSync as r,renameS
|
|
|
283
292
|
<button type="button" onclick="copySessionJson()">Copy JSON</button>
|
|
284
293
|
</div>
|
|
285
294
|
<pre id="session-json" class="session-json">${z(l)}</pre>
|
|
286
|
-
</section>`}const
|
|
287
|
-
<
|
|
295
|
+
</section>`}const Wt={bytes:{mcpServers:0,mdFiles:0,bloatware:0,actualValue:0},tokens:{mcpServers:0,mdFiles:0,bloatware:0,actualValue:0}};function Gt(e){if(!e)return;let t=Number(e);return Number.isFinite(t)&&t>=0?t:void 0}function Kt(e){if(!e)return;let t=Number(e);return Number.isInteger(t)&&t>=0?t:void 0}function qt(e){return e===`7d`?{hours:168,groupBy:`day`}:e===`30d`?{hours:720,groupBy:`day`}:e===`all`?{hours:0,groupBy:`day`}:{hours:24,groupBy:`hour`}}function Y(e){let t=e.searchParams.get(`since`),n=e.searchParams.get(`source`)||void 0,r=e.searchParams.get(`event`)||void 0,i=e.searchParams.get(`session`)||void 0,a=e.searchParams.get(`subagent`)||void 0,o=e.searchParams.get(`q`)||void 0,s=e.searchParams.get(`status`)||void 0,c=e.searchParams.get(`tool`)||void 0,l=e.searchParams.get(`skill`)||void 0,u=e.searchParams.get(`file`)||void 0,d=e.searchParams.get(`mdDir`)||void 0,f=e.searchParams.getAll(`dirs`).filter(e=>e!==``),p=e.searchParams.getAll(`skills`).filter(e=>e!==``),m=e.searchParams.getAll(`mcp`).filter(e=>e!==``),h=(e.searchParams.get(`view`)||void 0)===`list`?`list`:void 0,g=e.searchParams.get(`minDuration`),_=e.searchParams.get(`maxDuration`),v=e.searchParams.get(`range`)||`24h`,y=[`24h`,`7d`,`30d`,`all`].includes(v)?v:`24h`,b=e.searchParams.get(`limit`),x=e.searchParams.get(`offset`),S;if(t){let e=parseInt(t,10);Number.isNaN(e)||(S=e)}return{since:S,source:n,event:r,sessionId:i,subagentId:a,q:o,status:s===`active`||s===`completed`||s===`failed`?s:void 0,tool:c,skill:l,file:u,mdDir:d,view:h,projectPaths:f.length>0?f:void 0,skills:p.length>0?p:void 0,mcpServers:m.length>0?m:void 0,minDuration:Gt(g),maxDuration:Gt(_),range:y,limit:Kt(b),offset:Kt(x)}}function Jt(e,t){let n=e.limit&&e.limit>0?e.limit:25,r=e.offset??0;if(t<=n)return``;let i=Math.floor(r/n)+1,a=Math.ceil(t/n),o=r-n,s=r+n,c=W(e,{offset:Math.max(o,0),limit:n}),l=W(e,{offset:s,limit:n});return`<div class="pager">${o>=0?`<a href="${c}" hx-get="${c}" hx-target="#dashboard-content" hx-swap="innerHTML">Previous</a>`:`<button type="button" disabled>Previous</button>`}<span class="pager-info">Page ${i} of ${a}</span>${s<t?`<a href="${l}" hx-get="${l}" hx-target="#dashboard-content" hx-swap="innerHTML">Next</a>`:`<button type="button" disabled>Next</button>`}</div>`}function Yt(e){if(e.skill)return{kind:`skill`,value:e.skill};if(e.file)return{kind:`file`,value:e.file};if(e.tool)return{kind:`tool`,value:e.tool}}function Xt(e,t,n){let r=j(e,{...t,subagentId:void 0,limit:void 0,offset:void 0},n),i=t.limit&&t.limit>0?t.limit:25,a=t.offset??0,o=r.slice(a,a+i),s=Yt(t),c=W(t,{tool:void 0,skill:void 0,file:void 0,offset:0}),l=s?`<div class="sessions-list-filter">Sessions using ${z(s.kind)}: <strong>${z(s.value)}</strong> <a href="${c}" hx-get="${c}" hx-target="#dashboard-content" hx-swap="innerHTML">Clear</a></div>`:``;return`<div class="main-content sessions-list-view">
|
|
296
|
+
<div class="detail-header">
|
|
297
|
+
${Pt()}
|
|
298
|
+
<h2 class="session-list-title">Sessions <span class="session-collapse-count">${r.length}</span></h2>
|
|
299
|
+
</div>
|
|
288
300
|
${l}
|
|
289
301
|
<div class="session-list-wrapper">
|
|
290
|
-
${
|
|
291
|
-
${
|
|
302
|
+
${jt(o,n,void 0,void 0,t)}
|
|
303
|
+
${Jt(t,r.length)}
|
|
292
304
|
</div>
|
|
293
|
-
</div>`}function
|
|
294
|
-
<div class="main-metrics">${
|
|
295
|
-
<div class="top-charts">${
|
|
296
|
-
${
|
|
297
|
-
</div>`}function
|
|
305
|
+
</div>`}function Zt(e,t){let n=Date.now();if(t.view===`list`)return Xt(e,t,n);let{hours:r,groupBy:i}=qt(t.range),a=j(e,{...t,subagentId:void 0,limit:void 0,offset:void 0},n),o=a.map(e=>e.sessionId),s=[],c=[],l=[],u=[],d=Wt;if(o.length>0){let a={...t,subagentId:void 0,sessionIds:o};s=Be(e,a,r,i,n),c=je(e,a,10,n),l=Me(e,a,10,n),u=Ne(e,a,10,n),d=Ie(e,a,n)}let f=a.length,p=a.reduce((e,t)=>e+t.eventCount,0),m=f>0?a.reduce((e,t)=>e+t.durationMs,0)/f:0,h=a.filter(e=>e.failureCount===0).length;return`<div class="main-content">
|
|
306
|
+
<div class="main-metrics">${yt({totalSessions:f,totalEvents:p,averageDurationMs:m,successRate:f>0?h/f*100:0})}</div>
|
|
307
|
+
<div class="top-charts">${xt(s,i)}${Ct(c,t)}${wt(l,t)}${Tt(u,t)}</div>
|
|
308
|
+
${kt(d)}
|
|
309
|
+
</div>`}function Qt(e){return e.headers[`hx-trigger`]===`dashboard-content`}function $t(e,t){let n={"Content-Type":`text/html; charset=utf-8`};return Qt(e)||(n[`HX-Push-Url`]=`/${t.search}`),n}function en(e,t,n,r){let i=Zt(r,Y(n));t.writeHead(200,$t(e,n)),t.end(i)}function tn(e,t){let n=Date.now();if(!t.sessionId)return`<div class="main-content"><div class="detail-area"><div class="empty">No events.</div></div></div>`;let r=j(e,{...t,sessionId:void 0,subagentId:void 0,limit:void 0,offset:void 0},n),i=De(e,{...t,range:void 0,sessionId:t.sessionId,sessionIdExact:!0,subagentId:t.subagentId,limit:1e3,offset:0}),a=A(e,{...t,range:void 0,sessionId:t.sessionId,sessionIdExact:!0,subagentId:t.subagentId}),o=Ie(e,{...t,range:void 0,sessionId:t.sessionId,sessionIdExact:!0,subagentId:t.subagentId}),s=r.find(e=>e.sessionId===t.sessionId),c=t.subagentId?s?.children?.find(e=>e.subagentId===t.subagentId):s;return`<div class="main-content">${Ut(t.sessionId,i,a,t.subagentId,c,o)}</div>`}function nn(e,t,n,r){let i=tn(r,Y(n));t.writeHead(200,$t(e,n)),t.end(i)}function rn(e,t,n,r){let i=De(r,Y(n));t.writeHead(200,{"Content-Type":`application/json`}),t.end(JSON.stringify(i))}const an=[{value:`24h`,label:`Last 24h`},{value:`7d`,label:`Last 7 days`},{value:`30d`,label:`Last 30 days`},{value:`all`,label:`All time`}];function on(e){let t=z(e.q??``),n=e.range??`24h`;return`<header class="app-header">
|
|
298
310
|
<div class="brand"><svg class="brand-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="4"/><path d="M8 7v10"/><path d="M16 7v10"/><path d="M8 12h8"/></svg> happenin</div>
|
|
299
311
|
<h1 class="page-title">Session Overview Analytics</h1>
|
|
300
312
|
<div class="header-controls">
|
|
@@ -302,18 +314,32 @@ ${Dt(d)}
|
|
|
302
314
|
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
|
303
315
|
<input type="text" name="q" class="search" placeholder="session or payload" value="${t}" autocomplete="off">
|
|
304
316
|
</div>
|
|
305
|
-
<label>range <select name="range">${
|
|
317
|
+
<label>range <select name="range">${an.map(e=>`<option value="${B(e.value)}"${e.value===n?` selected`:``}>${z(e.label)}</option>`).join(``)}</select></label>
|
|
306
318
|
<button type="button" class="theme" data-theme-toggle title="Toggle light/dark theme" aria-label="toggle theme">🌙</button>
|
|
307
319
|
</div>
|
|
308
|
-
</header>`}const
|
|
309
|
-
<
|
|
310
|
-
|
|
311
|
-
<
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
<
|
|
315
|
-
|
|
316
|
-
|
|
320
|
+
</header>`}const sn=[`active`,`completed`,`failed`];function X(e,t,n,r=`all`){let i=n??``,a=B(i);return`<select name="${e}"><option value=""${i===``?` selected`:``}>${r}</option>${t.map(e=>{let t=B(e);return`<option value="${t}"${t===a?` selected`:``}>${z(e)}</option>`}).join(``)}</select>`}function cn(e,t,n){let r=ht(t),i=n??``,a=B(i),o=t.map(e=>{let t=B(e);return`<option value="${t}"${t===a?` selected`:``}>${z(e.slice(r.length))}</option>`}).join(``);return`<select name="${e}"><option value=""${i===``?` selected`:``}>all directories</option>${o}</select>`}function ln(e,t,n,r){let i=new Set(n),a=t.map(({value:e,label:t})=>{let n=B(e),r=i.has(e)?` selected`:``;return`<option value="${n}" data-label="${B(t)}"${r}>${z(t)}</option>`}).join(``);return`<div class="ms" data-ms-all="${B(r)}">
|
|
321
|
+
<select name="${e}" multiple class="ms-native">${a}</select>
|
|
322
|
+
</div>`}function un(e,t,n){let r=ht(t);return ln(e,t.map(e=>({value:e,label:e.slice(r.length)})),n,`All directories`)}function dn(e,t,n,r){return ln(e,t.map(e=>({value:e,label:e})),n,r)}function fn(e,t){let n=t.status,r=t.source,i=t.tool,a=t.event,o=t.minDuration===void 0?``:String(t.minDuration),s=t.maxDuration===void 0?``:String(t.maxDuration),c=t.mdDir,l=t.projectPaths??[],u=t.skills??[],d=t.mcpServers??[],f=t.view===`list`;return`<div class="toolbar-row">
|
|
323
|
+
<button type="button" id="filters-toggle" class="toggle-pill" aria-expanded="false" aria-controls="filter-panel" onclick="toggleFilters()">
|
|
324
|
+
<svg class="toggle-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg><span>Filters</span><span class="toggle-badge" id="filters-badge" hidden></span>
|
|
325
|
+
</button>
|
|
326
|
+
<div class="view-tabs" role="tablist">
|
|
327
|
+
<button type="button" id="tab-overview" class="view-tab" role="tab" aria-selected="${f?`false`:`true`}" onclick="switchView('overview')">Overview</button>
|
|
328
|
+
<button type="button" id="tab-sessions" class="view-tab" role="tab" aria-selected="${f?`true`:`false`}" onclick="switchView('list')">Sessions<span class="toggle-badge" id="sessions-tab-count" hidden></span></button>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="filter-panel" id="filter-panel">
|
|
332
|
+
<div class="field"><label>Status</label>${X(`status`,sn,n)}</div>
|
|
333
|
+
<div class="field"><label>Source</label>${X(`source`,e.sources,r)}</div>
|
|
334
|
+
<div class="field"><label>Tool</label>${X(`tool`,e.tools,i)}</div>
|
|
335
|
+
<div class="field"><label>Event</label>${X(`event`,e.events,a)}</div>
|
|
336
|
+
<div class="field field-wide"><label>Directories</label>${un(`dirs`,e.directories,l)}</div>
|
|
337
|
+
<div class="field field-wide"><label>Skills</label>${dn(`skills`,e.skills,u,`All skills`)}</div>
|
|
338
|
+
<div class="field field-wide"><label>Integrations (MCP)</label>${dn(`mcp`,e.mcpServers,d,`All integrations`)}</div>
|
|
339
|
+
<div class="field"><label>Md files directory</label>${cn(`mdDir`,e.directories,c)}</div>
|
|
340
|
+
<div class="field"><label>Min duration (m)</label><input type="number" name="minDuration" min="0" step="any" placeholder="min" value="${z(o)}"></div>
|
|
341
|
+
<div class="field"><label>Max duration (m)</label><input type="number" name="maxDuration" min="0" step="any" placeholder="max" value="${z(s)}"></div>
|
|
342
|
+
</div>`}function pn(e,t){let n=Ge(e),r=t.sessionId?tn(e,t):Zt(e,t),i=on(t),a=fn(n,t);return`<!DOCTYPE html>
|
|
317
343
|
<html lang="en" hx-ext="sse">
|
|
318
344
|
<head>
|
|
319
345
|
<meta charset="utf-8">
|
|
@@ -411,7 +437,14 @@ ${Dt(d)}
|
|
|
411
437
|
if (!form) return false;
|
|
412
438
|
for (const [name, value] of Object.entries(values)) {
|
|
413
439
|
const field = form.elements[name];
|
|
414
|
-
if (field
|
|
440
|
+
if (!field) continue;
|
|
441
|
+
if (Array.isArray(value)) {
|
|
442
|
+
if (field.multiple && field.options) {
|
|
443
|
+
for (const option of field.options) {
|
|
444
|
+
option.selected = value.includes(option.value);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
} else if (value != null && value !== '') {
|
|
415
448
|
field.value = String(value);
|
|
416
449
|
}
|
|
417
450
|
}
|
|
@@ -420,23 +453,270 @@ ${Dt(d)}
|
|
|
420
453
|
return false;
|
|
421
454
|
}
|
|
422
455
|
|
|
456
|
+
function buildMultiSelect(wrapper) {
|
|
457
|
+
const select = wrapper.querySelector('select.ms-native');
|
|
458
|
+
if (!select) return;
|
|
459
|
+
const allLabel = wrapper.getAttribute('data-ms-all') || 'All';
|
|
460
|
+
const noun = allLabel.replace(/^All /i, '').toLowerCase();
|
|
461
|
+
|
|
462
|
+
const pills = document.createElement('div');
|
|
463
|
+
pills.className = 'ms-pills';
|
|
464
|
+
const searchWrap = document.createElement('div');
|
|
465
|
+
searchWrap.className = 'ms-search-wrap';
|
|
466
|
+
const input = document.createElement('input');
|
|
467
|
+
input.type = 'text';
|
|
468
|
+
input.className = 'ms-search';
|
|
469
|
+
input.placeholder = 'Search ' + noun + '...';
|
|
470
|
+
input.autocomplete = 'off';
|
|
471
|
+
const dropdown = document.createElement('div');
|
|
472
|
+
dropdown.className = 'ms-dropdown';
|
|
473
|
+
dropdown.hidden = true;
|
|
474
|
+
searchWrap.appendChild(input);
|
|
475
|
+
searchWrap.appendChild(dropdown);
|
|
476
|
+
wrapper.insertBefore(pills, select);
|
|
477
|
+
wrapper.insertBefore(searchWrap, select);
|
|
478
|
+
wrapper.classList.add('enhanced');
|
|
479
|
+
|
|
480
|
+
function optionsList() {
|
|
481
|
+
return Array.from(select.options);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function renderPills() {
|
|
485
|
+
pills.innerHTML = '';
|
|
486
|
+
const selected = optionsList().filter((o) => o.selected);
|
|
487
|
+
if (selected.length === 0) {
|
|
488
|
+
const span = document.createElement('span');
|
|
489
|
+
span.className = 'ms-pill ms-pill--all';
|
|
490
|
+
span.textContent = allLabel;
|
|
491
|
+
pills.appendChild(span);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
for (const opt of selected) {
|
|
495
|
+
const label = opt.dataset.label || opt.textContent;
|
|
496
|
+
const pill = document.createElement('span');
|
|
497
|
+
pill.className = 'ms-pill';
|
|
498
|
+
const text = document.createElement('span');
|
|
499
|
+
text.textContent = label;
|
|
500
|
+
text.title = label;
|
|
501
|
+
const remove = document.createElement('button');
|
|
502
|
+
remove.type = 'button';
|
|
503
|
+
remove.setAttribute('aria-label', 'Remove ' + label);
|
|
504
|
+
remove.textContent = String.fromCharCode(215);
|
|
505
|
+
remove.addEventListener('click', (e) => {
|
|
506
|
+
e.stopPropagation();
|
|
507
|
+
opt.selected = false;
|
|
508
|
+
select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
509
|
+
renderPills();
|
|
510
|
+
renderDropdown();
|
|
511
|
+
});
|
|
512
|
+
pill.appendChild(text);
|
|
513
|
+
pill.appendChild(remove);
|
|
514
|
+
pills.appendChild(pill);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function renderDropdown() {
|
|
519
|
+
const term = input.value.trim().toLowerCase();
|
|
520
|
+
const unselected = optionsList().filter((o) => !o.selected);
|
|
521
|
+
const matches = term
|
|
522
|
+
? unselected.filter((o) => (o.dataset.label || o.textContent).toLowerCase().includes(term))
|
|
523
|
+
: unselected;
|
|
524
|
+
dropdown.innerHTML = '';
|
|
525
|
+
if (matches.length === 0) {
|
|
526
|
+
const empty = document.createElement('div');
|
|
527
|
+
empty.className = 'ms-empty';
|
|
528
|
+
empty.textContent = unselected.length === 0 ? 'All options selected' : 'No matches';
|
|
529
|
+
dropdown.appendChild(empty);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
for (const opt of matches.slice(0, 50)) {
|
|
533
|
+
const item = document.createElement('div');
|
|
534
|
+
item.className = 'ms-option';
|
|
535
|
+
item.textContent = opt.dataset.label || opt.textContent;
|
|
536
|
+
item.addEventListener('mousedown', (e) => {
|
|
537
|
+
e.preventDefault();
|
|
538
|
+
opt.selected = true;
|
|
539
|
+
select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
540
|
+
input.value = '';
|
|
541
|
+
renderPills();
|
|
542
|
+
renderDropdown();
|
|
543
|
+
});
|
|
544
|
+
dropdown.appendChild(item);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
input.addEventListener('focus', () => {
|
|
549
|
+
renderDropdown();
|
|
550
|
+
dropdown.hidden = false;
|
|
551
|
+
});
|
|
552
|
+
input.addEventListener('input', () => {
|
|
553
|
+
renderDropdown();
|
|
554
|
+
dropdown.hidden = false;
|
|
555
|
+
});
|
|
556
|
+
input.addEventListener('blur', () => {
|
|
557
|
+
setTimeout(() => {
|
|
558
|
+
dropdown.hidden = true;
|
|
559
|
+
}, 150);
|
|
560
|
+
});
|
|
561
|
+
input.addEventListener('keydown', (e) => {
|
|
562
|
+
if (e.key === 'Escape') {
|
|
563
|
+
dropdown.hidden = true;
|
|
564
|
+
input.blur();
|
|
565
|
+
}
|
|
566
|
+
if (e.key === 'Enter') {
|
|
567
|
+
e.preventDefault();
|
|
568
|
+
const first = dropdown.querySelector('.ms-option');
|
|
569
|
+
if (first) first.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
select.msRefresh = () => {
|
|
574
|
+
renderPills();
|
|
575
|
+
renderDropdown();
|
|
576
|
+
};
|
|
577
|
+
renderPills();
|
|
578
|
+
renderDropdown();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function initMultiSelects() {
|
|
582
|
+
for (const wrapper of document.querySelectorAll('.ms')) {
|
|
583
|
+
buildMultiSelect(wrapper);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function refreshMultiSelects() {
|
|
588
|
+
for (const select of document.querySelectorAll('select.ms-native')) {
|
|
589
|
+
if (select.msRefresh) select.msRefresh();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function getViewField() {
|
|
594
|
+
const form = document.querySelector('[data-filter-form]');
|
|
595
|
+
return form ? form.elements['view'] : null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function getActiveView() {
|
|
599
|
+
const field = getViewField();
|
|
600
|
+
return field && field.value === 'list' ? 'list' : 'overview';
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function updateViewTabs() {
|
|
604
|
+
const view = getActiveView();
|
|
605
|
+
const overviewTab = document.getElementById('tab-overview');
|
|
606
|
+
const sessionsTab = document.getElementById('tab-sessions');
|
|
607
|
+
if (overviewTab) overviewTab.setAttribute('aria-selected', String(view === 'overview'));
|
|
608
|
+
if (sessionsTab) sessionsTab.setAttribute('aria-selected', String(view === 'list'));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function requestSessionsFragment() {
|
|
612
|
+
const form = document.querySelector('[data-filter-form]');
|
|
613
|
+
const content = document.getElementById('dashboard-content');
|
|
614
|
+
if (!form || !content || typeof htmx === 'undefined') return;
|
|
615
|
+
const params = new URLSearchParams();
|
|
616
|
+
for (const [key, value] of collectFormEntries(form)) {
|
|
617
|
+
params.append(key, value);
|
|
618
|
+
}
|
|
619
|
+
const url = '/fragments/sessions' + (params.size > 0 ? '?' + params.toString() : '');
|
|
620
|
+
content.setAttribute('hx-get', url);
|
|
621
|
+
htmx.ajax('GET', url, { target: content, swap: 'innerHTML' });
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function switchView(view) {
|
|
625
|
+
const form = document.querySelector('[data-filter-form]');
|
|
626
|
+
const field = getViewField();
|
|
627
|
+
if (field) field.value = view === 'list' ? 'list' : '';
|
|
628
|
+
updateViewTabs();
|
|
629
|
+
if (form) {
|
|
630
|
+
saveFilters(form);
|
|
631
|
+
updateFiltersBadge();
|
|
632
|
+
}
|
|
633
|
+
requestSessionsFragment();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function syncViewCounts() {
|
|
637
|
+
const content = document.getElementById('dashboard-content');
|
|
638
|
+
const badge = document.getElementById('sessions-tab-count');
|
|
639
|
+
if (!content || !badge) return;
|
|
640
|
+
const countEl =
|
|
641
|
+
content.querySelector('.metric-sessions .metric-value') ||
|
|
642
|
+
content.querySelector('.session-collapse-count');
|
|
643
|
+
if (countEl) {
|
|
644
|
+
badge.textContent = countEl.textContent;
|
|
645
|
+
badge.hidden = false;
|
|
646
|
+
} else {
|
|
647
|
+
badge.hidden = true;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function countActiveFilters() {
|
|
652
|
+
const panel = document.getElementById('filter-panel');
|
|
653
|
+
if (!panel) return 0;
|
|
654
|
+
let count = 0;
|
|
655
|
+
for (const el of panel.querySelectorAll('select, input')) {
|
|
656
|
+
if (el.multiple) count += el.selectedOptions.length;
|
|
657
|
+
else if (el.value) count += 1;
|
|
658
|
+
}
|
|
659
|
+
return count;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function updateFiltersBadge() {
|
|
663
|
+
const badge = document.getElementById('filters-badge');
|
|
664
|
+
if (!badge) return;
|
|
665
|
+
const count = countActiveFilters();
|
|
666
|
+
badge.textContent = String(count);
|
|
667
|
+
badge.hidden = count === 0;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const FILTERS_OPEN_KEY = 'happenin-filters-open';
|
|
671
|
+
|
|
672
|
+
function toggleFilters() {
|
|
673
|
+
const btn = document.getElementById('filters-toggle');
|
|
674
|
+
const panel = document.getElementById('filter-panel');
|
|
675
|
+
if (!btn || !panel) return;
|
|
676
|
+
const next = btn.getAttribute('aria-expanded') !== 'true';
|
|
677
|
+
btn.setAttribute('aria-expanded', String(next));
|
|
678
|
+
panel.classList.toggle('open', next);
|
|
679
|
+
setItem(FILTERS_OPEN_KEY, next ? '1' : '');
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function initFilterPanel() {
|
|
683
|
+
const btn = document.getElementById('filters-toggle');
|
|
684
|
+
const panel = document.getElementById('filter-panel');
|
|
685
|
+
if (!btn || !panel) return;
|
|
686
|
+
const open = getItem(FILTERS_OPEN_KEY) === '1';
|
|
687
|
+
btn.setAttribute('aria-expanded', String(open));
|
|
688
|
+
panel.classList.toggle('open', open);
|
|
689
|
+
updateFiltersBadge();
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function collectFormEntries(form) {
|
|
693
|
+
const data = new FormData(form);
|
|
694
|
+
const entries = [];
|
|
695
|
+
for (const [key, value] of data.entries()) {
|
|
696
|
+
if (value) entries.push([key, String(value)]);
|
|
697
|
+
}
|
|
698
|
+
return entries;
|
|
699
|
+
}
|
|
700
|
+
|
|
423
701
|
function updateContentUrl() {
|
|
424
702
|
const form = document.querySelector('[data-filter-form]');
|
|
425
703
|
const content = document.getElementById('dashboard-content');
|
|
426
704
|
if (!form || !content) return;
|
|
427
|
-
const data = new FormData(form);
|
|
428
705
|
const params = new URLSearchParams();
|
|
429
|
-
for (const [key, value] of
|
|
430
|
-
|
|
706
|
+
for (const [key, value] of collectFormEntries(form)) {
|
|
707
|
+
params.append(key, value);
|
|
431
708
|
}
|
|
432
709
|
content.setAttribute('hx-get', '/fragments/sessions' + (params.size > 0 ? '?' + params.toString() : ''));
|
|
433
710
|
}
|
|
434
711
|
|
|
435
712
|
function saveFilters(form) {
|
|
436
|
-
const data = new FormData(form);
|
|
437
713
|
const filters = {};
|
|
438
|
-
for (const [key, value] of
|
|
439
|
-
if (
|
|
714
|
+
for (const [key, value] of collectFormEntries(form)) {
|
|
715
|
+
if (key in filters) {
|
|
716
|
+
filters[key] = Array.isArray(filters[key]) ? [...filters[key], value] : [filters[key], value];
|
|
717
|
+
} else {
|
|
718
|
+
filters[key] = value;
|
|
719
|
+
}
|
|
440
720
|
}
|
|
441
721
|
setItem(FILTER_KEY, JSON.stringify(filters));
|
|
442
722
|
}
|
|
@@ -445,15 +725,23 @@ ${Dt(d)}
|
|
|
445
725
|
const form = document.querySelector('[data-filter-form]');
|
|
446
726
|
const content = document.getElementById('dashboard-content');
|
|
447
727
|
if (!form || !content) return;
|
|
448
|
-
|
|
449
|
-
|
|
728
|
+
initMultiSelects();
|
|
729
|
+
const hadSaved = window.location.search ? false : loadFilters();
|
|
730
|
+
refreshMultiSelects();
|
|
731
|
+
form.addEventListener('change', () => {
|
|
732
|
+
saveFilters(form);
|
|
733
|
+
updateFiltersBadge();
|
|
734
|
+
updateViewTabs();
|
|
735
|
+
refreshMultiSelects();
|
|
736
|
+
});
|
|
450
737
|
form.addEventListener('input', (event) => {
|
|
451
738
|
if (event.target.classList.contains('search')) saveFilters(form);
|
|
452
739
|
});
|
|
453
740
|
updateContentUrl();
|
|
741
|
+
initFilterPanel();
|
|
454
742
|
if (hadSaved) {
|
|
455
743
|
setTimeout(() => {
|
|
456
|
-
|
|
744
|
+
requestSessionsFragment();
|
|
457
745
|
}, 0);
|
|
458
746
|
}
|
|
459
747
|
}
|
|
@@ -488,8 +776,7 @@ ${Dt(d)}
|
|
|
488
776
|
}
|
|
489
777
|
|
|
490
778
|
function backToDashboard() {
|
|
491
|
-
|
|
492
|
-
if (form && typeof htmx !== 'undefined') htmx.trigger(form, 'change');
|
|
779
|
+
requestSessionsFragment();
|
|
493
780
|
}
|
|
494
781
|
|
|
495
782
|
function syncDetailState() {
|
|
@@ -499,34 +786,13 @@ ${Dt(d)}
|
|
|
499
786
|
document.body.classList.toggle('session-detail-open', !!detail);
|
|
500
787
|
}
|
|
501
788
|
|
|
502
|
-
function
|
|
789
|
+
function syncSessionsListState() {
|
|
503
790
|
const content = document.getElementById('dashboard-content');
|
|
504
|
-
|
|
505
|
-
if (!content || !btn) return;
|
|
791
|
+
if (!content) return;
|
|
506
792
|
const isList = !!content.querySelector('.sessions-list-view');
|
|
507
|
-
btn.setAttribute('aria-expanded', isList ? 'true' : 'false');
|
|
508
793
|
document.body.classList.toggle('sessions-list-open', isList);
|
|
509
794
|
}
|
|
510
795
|
|
|
511
|
-
function toggleSessionsList() {
|
|
512
|
-
const btn = document.getElementById('sessions-toggle');
|
|
513
|
-
const form = document.querySelector('[data-filter-form]');
|
|
514
|
-
if (!btn) return;
|
|
515
|
-
const isOpen = btn.getAttribute('aria-expanded') === 'true';
|
|
516
|
-
const params = new URLSearchParams();
|
|
517
|
-
if (form) {
|
|
518
|
-
const data = new FormData(form);
|
|
519
|
-
for (const [key, value] of data.entries()) {
|
|
520
|
-
if (value) params.set(key, String(value));
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
if (!isOpen) params.set('view', 'list');
|
|
524
|
-
const url = '/fragments/sessions' + (params.size > 0 ? '?' + params.toString() : '');
|
|
525
|
-
if (typeof htmx !== 'undefined') {
|
|
526
|
-
htmx.ajax('GET', url, { target: '#dashboard-content', swap: 'innerHTML', source: btn });
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
796
|
document.addEventListener('htmx:beforeSwap', (e) => {
|
|
531
797
|
const elt = e.detail.requestConfig ? e.detail.requestConfig.elt : null;
|
|
532
798
|
const blocked =
|
|
@@ -542,22 +808,34 @@ ${Dt(d)}
|
|
|
542
808
|
window.copySessionJson = copySessionJson;
|
|
543
809
|
window.copyEventJson = copyEventJson;
|
|
544
810
|
window.backToDashboard = backToDashboard;
|
|
545
|
-
window.
|
|
811
|
+
window.switchView = switchView;
|
|
812
|
+
window.toggleFilters = toggleFilters;
|
|
546
813
|
|
|
547
814
|
document.addEventListener('htmx:afterSettle', () => {
|
|
548
815
|
updateContentUrl();
|
|
549
816
|
syncDetailState();
|
|
550
|
-
|
|
817
|
+
syncSessionsListState();
|
|
818
|
+
updateViewTabs();
|
|
819
|
+
syncViewCounts();
|
|
551
820
|
});
|
|
552
821
|
|
|
822
|
+
function initHistoryState() {
|
|
823
|
+
syncDetailState();
|
|
824
|
+
syncSessionsListState();
|
|
825
|
+
updateViewTabs();
|
|
826
|
+
syncViewCounts();
|
|
827
|
+
}
|
|
828
|
+
|
|
553
829
|
applyTheme(getTheme());
|
|
554
830
|
|
|
555
831
|
if (document.readyState === 'loading') {
|
|
556
832
|
document.addEventListener('DOMContentLoaded', initTheme);
|
|
557
833
|
document.addEventListener('DOMContentLoaded', initFilters);
|
|
834
|
+
document.addEventListener('DOMContentLoaded', initHistoryState);
|
|
558
835
|
} else {
|
|
559
836
|
initTheme();
|
|
560
837
|
initFilters();
|
|
838
|
+
initHistoryState();
|
|
561
839
|
}
|
|
562
840
|
})();
|
|
563
841
|
<\/script>
|
|
@@ -581,6 +859,8 @@ ${Dt(d)}
|
|
|
581
859
|
--success: #22c55e;
|
|
582
860
|
--warning: #eab308;
|
|
583
861
|
--danger: #ef4444;
|
|
862
|
+
--shadow: 0 1px 2px rgba(0,0,0,0.3), 0 4px 12px rgba(0,0,0,0.22);
|
|
863
|
+
--shadow-sm: 0 1px 2px rgba(0,0,0,0.28);
|
|
584
864
|
}
|
|
585
865
|
[data-theme="light"] {
|
|
586
866
|
color-scheme: light;
|
|
@@ -601,39 +881,77 @@ ${Dt(d)}
|
|
|
601
881
|
--success: #16a34a;
|
|
602
882
|
--warning: #ca8a04;
|
|
603
883
|
--danger: #dc2626;
|
|
884
|
+
--shadow: 0 1px 2px rgba(15,23,42,0.04), 0 4px 12px rgba(15,23,42,0.06);
|
|
885
|
+
--shadow-sm: 0 1px 2px rgba(15,23,42,0.05);
|
|
604
886
|
}
|
|
605
887
|
* { box-sizing: border-box; }
|
|
606
888
|
html, body { min-height: 100%; }
|
|
607
889
|
body { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 0; line-height: 1.5; }
|
|
608
|
-
.app { display: flex; flex-direction: column; min-height: 100%; max-width: 1400px; margin: 0 auto; padding: 1.
|
|
609
|
-
.sticky-bar { flex: 0 0 auto; display: flex; flex-direction: column; gap:
|
|
610
|
-
.app-header { display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap; padding: 0.
|
|
611
|
-
.brand { font-weight: 800; font-size: 1.
|
|
890
|
+
.app { display: flex; flex-direction: column; min-height: 100%; max-width: 1400px; margin: 0 auto; padding: 1.75rem; gap: 1.5rem; }
|
|
891
|
+
.sticky-bar { flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.85rem; background: var(--bg); position: sticky; top: 0; z-index: 10; padding-bottom: 0.25rem; }
|
|
892
|
+
.app-header { display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap; padding: 0.85rem 1.1rem; background: var(--surface); border: 1px solid var(--border); border-radius: 0.85rem; box-shadow: var(--shadow-sm); }
|
|
893
|
+
.brand { font-weight: 800; font-size: 1.3rem; display: flex; align-items: center; gap: 0.5rem; color: var(--text); background: linear-gradient(135deg, var(--text), var(--text) 40%, var(--accent)); -webkit-background-clip: text; background-clip: text; }
|
|
612
894
|
.brand-icon { color: var(--accent); }
|
|
613
|
-
.page-title { font-size: 1.
|
|
895
|
+
.page-title { font-size: 1.05rem; font-weight: 600; margin: 0; color: var(--text-2); }
|
|
614
896
|
.header-controls { display: flex; gap: 0.75rem; align-items: end; }
|
|
615
897
|
.search-wrap { position: relative; display: flex; align-items: center; }
|
|
616
898
|
.search-icon { position: absolute; left: 0.6rem; color: var(--text-4); pointer-events: none; }
|
|
617
899
|
input.search { padding-left: 2rem; min-width: 14rem; }
|
|
618
|
-
|
|
619
|
-
form { margin: 0; display: flex; flex-direction: column; gap: 1rem; }
|
|
900
|
+
form { margin: 0; display: flex; flex-direction: column; gap: 0.85rem; }
|
|
620
901
|
label { display: flex; flex-direction: column; font-size: 0.75rem; gap: 0.25rem; color: var(--text-2); }
|
|
621
|
-
input, select, button { background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: 0.4rem; padding: 0.45rem 0.7rem; font-size: 0.85rem; transition: border-color 0.15s, background 0.15s; }
|
|
902
|
+
input, select, button { background: var(--surface-2); color: var(--text); border: 1px solid var(--border); border-radius: 0.4rem; padding: 0.45rem 0.7rem; font-size: 0.85rem; transition: border-color 0.15s, background 0.15s, transform 0.1s; }
|
|
622
903
|
input:focus, select:focus, button:focus { outline: none; border-color: var(--accent); }
|
|
623
904
|
select { min-width: 7.5rem; cursor: pointer; }
|
|
905
|
+
select[multiple] { min-width: 10rem; padding: 0.3rem; cursor: default; }
|
|
906
|
+
select[multiple] option { padding: 0.15rem 0.4rem; border-radius: 0.25rem; }
|
|
907
|
+
select[multiple] option:checked { background: var(--accent); color: var(--bg); }
|
|
624
908
|
button { cursor: pointer; background: var(--surface-4); font-weight: 500; }
|
|
625
909
|
button:hover { background: var(--surface-5); }
|
|
910
|
+
button:active { transform: scale(0.97); }
|
|
626
911
|
.theme { display: inline-flex; align-items: center; justify-content: center; font-size: 1.25rem; padding: 0.35rem 0.6rem; line-height: 1; }
|
|
627
|
-
.
|
|
628
|
-
.
|
|
629
|
-
.
|
|
912
|
+
.toolbar-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
|
|
913
|
+
.toggle-pill { display: inline-flex; align-items: center; gap: 0.4rem; background: var(--surface); border: 1px solid var(--border); border-radius: 999px; padding: 0.5rem 1rem; font-size: 0.85rem; font-weight: 600; color: var(--text-2); box-shadow: var(--shadow-sm); }
|
|
914
|
+
.toggle-pill:hover { background: var(--surface-3); color: var(--text); }
|
|
915
|
+
.toggle-pill[aria-expanded="true"] { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); background: color-mix(in srgb, var(--accent) 12%, var(--surface)); }
|
|
916
|
+
.toggle-chevron { transition: transform 0.18s ease; flex: 0 0 auto; }
|
|
917
|
+
.toggle-pill[aria-expanded="true"] .toggle-chevron { transform: rotate(90deg); }
|
|
918
|
+
.toggle-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 1.15rem; height: 1.15rem; padding: 0 0.3rem; border-radius: 999px; background: var(--accent); color: var(--bg); font-size: 0.68rem; font-weight: 700; }
|
|
919
|
+
.view-tabs { display: flex; align-items: center; gap: 0.15rem; background: var(--surface); border: 1px solid var(--border); border-radius: 999px; padding: 0.25rem; box-shadow: var(--shadow-sm); }
|
|
920
|
+
.view-tab { border: none; background: transparent; color: var(--text-2); border-radius: 999px; padding: 0.42rem 1rem; font-size: 0.85rem; font-weight: 600; display: inline-flex; align-items: center; gap: 0.4rem; }
|
|
921
|
+
.view-tab:hover { color: var(--text); background: var(--surface-3); }
|
|
922
|
+
.view-tab[aria-selected="true"] { background: var(--accent); color: var(--bg); }
|
|
923
|
+
.view-tab[aria-selected="true"]:hover { background: var(--accent); }
|
|
924
|
+
.view-tab .toggle-badge { background: color-mix(in srgb, var(--text) 15%, transparent); color: var(--text-2); }
|
|
925
|
+
.view-tab[aria-selected="true"] .toggle-badge { background: rgba(0,0,0,0.22); color: var(--bg); }
|
|
926
|
+
.filter-panel { display: none; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: 1.1rem 1.25rem; align-items: start; padding: 1.15rem 1.25rem; background: var(--surface); border: 1px solid var(--border); border-radius: 0.85rem; box-shadow: var(--shadow-sm); animation: filterPanelIn 0.15s ease; }
|
|
927
|
+
.filter-panel.open { display: grid; }
|
|
928
|
+
.filter-panel .field { display: flex; flex-direction: column; gap: 0.4rem; min-width: 0; }
|
|
929
|
+
.filter-panel .field-wide { grid-column: span 2; min-width: 15rem; }
|
|
930
|
+
.filter-panel .field label { display: block; font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-3); }
|
|
931
|
+
.filter-panel .field select, .filter-panel .field input { width: 100%; }
|
|
932
|
+
@keyframes filterPanelIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
|
|
933
|
+
.ms { position: relative; display: flex; flex-direction: column; gap: 0.4rem; }
|
|
934
|
+
.ms select.ms-native { display: block; }
|
|
935
|
+
.ms.enhanced select.ms-native { display: none; }
|
|
936
|
+
.ms-pills { display: flex; flex-wrap: wrap; gap: 0.35rem; min-height: 2.1rem; padding: 0.3rem; background: var(--surface-2); border: 1px solid var(--border); border-radius: 0.5rem; align-items: center; }
|
|
937
|
+
.ms-pill { display: inline-flex; align-items: center; gap: 0.3rem; max-width: 100%; background: color-mix(in srgb, var(--accent) 16%, var(--surface-4)); border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border)); color: var(--text); border-radius: 999px; padding: 0.2rem 0.35rem 0.2rem 0.65rem; font-size: 0.75rem; font-weight: 600; line-height: 1.4; }
|
|
938
|
+
.ms-pill span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 10rem; }
|
|
939
|
+
.ms-pill button { all: unset; display: inline-flex; align-items: center; justify-content: center; width: 1.1rem; height: 1.1rem; border-radius: 50%; color: var(--text-2); font-size: 0.85rem; line-height: 1; cursor: pointer; }
|
|
940
|
+
.ms-pill button:hover { background: rgba(0,0,0,0.18); color: var(--text); }
|
|
941
|
+
.ms-pill--all { background: transparent; border-style: dashed; border-color: var(--border); color: var(--text-4); font-weight: 500; font-style: italic; }
|
|
942
|
+
.ms-search-wrap { position: relative; }
|
|
943
|
+
.ms-search { width: 100%; font-size: 0.78rem; padding: 0.4rem 0.6rem; }
|
|
944
|
+
.ms-dropdown { position: absolute; top: calc(100% + 0.3rem); left: 0; right: 0; z-index: 30; max-height: 12rem; overflow-y: auto; background: var(--surface-3); border: 1px solid var(--border); border-radius: 0.5rem; box-shadow: var(--shadow); padding: 0.3rem; }
|
|
945
|
+
.ms-option { padding: 0.4rem 0.6rem; border-radius: 0.3rem; font-size: 0.8rem; color: var(--text-2); cursor: pointer; }
|
|
946
|
+
.ms-option:hover { background: var(--surface-5); color: var(--text); }
|
|
947
|
+
.ms-empty { padding: 0.4rem 0.6rem; font-size: 0.75rem; color: var(--text-4); }
|
|
630
948
|
.dashboard-content-area { flex: 1 1 auto; }
|
|
631
949
|
.session-list-title { display: flex; align-items: center; gap: 0.5rem; font-size: 0.95rem; font-weight: 600; color: var(--text); margin: 0 0 0.5rem; }
|
|
632
950
|
.session-collapse-count { color: var(--text-4); font-weight: 500; }
|
|
633
951
|
.sessions-list-filter { display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--text-2); background: var(--surface-2); border: 1px solid var(--border); border-radius: 0.4rem; padding: 0.5rem 0.75rem; margin-bottom: 0.75rem; }
|
|
634
952
|
.sessions-list-filter a { color: var(--accent); text-decoration: none; }
|
|
635
953
|
.sessions-list-filter a:hover { text-decoration: underline; }
|
|
636
|
-
.session-list-wrapper { display: flex; flex-direction: column; background: var(--surface); border: 1px solid var(--border); border-radius: 0.
|
|
954
|
+
.session-list-wrapper { display: flex; flex-direction: column; background: var(--surface); border: 1px solid var(--border); border-radius: 0.85rem; overflow: hidden; box-shadow: var(--shadow-sm); }
|
|
637
955
|
.session-list { list-style: none; padding: 0; margin: 0; }
|
|
638
956
|
.status-badge { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; padding: 0.15rem 0.45rem; border-radius: 0.25rem; }
|
|
639
957
|
.status-active { background: rgba(34,197,94,0.15); color: var(--success); }
|
|
@@ -673,7 +991,8 @@ button:hover { background: var(--surface-5); }
|
|
|
673
991
|
.main-metrics .metric-card { min-height: 5rem; }
|
|
674
992
|
.main-metrics .metric-value { font-size: 1.65rem; }
|
|
675
993
|
.top-charts { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
|
|
676
|
-
.chart-panel { background: var(--surface); border: 1px solid var(--border); border-radius: 0.
|
|
994
|
+
.chart-panel { background: var(--surface); border: 1px solid var(--border); border-radius: 0.85rem; padding: 1.1rem; display: flex; flex-direction: column; min-height: 18rem; box-shadow: var(--shadow-sm); transition: box-shadow 0.15s, transform 0.15s; }
|
|
995
|
+
.chart-panel:hover { box-shadow: var(--shadow); transform: translateY(-1px); }
|
|
677
996
|
.chart-title { font-size: 0.9rem; font-weight: 600; color: var(--text-2); margin: 0 0 0.75rem; flex: 0 0 auto; }
|
|
678
997
|
.chart-empty { color: var(--text-4); text-align: center; padding: 4rem 0; }
|
|
679
998
|
.chart-max { text-align: right; color: var(--text-4); font-size: 0.7rem; margin-top: 0.25rem; flex: 0 0 auto; }
|
|
@@ -694,9 +1013,9 @@ a.tool-row:hover .tool-name { color: var(--accent); }
|
|
|
694
1013
|
.tool-bar-bg { background: var(--surface-4); border-radius: 0.4rem; height: 0.85rem; overflow: hidden; }
|
|
695
1014
|
.tool-bar { background: linear-gradient(90deg, var(--accent), var(--accent-2)); height: 100%; border-radius: 0.4rem; }
|
|
696
1015
|
.tool-count { color: var(--text-3); text-align: right; font-weight: 600; }
|
|
697
|
-
.detail-area { background: var(--surface); border: 1px solid var(--border); border-radius: 0.
|
|
1016
|
+
.detail-area { background: var(--surface); border: 1px solid var(--border); border-radius: 0.85rem; padding: 1rem; box-shadow: var(--shadow-sm); }
|
|
698
1017
|
.detail-area > .empty { color: var(--text-4); text-align: center; padding: 4rem 0; }
|
|
699
|
-
.session-detail-view { display: flex; flex-direction: column; gap: 0.75rem; background: var(--surface); border: 1px solid var(--border); border-radius: 0.
|
|
1018
|
+
.session-detail-view { display: flex; flex-direction: column; gap: 0.75rem; background: var(--surface); border: 1px solid var(--border); border-radius: 0.85rem; padding: 1.1rem; box-shadow: var(--shadow-sm); }
|
|
700
1019
|
.detail-header { display: flex; justify-content: space-between; align-items: center; gap: 0.75rem; flex: 0 0 auto; }
|
|
701
1020
|
.detail-back { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.8rem; padding: 0.35rem 0.6rem; flex: 0 0 auto; }
|
|
702
1021
|
.detail-title { font-size: 1rem; font-weight: 600; color: var(--text); word-break: break-all; flex: 1 1 auto; min-width: 0; margin: 0; }
|
|
@@ -749,7 +1068,8 @@ a.tool-row:hover .tool-name { color: var(--accent); }
|
|
|
749
1068
|
.pager button:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
750
1069
|
.pager-info { color: var(--text-3); font-size: 0.85rem; }
|
|
751
1070
|
.metric-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; }
|
|
752
|
-
.metric-card { position: relative; overflow: hidden; background: var(--surface-2); border: 1px solid var(--border); border-radius: 0.
|
|
1071
|
+
.metric-card { position: relative; overflow: hidden; background: var(--surface-2); border: 1px solid var(--border); border-radius: 0.75rem; padding: 0.9rem 1.1rem; display: flex; flex-direction: column; justify-content: center; box-shadow: var(--shadow-sm); transition: box-shadow 0.15s, transform 0.15s; }
|
|
1072
|
+
.metric-card:hover { box-shadow: var(--shadow); transform: translateY(-1px); }
|
|
753
1073
|
.metric-sessions .metric-icon { color: var(--accent); }
|
|
754
1074
|
.metric-events .metric-icon { color: var(--accent-2); }
|
|
755
1075
|
.metric-duration .metric-icon { color: var(--warning); }
|
|
@@ -770,7 +1090,7 @@ a.tool-row:hover .tool-name { color: var(--accent); }
|
|
|
770
1090
|
.context-legend-value { color: var(--text-3); font-weight: 600; }
|
|
771
1091
|
.empty { color: var(--text-4); text-align: center; padding: 2rem; }
|
|
772
1092
|
@media (max-width: 960px) { .top-charts { grid-template-columns: 1fr; } .metric-grid { grid-template-columns: repeat(2, 1fr); } .main-metrics .metric-grid { grid-template-columns: repeat(2, 1fr); } .detail-attrs { grid-template-columns: repeat(2, 1fr); } .context-bars { grid-template-columns: 1fr; } }
|
|
773
|
-
@media (max-width: 640px) { .app { padding: 0.75rem; } .app-header { flex-direction: column; align-items: flex-start; } .header-controls, .filter-
|
|
1093
|
+
@media (max-width: 640px) { .app { padding: 0.75rem; } .app-header { flex-direction: column; align-items: flex-start; } .header-controls, .filter-panel, .toolbar-row { width: 100%; } .filter-panel { grid-template-columns: 1fr; } .filter-panel .field-wide { grid-column: span 1; } .view-tabs { flex: 1 1 auto; justify-content: center; } input.search { min-width: 0; width: 100%; } .search-wrap { width: 100%; } .metric-grid { grid-template-columns: 1fr; } .main-metrics .metric-grid { grid-template-columns: 1fr; } .detail-attrs { grid-template-columns: 1fr; } .detail-toolbar input[type="search"] { min-width: 0; width: 100%; } }
|
|
774
1094
|
</style>
|
|
775
1095
|
<script src="https://unpkg.com/htmx.org@2.0.10/dist/htmx.min.js" integrity="sha256-cepnGFv6jJjDnTFxfG/OXYUjcPzf0SnbRUN3TTFFwN4=" crossorigin="anonymous"><\/script>
|
|
776
1096
|
<script src="https://unpkg.com/htmx-ext-sse@2.2.4/dist/sse.js" integrity="sha256-O1mSpUFhm6vvxMFpUFr0dN9cMDnaUeWblsz5JB7NYdI=" crossorigin="anonymous"><\/script>
|
|
@@ -779,23 +1099,21 @@ a.tool-row:hover .tool-name { color: var(--accent); }
|
|
|
779
1099
|
<div class="app">
|
|
780
1100
|
<div class="sticky-bar">
|
|
781
1101
|
<form data-filter-form hx-get="/fragments/sessions" hx-target="#dashboard-content" hx-swap="innerHTML" hx-trigger="change, input changed delay:300ms from:.search">
|
|
782
|
-
${
|
|
1102
|
+
<input type="hidden" name="view" value="${t.view===`list`?`list`:``}">
|
|
1103
|
+
${i}${a}
|
|
783
1104
|
</form>
|
|
784
1105
|
</div>
|
|
785
|
-
<button type="button" id="sessions-toggle" class="sessions-toggle-bar" aria-expanded="false" onclick="toggleSessionsList()">
|
|
786
|
-
<span class="sessions-toggle-arrow" aria-hidden="true">▸</span> Recent Sessions
|
|
787
|
-
</button>
|
|
788
1106
|
<div id="dashboard-content" class="dashboard-content-area" sse-connect="/events/stream" hx-get="/fragments/sessions" hx-target="#dashboard-content" hx-swap="innerHTML" hx-trigger="sse:message">
|
|
789
1107
|
${r}
|
|
790
1108
|
</div>
|
|
791
1109
|
</div>
|
|
792
1110
|
</body>
|
|
793
|
-
</html>`}function
|
|
1111
|
+
</html>`}function mn(e,t,n,r){let i=Y(n);t.writeHead(200,{"Content-Type":`text/html; charset=utf-8`}),t.end(pn(r,i))}let Z;const hn=new Set;function gn(e,t){t.writeHead(200,{"Content-Type":`text/event-stream`,"Cache-Control":`no-cache`,Connection:`keep-alive`}),t.setTimeout(0),t.write(`retry: 500
|
|
794
1112
|
|
|
795
|
-
`);let n=e.headers[`last-event-id`],r=Array.isArray(n)?n[0]:n,i=r?parseInt(r,10):NaN,a={res:t,lastId:Number.isNaN(i)?
|
|
1113
|
+
`);let n=e.headers[`last-event-id`],r=Array.isArray(n)?n[0]:n,i=r?parseInt(r,10):NaN,a={res:t,lastId:Number.isNaN(i)?Ve(Z):i,timer:null};a.timer=setInterval(()=>{try{let e=Ve(Z);e>a.lastId&&(t.write(`event: message
|
|
796
1114
|
data: ping
|
|
797
1115
|
|
|
798
|
-
`),a.lastId=e)}catch(e){console.error(`SSE poll error:`,e),clearInterval(a.timer),
|
|
1116
|
+
`),a.lastId=e)}catch(e){console.error(`SSE poll error:`,e),clearInterval(a.timer),hn.delete(a)}},300),hn.add(a),e.on(`close`,()=>{clearInterval(a.timer),hn.delete(a)})}function _n(e,t){let n=new URL(e.url??`/`,`http://localhost`);try{if(e.method===`GET`){if(n.pathname===`/`){mn(e,t,n,Z);return}if(n.pathname===`/fragments/sessions`){en(e,t,n,Z);return}if(n.pathname===`/fragments/detail`){nn(e,t,n,Z);return}if(n.pathname===`/events`){rn(e,t,n,Z);return}if(n.pathname===`/events/stream`){gn(e,t);return}}t.writeHead(404,{"Content-Type":`text/plain`}),t.end(`Not found`)}catch(e){console.error(`Dashboard request error:`,e),t.headersSent?t.writableEnded||t.destroy?.():(t.writeHead(500,{"Content-Type":`text/plain`}),t.end(`Internal server error`))}}function vn(e){p(c.platform===`win32`?`cmd`:c.platform===`darwin`?`open`:`xdg-open`,c.platform===`win32`?[`/c`,`start`,``,e]:[e],e=>{e&&console.error(`open failed:`,e.message,`(use --no-open to suppress)`)})}async function yn(e,t,n=!1){let r=f.createServer((e,t)=>{_n(e,t)});await new Promise((i,a)=>{let o=e=>{function s(t){r.off(`error`,s),r.off(`listening`,c),t.code===`EADDRINUSE`&&e<65535?o(e+1):a(t)}function c(){r.off(`error`,s);let a=`http://localhost:${e}`;n||console.log(`Dashboard: ${a}`),t&&vn(a),i()}r.once(`error`,s),r.once(`listening`,c),r.listen(e,`127.0.0.1`)};o(e)})}async function bn(e){let t=8765,n=!0,r=!1,i=!1;for(let a of e){if(i){let e=parseInt(a,10);(Number.isNaN(e)||e<1||e>65535)&&(console.error(`Invalid --port`),c.exit(1)),t=e,i=!1;continue}if(a===`--port`){i=!0;continue}if(a.startsWith(`--port=`)){let e=parseInt(a.slice(7),10);(Number.isNaN(e)||e<1||e>65535)&&(console.error(`Invalid --port`),c.exit(1)),t=e}else a===`--no-open`?n=!1:a===`--silent`&&(r=!0,n=!1)}i&&(console.error(`Missing value for --port`),c.exit(1)),await pt(),Z=E(T()),await yn(t,n,r)}const xn=[`json`,`jsonl`,`summary`];function Q(e){let t=e.trim();if(t===``)return;let n=Number(t);if(!(!Number.isFinite(n)||!Number.isInteger(n)||n<0))return n}function $(e){let t=e.trim();if(t===``)return;let n=Number(t);if(!(!Number.isFinite(n)||n<0))return n}const Sn=[`24h`,`7d`,`30d`,`all`];function Cn(){c.stdout.write(`Usage: happenin query [options]
|
|
799
1117
|
|
|
800
1118
|
Options:
|
|
801
1119
|
--source <source> filter by source (cursor, claude, ...)
|
|
@@ -810,7 +1128,7 @@ Options:
|
|
|
810
1128
|
--format <format> output format: json (default), jsonl, summary
|
|
811
1129
|
--db <path> database path (default: HAPPENIN_DB or ~/.happenin/happenin.db)
|
|
812
1130
|
-h, --help show this help
|
|
813
|
-
`)}function
|
|
1131
|
+
`)}function wn(e){let t={},n=`json`,r,i=!1,a;for(let o of e){if(a){switch(a){case`source`:t.source=o;break;case`event`:t.event=o;break;case`session`:t.sessionId=o;break;case`q`:t.q=o;break;case`since`:{let e=Q(o);e!==void 0&&(t.since=e);break}case`range`:Sn.includes(o)&&(t.range=o);break;case`status`:(o===`active`||o===`completed`||o===`failed`)&&(t.status=o);break;case`tool`:t.tool=o;break;case`minDuration`:{let e=$(o);e!==void 0&&(t.minDuration=e);break}case`maxDuration`:{let e=$(o);e!==void 0&&(t.maxDuration=e);break}case`limit`:{let e=Q(o);e!==void 0&&(t.limit=e);break}case`offset`:{let e=Q(o);e!==void 0&&(t.offset=e);break}case`format`:xn.includes(o)&&(n=o);break;case`db`:r=o;break}a=void 0;continue}if(o===`-h`||o===`--help`){i=!0;continue}if(o===`--source`||o===`--event`||o===`--session`||o===`--q`||o===`--since`||o===`--range`||o===`--status`||o===`--tool`||o===`--minDuration`||o===`--maxDuration`||o===`--limit`||o===`--offset`||o===`--format`||o===`--db`){a=o.slice(2);continue}if(o.startsWith(`--`)){let e=o.indexOf(`=`);if(e!==-1){let i=o.slice(2,e),a=o.slice(e+1);switch(i){case`source`:t.source=a;break;case`event`:t.event=a;break;case`session`:t.sessionId=a;break;case`q`:t.q=a;break;case`since`:{let e=Q(a);e!==void 0&&(t.since=e);break}case`range`:Sn.includes(a)&&(t.range=a);break;case`status`:(a===`active`||a===`completed`||a===`failed`)&&(t.status=a);break;case`tool`:t.tool=a;break;case`minDuration`:{let e=$(a);e!==void 0&&(t.minDuration=e);break}case`maxDuration`:{let e=$(a);e!==void 0&&(t.maxDuration=e);break}case`limit`:{let e=Q(a);e!==void 0&&(t.limit=e);break}case`offset`:{let e=Q(a);e!==void 0&&(t.offset=e);break}case`format`:xn.includes(a)&&(n=a);break;case`db`:r=a;break}}}}return{options:{range:`24h`,...t},format:n,dbPath:r,help:i}}async function Tn(e,t,n,r=!1){let{options:i,format:a,dbPath:o,help:s}=wn(e);if(s){t();return}r||(delete i.status,delete i.minDuration,delete i.maxDuration);let c=E(o);try{await n(c,i,a)}finally{c.close()}}async function En(e){await Tn(e,Cn,(e,t,n)=>{if(n===`summary`){let n=Oe(e,t);c.stdout.write(`${JSON.stringify(n,null,2)}\n`);return}let r={total:A(e,t),rows:De(e,t).map(Nt)};if(n===`jsonl`)for(let e of r.rows)c.stdout.write(`${JSON.stringify(e)}\n`);else c.stdout.write(`${JSON.stringify(r,null,2)}\n`)})}function Dn(){c.stdout.write(`Usage: happenin sessions [options]
|
|
814
1132
|
|
|
815
1133
|
Options:
|
|
816
1134
|
--source <source> filter by source (cursor, claude, ...)
|
|
@@ -828,8 +1146,8 @@ Options:
|
|
|
828
1146
|
--format <format> output format: json (default), jsonl, summary
|
|
829
1147
|
--db <path> database path (default: HAPPENIN_DB or ~/.happenin/happenin.db)
|
|
830
1148
|
-h, --help show this help
|
|
831
|
-
`)}function
|
|
832
|
-
`)}async function
|
|
1149
|
+
`)}function On(e){return e.map(e=>{let t=`${Math.floor(e.durationMs/1e3)}s`,n=e.sessionId??`no-session`,r=e.firstAt??`?`,i=e.lastAt??`?`,a=e.projectPath??`-`,o=e.tools.join(`,`)||`-`,s=e.skills.join(`,`)||`-`,c=e.files.join(`,`)||`-`;return`${n}\t${r}\t${i}\t${t}\t${e.eventCount} events\tproject:${a}\ttools:${o}\tskills:${s}\tfiles:${c}\tfailures:${e.failureCount}`}).join(`
|
|
1150
|
+
`)}async function kn(e){await Tn(e,Dn,(e,t,n)=>{let r=j(e,t);if(n===`jsonl`){for(let e of r)c.stdout.write(`${JSON.stringify(e)}\n`);return}if(n===`summary`){c.stdout.write(`${On(r)}\n`);return}c.stdout.write(`${JSON.stringify(r,null,2)}\n`)},!0)}function An(){let t=s.dirname(l(import.meta.url)),n=t;for(;;){let t=s.dirname(n);if(t===n)break;if(e(s.join(n,`package.json`)))return n;n=t}return t}const jn=An(),Mn=s.join(jn,`assets`,`help.md`),Nn=s.join(jn,`package.json`);function Pn(e){let t=!!c.stdout.isTTY&&!c.env.NO_COLOR,n=t?`\x1B[1m`:``,r=t?`\x1B[2m`:``,i=t?`\x1B[36m`:``,a=t?`\x1B[0m`:``;return e.replace(/^# (.+)$/gm,`${n}$1${a}`).replace(/^## (.+)$/gm,`${n}$1${a}`).replace(/^### `(.+)`$/gm,`${n}$1${a}`).replace(/^### (.+)$/gm,`${n}$1${a}`).replace(/^#### (.+)$/gm,`${r}$1${a}`).replace(/^- /gm,` • `).replace(/`([^`]+)`/g,`${i}$1${a}`)}function Fn(){let e;try{e=n(Mn,`utf8`)}catch{e=`# happenin
|
|
833
1151
|
|
|
834
1152
|
Run \`happenin <command>\`.
|
|
835
|
-
`}c.stdout.write(`\n${
|
|
1153
|
+
`}c.stdout.write(`\n${Pn(e)}\n`)}function In(){let e=`0.0.0`;try{let t=n(Nn,`utf8`);e=JSON.parse(t).version??e}catch{}c.stdout.write(`${e}\n`)}async function Ln(){let[e,...t]=c.argv.slice(2);if(!e||e===`--help`||e===`-h`){Fn();return}if(e===`--version`||e===`-v`){In();return}if(e===`install`){await he(t);return}if(e===`record`){await $e(t);return}if(e===`import`){await pt(t);return}if(e===`dashboard`){await bn(t);return}if(e===`query`){await En(t);return}if(e===`sessions`){await kn(t);return}c.stderr.write(`Unknown command: ${e}\n`),Fn(),c.exitCode=1}await Ln();export{};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "happenin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "A CLI that records Cursor and Claude Code agent events to a local SQLite database and serves a live browser dashboard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
7
7
|
"ai-agents",
|
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
"cli",
|
|
11
11
|
"cursor",
|
|
12
12
|
"dashboard",
|
|
13
|
+
"linux",
|
|
13
14
|
"logging",
|
|
14
15
|
"macos",
|
|
15
|
-
"sqlite"
|
|
16
|
+
"sqlite",
|
|
17
|
+
"windows"
|
|
16
18
|
],
|
|
17
19
|
"homepage": "https://github.com/YogliB/happenin#readme",
|
|
18
20
|
"bugs": {
|