crumbtrail-node 0.1.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/LICENSE +21 -0
- package/README.md +242 -0
- package/dist/bug-widget-LGIRLS76-HL62OAMR.js +201 -0
- package/dist/chunk-EASSXKUJ.js +18920 -0
- package/dist/chunk-QGM4M3NI.js +37 -0
- package/dist/cli.cjs +233486 -0
- package/dist/cli.d.cts +21 -0
- package/dist/cli.d.ts +21 -0
- package/dist/cli.js +214747 -0
- package/dist/index.cjs +21337 -0
- package/dist/index.d.cts +2798 -0
- package/dist/index.d.ts +2798 -0
- package/dist/index.js +2519 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Crumbtrail contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# crumbtrail-node
|
|
2
|
+
|
|
3
|
+
Local Crumbtrail HTTP server, MCP server, Express middleware, and package CLI.
|
|
4
|
+
|
|
5
|
+
This package is the local self-host runtime boundary for Crumbtrail. It owns the server process that receives session events, writes local artifacts, post-processes sessions, and exposes MCP-readable evidence.
|
|
6
|
+
|
|
7
|
+
## Runtime boundary
|
|
8
|
+
|
|
9
|
+
The package runtime entrypoint is the built CLI binary:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
crumbtrail-server --host 127.0.0.1 --port 9898 --output ~/.crumbtrail/sessions
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
In this repository, the same boundary is exercised from built output with:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm --filter crumbtrail-node verify:package-runtime
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The verifier builds `crumbtrail-node`, starts `dist/cli.cjs` from a temporary runtime directory, probes `GET /health`, verifies static file serving, checks safe startup diagnostics, checks degraded health when the output directory becomes unavailable, and shuts the process down. A passing run prints:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
CRUMBTRAIL_PACKAGE_RUNTIME_PASS cli=dist/cli.cjs ...
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Local configuration contract
|
|
28
|
+
|
|
29
|
+
| Flag | Default | Validation | Purpose |
|
|
30
|
+
| ----------------------- | ---------------------------------: | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
31
|
+
| `--host` | `127.0.0.1` | Must be non-empty. | Interface for the local server to bind. |
|
|
32
|
+
| `--port` | `9898` | Must be an integer from 1 to 65535. | HTTP port. |
|
|
33
|
+
| `--output` | `~/.crumbtrail/sessions` | Must be a non-empty local path. | Directory where session artifacts are written. |
|
|
34
|
+
| `--static` | unset | If set, path must exist and be a directory. | Optional static directory to serve alongside API/session routes. |
|
|
35
|
+
| `--allow-origin` | localhost origins only | Must be an `http` or `https` origin containing only scheme, host, and optional port. Repeatable. | Additional browser origins allowed by CORS. |
|
|
36
|
+
| `--auth-token` | unset (or `CRUMBTRAIL_AUTH_TOKEN`) | Presence is reported, but token content is never logged. | Optional token required for `/api/*` routes. The `--auth-token` flag wins; otherwise a non-blank `CRUMBTRAIL_AUTH_TOKEN` env var is used. |
|
|
37
|
+
| `--mcp` | `false` | Boolean flag. | Run MCP server mode against the output directory instead of HTTP mode. |
|
|
38
|
+
| `--ai` | `false` | Boolean flag. | Opt into AI diagnosis after finalization. |
|
|
39
|
+
| `--ai-model` | unset | Parsed as an opaque model string. | Model override for AI diagnosis. |
|
|
40
|
+
| `--ai-allow-auto-model` | `false` | Boolean flag. | Allow provider auto-model selection. |
|
|
41
|
+
|
|
42
|
+
Invalid config fails before the server binds and prints a bounded message like:
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
crumbtrail-server config error [invalid_port]: Invalid --port: expected an integer from 1 to 65535.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Startup diagnostics report the resolved listening URL, session output directory, static directory when configured, allowed-origin count, auth protection enabled state, and AI opt-in state. They do not print auth token contents.
|
|
49
|
+
|
|
50
|
+
## Health diagnostics
|
|
51
|
+
|
|
52
|
+
HTTP mode exposes:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
curl http://127.0.0.1:9898/health
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
A healthy response has this shape:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"ok": true,
|
|
63
|
+
"status": "ready",
|
|
64
|
+
"service": "crumbtrail-node",
|
|
65
|
+
"version": "0.1.0",
|
|
66
|
+
"timestamp": "2026-06-29T00:00:00.000Z",
|
|
67
|
+
"uptimeMs": 1234,
|
|
68
|
+
"config": {
|
|
69
|
+
"host": "127.0.0.1",
|
|
70
|
+
"port": 9898,
|
|
71
|
+
"outputDir": "/Users/example/.crumbtrail/sessions",
|
|
72
|
+
"staticDir": "./examples/basic",
|
|
73
|
+
"authEnabled": true,
|
|
74
|
+
"allowedOriginCount": 1,
|
|
75
|
+
"aiEnabled": false,
|
|
76
|
+
"mcpMode": false
|
|
77
|
+
},
|
|
78
|
+
"checks": {
|
|
79
|
+
"outputDir": {
|
|
80
|
+
"path": "/Users/example/.crumbtrail/sessions",
|
|
81
|
+
"exists": true,
|
|
82
|
+
"writable": true
|
|
83
|
+
},
|
|
84
|
+
"staticDir": {
|
|
85
|
+
"configured": true,
|
|
86
|
+
"path": "./examples/basic",
|
|
87
|
+
"exists": true
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If the output directory becomes unavailable while the server is running, `/health` returns HTTP 200 with `ok: false`, `status: "degraded"`, and a bounded filesystem error under `checks.outputDir.error`. This is intentional: health is an inspection surface, not a mutating API.
|
|
94
|
+
|
|
95
|
+
Health output reports auth and allowed-origin configuration as booleans/counts. It must not include auth token contents or raw allowed-origin values.
|
|
96
|
+
|
|
97
|
+
## Self-host quickstart proof
|
|
98
|
+
|
|
99
|
+
Run the packaged local server plus full-stack Express example proof from the repository root:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pnpm verify:self-host
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The command builds `crumbtrail-core` and `crumbtrail-node`, starts built `dist/cli.cjs`, checks `/health`, triggers the deliberate Express demo failure, finalizes artifacts, and verifies linked `events.ndjson`, `index.json`, `llm.json`, `llm.md`, and MCP context. See [`examples/full-stack-express/README.md`](../../examples/full-stack-express/README.md) for expected output and troubleshooting.
|
|
106
|
+
|
|
107
|
+
## Fresh-install validation
|
|
108
|
+
|
|
109
|
+
Run the same local self-host behavior through a temporary standalone install:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pnpm verify:fresh-install
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The verifier builds and packs `crumbtrail-core` and `crumbtrail-node`, installs the packed tarballs into a temporary npm project, resolves the installed `crumbtrail-server` binary, waits for ready `/health`, captures a deliberate failed request session, verifies `events.ndjson`, `index.json`, `llm.json`, `llm.md`, and shuts down cleanly. Passing output includes phase-specific status for package metadata/build, temp install, binary startup, health readiness, self-host artifact proof, and shutdown.
|
|
116
|
+
|
|
117
|
+
For final package validation, run all three packaged-runtime surfaces together:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pnpm --filter crumbtrail-node verify:package-runtime && pnpm verify:self-host && pnpm verify:fresh-install
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## CLI subcommands
|
|
124
|
+
|
|
125
|
+
The same `crumbtrail-server` binary exposes subcommands beyond `serve`. Every subcommand accepts
|
|
126
|
+
`--help` / `-h` for focused help, and `crumbtrail-server --version` / `-v` prints the package version.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
crumbtrail-server --version # print crumbtrail-node version
|
|
130
|
+
crumbtrail-server serve --help # focused help for any subcommand
|
|
131
|
+
crumbtrail-server fix-context <sessionId> --json # ranked, correlated, LLM-ready fix-context.v1 bundle
|
|
132
|
+
crumbtrail-server fix-context <sessionId> # human-readable summary
|
|
133
|
+
crumbtrail-server inspect <sessionId> # hot-plane-only session summary
|
|
134
|
+
crumbtrail-server inspect <sessionId> --json # machine-readable summary
|
|
135
|
+
crumbtrail-server scan ./src --strict # coverage scanner (CI gate); findings carry a suggested fix
|
|
136
|
+
crumbtrail-server doctor --port 9898 # verify capture + correlation + MCP-readability locally
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`fix-context` and `inspect` accept either a bare session id (resolved under the sessions dir,
|
|
140
|
+
override with `--output`) or a path to a session directory. Both read hot-plane artifacts
|
|
141
|
+
only and never open the raw event log. `inspect` reports duration, event/error/failed-request
|
|
142
|
+
counts, candidate count, truncation state, and on-disk artifact sizes.
|
|
143
|
+
|
|
144
|
+
## MCP evidence tools
|
|
145
|
+
|
|
146
|
+
`crumbtrail-server serve --mcp` runs the stdio MCP server against the sessions directory. The
|
|
147
|
+
evidence-first tools are:
|
|
148
|
+
|
|
149
|
+
- `getFixContext(sessionId)` — the `fix-context.v1` bundle (`ranked_candidates`,
|
|
150
|
+
`primary_window` with `frontend` / `backend` / `db_diffs`, `environment`, `repro_hint`).
|
|
151
|
+
- `getSessionManifest(sessionId)` (alias `get_session_manifest`) — manifest metadata,
|
|
152
|
+
markers, timeline, candidates, `accessPattern`. Synthesizes a manifest from `index.json`
|
|
153
|
+
for older sessions.
|
|
154
|
+
- `getWindow(sessionId, t0, t1)` (alias `get_window`) — cold events inside an absolute-ms
|
|
155
|
+
window; bounded and capped (default/max 500) with truncation reporting. The only tool that
|
|
156
|
+
reads the cold stream.
|
|
157
|
+
- `getEvidence(sessionId, ref)` (alias `get_evidence`) — resolve a candidate id,
|
|
158
|
+
interactive-element signature, or request/event id from hot-plane artifacts.
|
|
159
|
+
|
|
160
|
+
## Database diffing
|
|
161
|
+
|
|
162
|
+
Four engine shims wrap a duck-typed driver object the host injects (no driver dependency is
|
|
163
|
+
ever imported) so INSERT/UPDATE/DELETE statements executed inside a request scope record a
|
|
164
|
+
`k:'db.diff'` event (`{ engine, op, table, pk, after, before?, requestId }`):
|
|
165
|
+
|
|
166
|
+
| Engine | Wrap | After-image strategy |
|
|
167
|
+
| -------- | ---------------------------------------- | ------------------------------------------------------------------------------ |
|
|
168
|
+
| postgres | `instrumentPgClient(client, options)` | appends `RETURNING *` |
|
|
169
|
+
| mysql | `instrumentMysqlClient(client, options)` | post-`SELECT` by `insertId` / pk (no SQL rewriting) |
|
|
170
|
+
| mssql | `instrumentMssqlPool(pool, options)` | injects `OUTPUT INSERTED.*` / `DELETED.*` (rows stripped from the host result) |
|
|
171
|
+
| sqlite | `instrumentSqliteDatabase(db, options)` | post-`SELECT` by `lastInsertRowid` / pk (fully synchronous) |
|
|
172
|
+
|
|
173
|
+
All four take the same `InstrumentDbClientOptions` and share the same guarantees: the host
|
|
174
|
+
query never fails and never runs twice because of instrumentation — parse/correlation/capture/
|
|
175
|
+
emit failures degrade to "no diff emitted", and statements the shim cannot confidently handle
|
|
176
|
+
(multi-statement batches, comment-wedged SQL on mssql, multi-row MySQL inserts) fall back to an
|
|
177
|
+
image-less `db.diff` (`pk: null`, `rowCount`) so the write stays visible to differencing.
|
|
178
|
+
Sensitive columns are dropped before any event rests (`DEFAULT_SENSITIVE_DB_COLUMNS` =
|
|
179
|
+
`password`, `token`, `secret`, `api_key`, `ssn`; extend with `redactColumns`).
|
|
180
|
+
`captureBefore: true` also records UPDATE pre-images (and is how MySQL/SQLite before-images are
|
|
181
|
+
sourced); `captureReads: true` opts into capped `db.read` row capture. The events correlate by
|
|
182
|
+
`requestId` (= the request's trace id), so they land in the same evidence window, fill
|
|
183
|
+
`primary_window.db_diffs` in the fix-context bundle, and feed session db differencing across
|
|
184
|
+
all engines. Per-engine wiring examples: `docs/integrations/databases.md`.
|
|
185
|
+
|
|
186
|
+
## Headless job-run sessions
|
|
187
|
+
|
|
188
|
+
Queue workers, cron jobs, and batch runs can create a session without a browser:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { startHeadlessSession } from "crumbtrail-node";
|
|
192
|
+
|
|
193
|
+
const session = await startHeadlessSession({
|
|
194
|
+
endpoint: "http://127.0.0.1:9898",
|
|
195
|
+
sessionId: `job-${Date.now()}`,
|
|
196
|
+
metadata: {
|
|
197
|
+
app: "billing-worker",
|
|
198
|
+
release: process.env.RELEASE,
|
|
199
|
+
build: process.env.GIT_SHA,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await session.record({
|
|
204
|
+
t: Date.now(),
|
|
205
|
+
k: "con",
|
|
206
|
+
d: { lv: "info", msg: "job started" },
|
|
207
|
+
});
|
|
208
|
+
await session.end();
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
If the job already exports OpenTelemetry, stamp spans/logs with the same
|
|
212
|
+
`crumbtrail.session.id`; Crumbtrail files those signals into the same agent-readable
|
|
213
|
+
session as logs and row diffs.
|
|
214
|
+
|
|
215
|
+
## Two-plane storage (operator note)
|
|
216
|
+
|
|
217
|
+
Finalized sessions are written across two planes under
|
|
218
|
+
`<output>/<sessionId>/`. The **hot plane** holds the small, redacted, AI-readable summaries an
|
|
219
|
+
LLM reads first — `manifest.json` (the entry point), `bundle.json`/`llm.json`, `index.json`,
|
|
220
|
+
`candidates.jsonl`, plus `llm.md`/`timeline.md` and `search.jsonl`. The **cold plane** holds
|
|
221
|
+
the full chronological event stream, zstd-compressed as `events.ndjson.zst`, alongside
|
|
222
|
+
`signatures.json` (the interactive-element signature dictionary) and any media
|
|
223
|
+
(`recording.webm`, `audio.webm`, `frames/`). Redaction runs **before** the cold write
|
|
224
|
+
(`cold.transcode.redaction: "sanitized-before-cold-write"`), and the cold event stream is
|
|
225
|
+
opened only when raw chronological evidence is required (zstd needs Node ≥ 22.15.0). The
|
|
226
|
+
manifest's `accessPattern` field documents this read order for tools and operators.
|
|
227
|
+
|
|
228
|
+
## Public API boundary
|
|
229
|
+
|
|
230
|
+
The package exports the server and integration primitives used by local self-host integrations:
|
|
231
|
+
|
|
232
|
+
- `createServer`
|
|
233
|
+
- `SessionManager`
|
|
234
|
+
- `McpServer`
|
|
235
|
+
- `createCrumbtrailExpressMiddleware`
|
|
236
|
+
- `createCrumbtrailExpressErrorMiddleware`
|
|
237
|
+
|
|
238
|
+
The `src/__tests__/package-boundary.test.ts` suite locks the package metadata, built CLI path, public exports, and default CLI configuration. The `src/__tests__/config.test.ts` and `src/__tests__/cli.test.ts` suites lock config validation and safe startup diagnostics. The `src/__tests__/health.test.ts` and server health tests lock health payload safety and degraded output-directory behavior.
|
|
239
|
+
|
|
240
|
+
## What this does not claim yet
|
|
241
|
+
|
|
242
|
+
This package is not yet a production/cloud hosting story. M003 proves local self-host packaging and fresh-install validation; later work can still expand deployment guides and hosted operations.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import "./chunk-QGM4M3NI.js";
|
|
2
|
+
|
|
3
|
+
// ../core/dist/bug-widget-LGIRLS76.js
|
|
4
|
+
var WIDGET_CSS = `
|
|
5
|
+
:host {
|
|
6
|
+
all: initial;
|
|
7
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
8
|
+
font-size: 14px;
|
|
9
|
+
color: #e0e0e0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.bl-trigger {
|
|
13
|
+
position: fixed;
|
|
14
|
+
bottom: 20px;
|
|
15
|
+
right: 20px;
|
|
16
|
+
width: 48px;
|
|
17
|
+
height: 48px;
|
|
18
|
+
border-radius: 50%;
|
|
19
|
+
background: #dc2626;
|
|
20
|
+
border: none;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
justify-content: center;
|
|
25
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
26
|
+
z-index: 2147483647;
|
|
27
|
+
transition: transform 0.15s ease, background 0.15s ease;
|
|
28
|
+
}
|
|
29
|
+
.bl-trigger:hover {
|
|
30
|
+
transform: scale(1.1);
|
|
31
|
+
background: #ef4444;
|
|
32
|
+
}
|
|
33
|
+
.bl-trigger svg {
|
|
34
|
+
width: 24px;
|
|
35
|
+
height: 24px;
|
|
36
|
+
fill: white;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.bl-popover {
|
|
40
|
+
position: fixed;
|
|
41
|
+
bottom: 80px;
|
|
42
|
+
right: 20px;
|
|
43
|
+
width: 320px;
|
|
44
|
+
background: #1e1e1e;
|
|
45
|
+
border: 1px solid #333;
|
|
46
|
+
border-radius: 12px;
|
|
47
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
|
48
|
+
z-index: 2147483647;
|
|
49
|
+
padding: 16px;
|
|
50
|
+
display: none;
|
|
51
|
+
}
|
|
52
|
+
.bl-popover.open {
|
|
53
|
+
display: block;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.bl-popover h3 {
|
|
57
|
+
margin: 0 0 12px;
|
|
58
|
+
font-size: 15px;
|
|
59
|
+
font-weight: 600;
|
|
60
|
+
color: #f5f5f5;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.bl-note {
|
|
64
|
+
width: 100%;
|
|
65
|
+
min-height: 60px;
|
|
66
|
+
background: #2a2a2a;
|
|
67
|
+
border: 1px solid #444;
|
|
68
|
+
border-radius: 8px;
|
|
69
|
+
color: #e0e0e0;
|
|
70
|
+
padding: 8px 10px;
|
|
71
|
+
font-size: 13px;
|
|
72
|
+
font-family: inherit;
|
|
73
|
+
resize: vertical;
|
|
74
|
+
box-sizing: border-box;
|
|
75
|
+
}
|
|
76
|
+
.bl-note::placeholder {
|
|
77
|
+
color: #777;
|
|
78
|
+
}
|
|
79
|
+
.bl-note:focus {
|
|
80
|
+
outline: none;
|
|
81
|
+
border-color: #dc2626;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.bl-actions {
|
|
85
|
+
display: flex;
|
|
86
|
+
gap: 8px;
|
|
87
|
+
margin-top: 12px;
|
|
88
|
+
align-items: center;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.bl-btn {
|
|
92
|
+
padding: 8px 16px;
|
|
93
|
+
border-radius: 8px;
|
|
94
|
+
border: none;
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
font-size: 13px;
|
|
97
|
+
font-weight: 500;
|
|
98
|
+
transition: background 0.15s ease;
|
|
99
|
+
}
|
|
100
|
+
.bl-submit {
|
|
101
|
+
background: #dc2626;
|
|
102
|
+
color: white;
|
|
103
|
+
flex: 1;
|
|
104
|
+
}
|
|
105
|
+
.bl-submit:hover {
|
|
106
|
+
background: #ef4444;
|
|
107
|
+
}
|
|
108
|
+
.bl-submit:disabled {
|
|
109
|
+
opacity: 0.5;
|
|
110
|
+
cursor: not-allowed;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.bl-status {
|
|
114
|
+
font-size: 11px;
|
|
115
|
+
color: #777;
|
|
116
|
+
margin-top: 8px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.bl-hint {
|
|
120
|
+
font-size: 11px;
|
|
121
|
+
color: #555;
|
|
122
|
+
margin-top: 8px;
|
|
123
|
+
text-align: center;
|
|
124
|
+
}
|
|
125
|
+
`;
|
|
126
|
+
var BUG_SVG = `<svg viewBox="0 0 24 24"><path d="M20 8h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5s-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z"/></svg>`;
|
|
127
|
+
function mountWidget(logger) {
|
|
128
|
+
const host = document.createElement("div");
|
|
129
|
+
host.id = "crumbtrail-widget";
|
|
130
|
+
const shadow = host.attachShadow({ mode: "closed" });
|
|
131
|
+
const style = document.createElement("style");
|
|
132
|
+
style.textContent = WIDGET_CSS;
|
|
133
|
+
shadow.appendChild(style);
|
|
134
|
+
const trigger = document.createElement("button");
|
|
135
|
+
trigger.className = "bl-trigger";
|
|
136
|
+
trigger.innerHTML = BUG_SVG;
|
|
137
|
+
trigger.title = "Flag a bug (Ctrl+Shift+B)";
|
|
138
|
+
shadow.appendChild(trigger);
|
|
139
|
+
const popover = document.createElement("div");
|
|
140
|
+
popover.className = "bl-popover";
|
|
141
|
+
popover.innerHTML = `
|
|
142
|
+
<h3>Flag a Bug</h3>
|
|
143
|
+
<textarea class="bl-note" placeholder="What went wrong? (optional)"></textarea>
|
|
144
|
+
<div class="bl-actions">
|
|
145
|
+
<button class="bl-btn bl-submit">Send Bug Report</button>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="bl-status"></div>
|
|
148
|
+
<div class="bl-hint">Ctrl+Shift+B to toggle</div>
|
|
149
|
+
`;
|
|
150
|
+
shadow.appendChild(popover);
|
|
151
|
+
const noteInput = popover.querySelector(".bl-note");
|
|
152
|
+
const submitBtn = popover.querySelector(".bl-submit");
|
|
153
|
+
const statusEl = popover.querySelector(".bl-status");
|
|
154
|
+
let isOpen = false;
|
|
155
|
+
function toggle() {
|
|
156
|
+
isOpen = !isOpen;
|
|
157
|
+
popover.classList.toggle("open", isOpen);
|
|
158
|
+
if (isOpen) {
|
|
159
|
+
noteInput.focus();
|
|
160
|
+
} else {
|
|
161
|
+
reset();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function reset() {
|
|
165
|
+
noteInput.value = "";
|
|
166
|
+
statusEl.textContent = "";
|
|
167
|
+
}
|
|
168
|
+
trigger.addEventListener("click", toggle);
|
|
169
|
+
submitBtn.addEventListener("click", async () => {
|
|
170
|
+
submitBtn.disabled = true;
|
|
171
|
+
statusEl.textContent = "Sending...";
|
|
172
|
+
try {
|
|
173
|
+
const { bugId } = await logger.flagBug({
|
|
174
|
+
note: noteInput.value || void 0
|
|
175
|
+
});
|
|
176
|
+
statusEl.textContent = `Saved: ${bugId}`;
|
|
177
|
+
setTimeout(() => {
|
|
178
|
+
toggle();
|
|
179
|
+
submitBtn.disabled = false;
|
|
180
|
+
}, 1500);
|
|
181
|
+
} catch {
|
|
182
|
+
statusEl.textContent = "Failed to send";
|
|
183
|
+
submitBtn.disabled = false;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
function onKeyDown(e) {
|
|
187
|
+
if (e.ctrlKey && e.shiftKey && e.key === "B") {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
toggle();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
document.addEventListener("keydown", onKeyDown);
|
|
193
|
+
document.body.appendChild(host);
|
|
194
|
+
return () => {
|
|
195
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
196
|
+
host.remove();
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
export {
|
|
200
|
+
mountWidget
|
|
201
|
+
};
|