@korajs/devtools 0.0.0-canary-20261008174129
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 +49 -0
- package/dist/chunk-LQFL4TWM.js +999 -0
- package/dist/chunk-LQFL4TWM.js.map +1 -0
- package/dist/embed-overlay-Dwys5PYM.d.cts +170 -0
- package/dist/embed-overlay-Dwys5PYM.d.ts +170 -0
- package/dist/extension/background.cjs +65 -0
- package/dist/extension/background.cjs.map +1 -0
- package/dist/extension/background.d.cts +2 -0
- package/dist/extension/background.d.ts +2 -0
- package/dist/extension/background.js +63 -0
- package/dist/extension/background.js.map +1 -0
- package/dist/extension/content-script.cjs +18 -0
- package/dist/extension/content-script.cjs.map +1 -0
- package/dist/extension/content-script.d.cts +2 -0
- package/dist/extension/content-script.d.ts +2 -0
- package/dist/extension/content-script.js +16 -0
- package/dist/extension/content-script.js.map +1 -0
- package/dist/extension/devtools-page.html +45 -0
- package/dist/extension/devtools.cjs +8 -0
- package/dist/extension/devtools.cjs.map +1 -0
- package/dist/extension/devtools.d.cts +2 -0
- package/dist/extension/devtools.d.ts +2 -0
- package/dist/extension/devtools.html +599 -0
- package/dist/extension/devtools.js +6 -0
- package/dist/extension/devtools.js.map +1 -0
- package/dist/extension/manifest.json +20 -0
- package/dist/extension/panel.cjs +1311 -0
- package/dist/extension/panel.cjs.map +1 -0
- package/dist/extension/panel.d.cts +2 -0
- package/dist/extension/panel.d.ts +2 -0
- package/dist/extension/panel.js +1309 -0
- package/dist/extension/panel.js.map +1 -0
- package/dist/index.cjs +1353 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +107 -0
- package/dist/index.d.ts +107 -0
- package/dist/index.js +326 -0
- package/dist/index.js.map +1 -0
- package/dist/overlay.cjs +1027 -0
- package/dist/overlay.cjs.map +1 -0
- package/dist/overlay.d.cts +2 -0
- package/dist/overlay.d.ts +2 -0
- package/dist/overlay.js +7 -0
- package/dist/overlay.js.map +1 -0
- package/package.json +55 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1353 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
EventBuffer: () => EventBuffer,
|
|
34
|
+
Instrumenter: () => Instrumenter,
|
|
35
|
+
MessageBridge: () => MessageBridge,
|
|
36
|
+
PortRouter: () => PortRouter,
|
|
37
|
+
buildPanelModel: () => buildPanelModel,
|
|
38
|
+
computeStatistics: () => computeStatistics,
|
|
39
|
+
filterEvents: () => filterEvents,
|
|
40
|
+
getEventCategory: () => getEventCategory,
|
|
41
|
+
mountKoraDevtoolsOverlay: () => mountKoraDevtoolsOverlay,
|
|
42
|
+
renderDevtoolsPanel: () => renderDevtoolsPanel
|
|
43
|
+
});
|
|
44
|
+
module.exports = __toCommonJS(src_exports);
|
|
45
|
+
|
|
46
|
+
// src/bridge/message-bridge.ts
|
|
47
|
+
var DEFAULT_CHANNEL = "kora-devtools";
|
|
48
|
+
var MessageBridge = class {
|
|
49
|
+
channelName;
|
|
50
|
+
listeners = /* @__PURE__ */ new Set();
|
|
51
|
+
messageHandler;
|
|
52
|
+
destroyed = false;
|
|
53
|
+
constructor(channelName = DEFAULT_CHANNEL) {
|
|
54
|
+
this.channelName = channelName;
|
|
55
|
+
if (typeof window === "undefined") {
|
|
56
|
+
this.messageHandler = null;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this.messageHandler = (event) => {
|
|
60
|
+
if (this.destroyed) return;
|
|
61
|
+
const data = event.data;
|
|
62
|
+
if (!data || data.source !== this.channelName) return;
|
|
63
|
+
for (const listener of this.listeners) {
|
|
64
|
+
listener(data.payload);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
window.addEventListener("message", this.messageHandler);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Post a timestamped event through the bridge.
|
|
71
|
+
* No-op if window is not available or the bridge has been destroyed.
|
|
72
|
+
*/
|
|
73
|
+
send(event) {
|
|
74
|
+
if (this.destroyed || typeof window === "undefined") return;
|
|
75
|
+
const message = {
|
|
76
|
+
source: this.channelName,
|
|
77
|
+
payload: event
|
|
78
|
+
};
|
|
79
|
+
window.postMessage(message, "*");
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Register a callback for events received through the bridge.
|
|
83
|
+
* Returns an unsubscribe function.
|
|
84
|
+
*/
|
|
85
|
+
onReceive(callback) {
|
|
86
|
+
if (this.destroyed) return () => {
|
|
87
|
+
};
|
|
88
|
+
this.listeners.add(callback);
|
|
89
|
+
return () => {
|
|
90
|
+
this.listeners.delete(callback);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Remove all listeners and detach from window.
|
|
95
|
+
* After calling destroy, all operations become no-ops.
|
|
96
|
+
*/
|
|
97
|
+
destroy() {
|
|
98
|
+
if (this.destroyed) return;
|
|
99
|
+
this.destroyed = true;
|
|
100
|
+
this.listeners.clear();
|
|
101
|
+
if (this.messageHandler && typeof window !== "undefined") {
|
|
102
|
+
window.removeEventListener("message", this.messageHandler);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// src/buffer/event-buffer.ts
|
|
108
|
+
var DEFAULT_CAPACITY = 1e4;
|
|
109
|
+
var EventBuffer = class {
|
|
110
|
+
_capacity;
|
|
111
|
+
buffer;
|
|
112
|
+
/** Index where the next event will be written */
|
|
113
|
+
head = 0;
|
|
114
|
+
/** Total number of events ever pushed (used to compute readable range) */
|
|
115
|
+
_totalPushed = 0;
|
|
116
|
+
constructor(capacity = DEFAULT_CAPACITY) {
|
|
117
|
+
if (capacity < 1) {
|
|
118
|
+
throw new Error("EventBuffer capacity must be at least 1");
|
|
119
|
+
}
|
|
120
|
+
this._capacity = capacity;
|
|
121
|
+
this.buffer = new Array(capacity);
|
|
122
|
+
}
|
|
123
|
+
/** Maximum number of events the buffer can hold */
|
|
124
|
+
get capacity() {
|
|
125
|
+
return this._capacity;
|
|
126
|
+
}
|
|
127
|
+
/** Current number of events in the buffer */
|
|
128
|
+
get size() {
|
|
129
|
+
return Math.min(this._totalPushed, this._capacity);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Append an event to the buffer.
|
|
133
|
+
* If the buffer is at capacity, the oldest event is evicted.
|
|
134
|
+
*/
|
|
135
|
+
push(event) {
|
|
136
|
+
this.buffer[this.head] = event;
|
|
137
|
+
this.head = (this.head + 1) % this._capacity;
|
|
138
|
+
this._totalPushed++;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Returns all events in insertion order (oldest first).
|
|
142
|
+
*/
|
|
143
|
+
getAll() {
|
|
144
|
+
if (this._totalPushed === 0) return [];
|
|
145
|
+
const result = [];
|
|
146
|
+
if (this._totalPushed <= this._capacity) {
|
|
147
|
+
for (let i = 0; i < this.head; i++) {
|
|
148
|
+
const event = this.buffer[i];
|
|
149
|
+
if (event) result.push(event);
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
for (let i = 0; i < this._capacity; i++) {
|
|
153
|
+
const index = (this.head + i) % this._capacity;
|
|
154
|
+
const event = this.buffer[index];
|
|
155
|
+
if (event) result.push(event);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Returns events whose sequential IDs fall within [start, end] (inclusive).
|
|
162
|
+
*/
|
|
163
|
+
getRange(start, end) {
|
|
164
|
+
return this.getAll().filter((e) => e.id >= start && e.id <= end);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Returns events matching a specific KoraEventType.
|
|
168
|
+
*/
|
|
169
|
+
getByType(type) {
|
|
170
|
+
return this.getAll().filter((e) => e.event.type === type);
|
|
171
|
+
}
|
|
172
|
+
/** Remove all events from the buffer */
|
|
173
|
+
clear() {
|
|
174
|
+
this.buffer.fill(void 0);
|
|
175
|
+
this.head = 0;
|
|
176
|
+
this._totalPushed = 0;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// src/instrumenter/instrumenter.ts
|
|
181
|
+
var DEFAULT_BUFFER_SIZE = 1e4;
|
|
182
|
+
var DEFAULT_CHANNEL_NAME = "kora-devtools";
|
|
183
|
+
var ALL_EVENT_TYPES = [
|
|
184
|
+
"operation:created",
|
|
185
|
+
"operation:applied",
|
|
186
|
+
"merge:started",
|
|
187
|
+
"merge:completed",
|
|
188
|
+
"merge:conflict",
|
|
189
|
+
"constraint:violated",
|
|
190
|
+
"sync:connected",
|
|
191
|
+
"sync:disconnected",
|
|
192
|
+
"sync:schema-mismatch",
|
|
193
|
+
"sync:auth-failed",
|
|
194
|
+
"sync:suspended",
|
|
195
|
+
"sync:apply-failed",
|
|
196
|
+
"sync:apply-blocked",
|
|
197
|
+
"sync:apply-retrying",
|
|
198
|
+
"sync:apply-recovered",
|
|
199
|
+
"sync:apply-abandoned",
|
|
200
|
+
"sync:sent",
|
|
201
|
+
"sync:received",
|
|
202
|
+
"sync:acknowledged",
|
|
203
|
+
"sync:diagnostics",
|
|
204
|
+
"sync:bandwidth",
|
|
205
|
+
"sync:initial-sync-progress",
|
|
206
|
+
"sync:durability-degraded",
|
|
207
|
+
"sync:durability-restored",
|
|
208
|
+
"store:persistence-error",
|
|
209
|
+
"store:quota-exceeded",
|
|
210
|
+
"store:log-integrity",
|
|
211
|
+
"store:rematerialized",
|
|
212
|
+
"store:storage-fallback",
|
|
213
|
+
"store:opfs-unavailable",
|
|
214
|
+
"store:durability-lost",
|
|
215
|
+
"store:db-name-collision",
|
|
216
|
+
"store:storage-blocked",
|
|
217
|
+
"store:storage-migrated",
|
|
218
|
+
"store:schema-ahead",
|
|
219
|
+
"replay:completed",
|
|
220
|
+
"query:subscribed",
|
|
221
|
+
"query:invalidated",
|
|
222
|
+
"query:executed",
|
|
223
|
+
"query:error",
|
|
224
|
+
"storage:persistence",
|
|
225
|
+
"connection:quality"
|
|
226
|
+
];
|
|
227
|
+
var Instrumenter = class {
|
|
228
|
+
constructor(emitter, config) {
|
|
229
|
+
this.emitter = emitter;
|
|
230
|
+
const bufferSize = config?.bufferSize ?? DEFAULT_BUFFER_SIZE;
|
|
231
|
+
const bridgeEnabled = config?.bridgeEnabled ?? true;
|
|
232
|
+
const channelName = config?.channelName ?? DEFAULT_CHANNEL_NAME;
|
|
233
|
+
this.buffer = new EventBuffer(bufferSize);
|
|
234
|
+
this.bridge = bridgeEnabled ? new MessageBridge(channelName) : null;
|
|
235
|
+
this.attachListeners();
|
|
236
|
+
}
|
|
237
|
+
emitter;
|
|
238
|
+
buffer;
|
|
239
|
+
bridge;
|
|
240
|
+
unsubscribers = [];
|
|
241
|
+
nextId = 1;
|
|
242
|
+
paused = false;
|
|
243
|
+
destroyed = false;
|
|
244
|
+
/** Access the underlying event buffer */
|
|
245
|
+
getBuffer() {
|
|
246
|
+
return this.buffer;
|
|
247
|
+
}
|
|
248
|
+
/** Access the message bridge, or null if bridge is disabled */
|
|
249
|
+
getBridge() {
|
|
250
|
+
return this.bridge;
|
|
251
|
+
}
|
|
252
|
+
/** Temporarily stop recording events. Events emitted while paused are dropped. */
|
|
253
|
+
pause() {
|
|
254
|
+
this.paused = true;
|
|
255
|
+
}
|
|
256
|
+
/** Resume recording events after a pause. */
|
|
257
|
+
resume() {
|
|
258
|
+
this.paused = false;
|
|
259
|
+
}
|
|
260
|
+
/** Whether the instrumenter is currently paused */
|
|
261
|
+
isPaused() {
|
|
262
|
+
return this.paused;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Detach all listeners from the emitter and destroy the bridge.
|
|
266
|
+
* After calling destroy, the instrumenter is inert.
|
|
267
|
+
*/
|
|
268
|
+
destroy() {
|
|
269
|
+
if (this.destroyed) return;
|
|
270
|
+
this.destroyed = true;
|
|
271
|
+
for (const unsub of this.unsubscribers) {
|
|
272
|
+
unsub();
|
|
273
|
+
}
|
|
274
|
+
this.unsubscribers.length = 0;
|
|
275
|
+
this.bridge?.destroy();
|
|
276
|
+
}
|
|
277
|
+
attachListeners() {
|
|
278
|
+
for (const eventType of ALL_EVENT_TYPES) {
|
|
279
|
+
const unsub = this.emitter.on(eventType, (event) => {
|
|
280
|
+
this.handleEvent(event);
|
|
281
|
+
});
|
|
282
|
+
this.unsubscribers.push(unsub);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
handleEvent(event) {
|
|
286
|
+
if (this.paused || this.destroyed) return;
|
|
287
|
+
const timestamped = {
|
|
288
|
+
id: this.nextId++,
|
|
289
|
+
event,
|
|
290
|
+
receivedAt: Date.now()
|
|
291
|
+
};
|
|
292
|
+
this.buffer.push(timestamped);
|
|
293
|
+
this.bridge?.send(timestamped);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// src/types.ts
|
|
298
|
+
var EVENT_TYPE_CATEGORIES = {
|
|
299
|
+
"operation:created": "operation",
|
|
300
|
+
"operation:applied": "operation",
|
|
301
|
+
"merge:started": "merge",
|
|
302
|
+
"merge:completed": "merge",
|
|
303
|
+
"merge:conflict": "merge",
|
|
304
|
+
"constraint:violated": "merge",
|
|
305
|
+
"sync:connected": "sync",
|
|
306
|
+
"sync:disconnected": "sync",
|
|
307
|
+
"sync:auth-failed": "sync",
|
|
308
|
+
"sync:suspended": "sync",
|
|
309
|
+
"sync:clock-skew": "sync",
|
|
310
|
+
"encryption:status": "sync",
|
|
311
|
+
"sync:node-id-rotated": "sync",
|
|
312
|
+
"sync:local-node": "sync",
|
|
313
|
+
"sync:clock-rebase": "sync",
|
|
314
|
+
"sync:schema-mismatch": "sync",
|
|
315
|
+
"sync:protocol-deprecated": "sync",
|
|
316
|
+
"sync:unverified-legacy-operation": "sync",
|
|
317
|
+
"sync:forged-duplicate": "sync",
|
|
318
|
+
"sync:apply-failed": "sync",
|
|
319
|
+
"sync:apply-blocked": "sync",
|
|
320
|
+
"sync:apply-retrying": "sync",
|
|
321
|
+
"sync:apply-recovered": "sync",
|
|
322
|
+
"sync:apply-abandoned": "sync",
|
|
323
|
+
"sync:operation-rejected": "sync",
|
|
324
|
+
"sync:sent": "sync",
|
|
325
|
+
"sync:received": "sync",
|
|
326
|
+
"sync:acknowledged": "sync",
|
|
327
|
+
"sync:scope-retracted": "sync",
|
|
328
|
+
"query:subscribed": "query",
|
|
329
|
+
"query:invalidated": "query",
|
|
330
|
+
"query:executed": "query",
|
|
331
|
+
"query:error": "query",
|
|
332
|
+
"storage:persistence": "connection",
|
|
333
|
+
"connection:quality": "connection",
|
|
334
|
+
"sync:diagnostics": "sync",
|
|
335
|
+
"sync:bandwidth": "sync",
|
|
336
|
+
"sync:initial-sync-progress": "sync",
|
|
337
|
+
"sync:delivery-gap": "sync",
|
|
338
|
+
"sync:delivery-stalled": "sync",
|
|
339
|
+
"sync:durability-degraded": "connection",
|
|
340
|
+
"sync:durability-restored": "connection",
|
|
341
|
+
"awareness:updated": "sync",
|
|
342
|
+
"state-machine:transition": "operation",
|
|
343
|
+
"state-machine:rejected": "operation",
|
|
344
|
+
"store:persistence-error": "connection",
|
|
345
|
+
"store:quota-exceeded": "connection",
|
|
346
|
+
"store:log-integrity": "operation",
|
|
347
|
+
"store:rematerialized": "operation",
|
|
348
|
+
"store:storage-fallback": "connection",
|
|
349
|
+
"store:opfs-unavailable": "connection",
|
|
350
|
+
"store:durability-lost": "connection",
|
|
351
|
+
"store:db-name-collision": "connection",
|
|
352
|
+
"store:storage-blocked": "connection",
|
|
353
|
+
"store:storage-migrated": "connection",
|
|
354
|
+
"store:schema-ahead": "connection",
|
|
355
|
+
"replay:completed": "operation"
|
|
356
|
+
};
|
|
357
|
+
function eventTypeToCategory(type) {
|
|
358
|
+
return EVENT_TYPE_CATEGORIES[type];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/filter/event-filter.ts
|
|
362
|
+
function getEventCategory(type) {
|
|
363
|
+
return eventTypeToCategory(type);
|
|
364
|
+
}
|
|
365
|
+
function extractCollection(event) {
|
|
366
|
+
const e = event.event;
|
|
367
|
+
switch (e.type) {
|
|
368
|
+
case "operation:created":
|
|
369
|
+
case "operation:applied":
|
|
370
|
+
return e.operation.collection;
|
|
371
|
+
case "merge:started":
|
|
372
|
+
return e.operationA.collection;
|
|
373
|
+
case "merge:completed":
|
|
374
|
+
case "merge:conflict":
|
|
375
|
+
return e.trace.operationA.collection;
|
|
376
|
+
case "constraint:violated":
|
|
377
|
+
return e.trace.operationA.collection;
|
|
378
|
+
case "query:subscribed":
|
|
379
|
+
return e.collection;
|
|
380
|
+
case "query:invalidated":
|
|
381
|
+
return e.trigger.collection;
|
|
382
|
+
case "sync:sent":
|
|
383
|
+
return e.operations[0]?.collection ?? null;
|
|
384
|
+
case "sync:received":
|
|
385
|
+
return e.operations[0]?.collection ?? null;
|
|
386
|
+
default:
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function filterEvents(events, criteria) {
|
|
391
|
+
const { categories, types, timeRange, collection } = criteria;
|
|
392
|
+
if (!categories && !types && !timeRange && !collection) {
|
|
393
|
+
return events;
|
|
394
|
+
}
|
|
395
|
+
const categorySet = categories ? new Set(categories) : null;
|
|
396
|
+
const typeSet = types ? new Set(types) : null;
|
|
397
|
+
return events.filter((event) => {
|
|
398
|
+
if (categorySet) {
|
|
399
|
+
const cat = eventTypeToCategory(event.event.type);
|
|
400
|
+
if (!categorySet.has(cat)) return false;
|
|
401
|
+
}
|
|
402
|
+
if (typeSet) {
|
|
403
|
+
if (!typeSet.has(event.event.type)) return false;
|
|
404
|
+
}
|
|
405
|
+
if (timeRange) {
|
|
406
|
+
if (event.receivedAt < timeRange.start || event.receivedAt > timeRange.end) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (collection) {
|
|
411
|
+
const eventCollection = extractCollection(event);
|
|
412
|
+
if (eventCollection !== collection) return false;
|
|
413
|
+
}
|
|
414
|
+
return true;
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/stats/event-stats.ts
|
|
419
|
+
function computeStatistics(events) {
|
|
420
|
+
const eventsByCategory = {
|
|
421
|
+
operation: 0,
|
|
422
|
+
merge: 0,
|
|
423
|
+
sync: 0,
|
|
424
|
+
query: 0,
|
|
425
|
+
connection: 0
|
|
426
|
+
};
|
|
427
|
+
const eventsByType = {};
|
|
428
|
+
let mergeConflicts = 0;
|
|
429
|
+
let constraintViolations = 0;
|
|
430
|
+
let mergeDurationSum = 0;
|
|
431
|
+
let mergeDurationCount = 0;
|
|
432
|
+
let queryDurationSum = 0;
|
|
433
|
+
let queryDurationCount = 0;
|
|
434
|
+
let syncOperationsSent = 0;
|
|
435
|
+
let syncOperationsReceived = 0;
|
|
436
|
+
for (const timestamped of events) {
|
|
437
|
+
const e = timestamped.event;
|
|
438
|
+
const category = eventTypeToCategory(e.type);
|
|
439
|
+
eventsByCategory[category]++;
|
|
440
|
+
eventsByType[e.type] = (eventsByType[e.type] ?? 0) + 1;
|
|
441
|
+
switch (e.type) {
|
|
442
|
+
case "merge:completed":
|
|
443
|
+
mergeDurationSum += e.trace.duration;
|
|
444
|
+
mergeDurationCount++;
|
|
445
|
+
break;
|
|
446
|
+
case "merge:conflict":
|
|
447
|
+
mergeConflicts++;
|
|
448
|
+
mergeDurationSum += e.trace.duration;
|
|
449
|
+
mergeDurationCount++;
|
|
450
|
+
break;
|
|
451
|
+
case "constraint:violated":
|
|
452
|
+
constraintViolations++;
|
|
453
|
+
break;
|
|
454
|
+
case "query:executed":
|
|
455
|
+
queryDurationSum += e.duration;
|
|
456
|
+
queryDurationCount++;
|
|
457
|
+
break;
|
|
458
|
+
case "sync:sent":
|
|
459
|
+
syncOperationsSent += e.batchSize;
|
|
460
|
+
break;
|
|
461
|
+
case "sync:received":
|
|
462
|
+
syncOperationsReceived += e.batchSize;
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
totalEvents: events.length,
|
|
468
|
+
eventsByCategory,
|
|
469
|
+
eventsByType,
|
|
470
|
+
mergeConflicts,
|
|
471
|
+
constraintViolations,
|
|
472
|
+
avgMergeDuration: mergeDurationCount > 0 ? mergeDurationSum / mergeDurationCount : null,
|
|
473
|
+
avgQueryDuration: queryDurationCount > 0 ? queryDurationSum / queryDurationCount : null,
|
|
474
|
+
syncOperationsSent,
|
|
475
|
+
syncOperationsReceived
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/ui/panel-state.ts
|
|
480
|
+
function buildPanelModel(events) {
|
|
481
|
+
const timeline = events.map((entry) => ({
|
|
482
|
+
id: entry.id,
|
|
483
|
+
type: entry.event.type,
|
|
484
|
+
label: timelineLabel(entry.event),
|
|
485
|
+
color: timelineColor(entry.event.type),
|
|
486
|
+
receivedAt: entry.receivedAt,
|
|
487
|
+
dependsOn: extractCausalDependencies(entry.event)
|
|
488
|
+
}));
|
|
489
|
+
const conflicts = events.flatMap((entry) => {
|
|
490
|
+
if (entry.event.type !== "merge:completed" && entry.event.type !== "merge:conflict" && entry.event.type !== "constraint:violated") {
|
|
491
|
+
return [];
|
|
492
|
+
}
|
|
493
|
+
const trace = entry.event.trace;
|
|
494
|
+
return [
|
|
495
|
+
{
|
|
496
|
+
id: entry.id,
|
|
497
|
+
timestamp: entry.receivedAt,
|
|
498
|
+
collection: trace.operationA.collection,
|
|
499
|
+
field: trace.field,
|
|
500
|
+
strategy: trace.strategy,
|
|
501
|
+
tier: trace.tier,
|
|
502
|
+
inputA: trace.inputA,
|
|
503
|
+
inputB: trace.inputB,
|
|
504
|
+
output: trace.output,
|
|
505
|
+
constraintViolated: trace.constraintViolated
|
|
506
|
+
}
|
|
507
|
+
];
|
|
508
|
+
});
|
|
509
|
+
const operations = events.map((entry) => {
|
|
510
|
+
const operation = extractOperation(entry.event);
|
|
511
|
+
if (!operation) return null;
|
|
512
|
+
return {
|
|
513
|
+
id: entry.id,
|
|
514
|
+
timestamp: entry.receivedAt,
|
|
515
|
+
operationId: operation.id,
|
|
516
|
+
collection: operation.collection,
|
|
517
|
+
recordId: operation.recordId,
|
|
518
|
+
opType: operation.type,
|
|
519
|
+
data: operation.data,
|
|
520
|
+
causalDeps: operation.causalDeps,
|
|
521
|
+
nodeId: operation.nodeId,
|
|
522
|
+
sequenceNumber: operation.sequenceNumber
|
|
523
|
+
};
|
|
524
|
+
}).filter((item) => item !== null);
|
|
525
|
+
const network = buildNetworkStatus(events, operations);
|
|
526
|
+
return {
|
|
527
|
+
timeline,
|
|
528
|
+
conflicts,
|
|
529
|
+
operations,
|
|
530
|
+
network
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function buildNetworkStatus(events, operations) {
|
|
534
|
+
let connected = false;
|
|
535
|
+
let quality = null;
|
|
536
|
+
let pendingAcks = 0;
|
|
537
|
+
let lastSyncAt = null;
|
|
538
|
+
let sentOps = 0;
|
|
539
|
+
let receivedOps = 0;
|
|
540
|
+
for (const entry of events) {
|
|
541
|
+
switch (entry.event.type) {
|
|
542
|
+
case "sync:connected":
|
|
543
|
+
connected = true;
|
|
544
|
+
lastSyncAt = entry.receivedAt;
|
|
545
|
+
break;
|
|
546
|
+
case "sync:disconnected":
|
|
547
|
+
connected = false;
|
|
548
|
+
break;
|
|
549
|
+
case "connection:quality":
|
|
550
|
+
quality = entry.event.quality;
|
|
551
|
+
break;
|
|
552
|
+
case "sync:sent":
|
|
553
|
+
sentOps += entry.event.operations.length;
|
|
554
|
+
pendingAcks += entry.event.operations.length;
|
|
555
|
+
lastSyncAt = entry.receivedAt;
|
|
556
|
+
break;
|
|
557
|
+
case "sync:received":
|
|
558
|
+
receivedOps += entry.event.operations.length;
|
|
559
|
+
lastSyncAt = entry.receivedAt;
|
|
560
|
+
break;
|
|
561
|
+
case "sync:acknowledged":
|
|
562
|
+
pendingAcks = Math.max(0, pendingAcks - 1);
|
|
563
|
+
lastSyncAt = entry.receivedAt;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const vector = /* @__PURE__ */ new Map();
|
|
568
|
+
for (const operation of operations) {
|
|
569
|
+
const current = vector.get(operation.nodeId) ?? 0;
|
|
570
|
+
if (operation.sequenceNumber > current) {
|
|
571
|
+
vector.set(operation.nodeId, operation.sequenceNumber);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
connected,
|
|
576
|
+
quality,
|
|
577
|
+
pendingAcks,
|
|
578
|
+
lastSyncAt,
|
|
579
|
+
sentOps,
|
|
580
|
+
receivedOps,
|
|
581
|
+
versionVector: [...vector.entries()].map(([nodeId, sequenceNumber]) => ({ nodeId, sequenceNumber })).sort((left, right) => left.nodeId.localeCompare(right.nodeId))
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function timelineLabel(event) {
|
|
585
|
+
switch (event.type) {
|
|
586
|
+
case "operation:created":
|
|
587
|
+
case "operation:applied":
|
|
588
|
+
return `${event.operation.type} ${event.operation.collection}/${event.operation.recordId}`;
|
|
589
|
+
case "merge:started":
|
|
590
|
+
return `merge start ${event.operationA.collection}`;
|
|
591
|
+
case "merge:completed":
|
|
592
|
+
return `merge complete ${event.trace.field}`;
|
|
593
|
+
case "merge:conflict":
|
|
594
|
+
return `merge conflict ${event.trace.field}`;
|
|
595
|
+
case "constraint:violated":
|
|
596
|
+
return `constraint ${event.constraint}`;
|
|
597
|
+
case "sync:connected":
|
|
598
|
+
return `sync connected ${event.nodeId}`;
|
|
599
|
+
case "sync:disconnected":
|
|
600
|
+
return "sync disconnected";
|
|
601
|
+
case "sync:auth-failed":
|
|
602
|
+
return "sync auth failed";
|
|
603
|
+
case "sync:suspended":
|
|
604
|
+
return `sync suspended (${event.reason})`;
|
|
605
|
+
case "encryption:status":
|
|
606
|
+
return `encryption ${event.status.state}${event.status.code ? ` (${event.status.code})` : ""}`;
|
|
607
|
+
case "sync:clock-skew":
|
|
608
|
+
return `clock skew ${Math.round(event.skewMs / 1e3)}s (${event.severity})`;
|
|
609
|
+
case "sync:node-id-rotated":
|
|
610
|
+
return `node id rotated (${event.reenqueuedCount} ops re-queued)`;
|
|
611
|
+
case "sync:local-node":
|
|
612
|
+
return `local node ${event.nodeId} ${event.action}`;
|
|
613
|
+
case "sync:clock-rebase":
|
|
614
|
+
return `clock rebase ${event.rebasedCount} ops (${Math.round(event.maxSkewMs / 1e3)}s ahead)`;
|
|
615
|
+
case "sync:schema-mismatch":
|
|
616
|
+
return `schema mismatch client ${event.clientSchemaVersion} server ${event.serverSchemaVersion}`;
|
|
617
|
+
case "sync:protocol-deprecated":
|
|
618
|
+
return `protocol v${event.clientProtocolVersion} deprecated (node ${event.nodeId})`;
|
|
619
|
+
case "sync:unverified-legacy-operation":
|
|
620
|
+
return `unverified legacy op ${event.operationId} (node ${event.nodeId})`;
|
|
621
|
+
case "sync:forged-duplicate":
|
|
622
|
+
return `forged duplicate refused ${event.operationId} (node ${event.nodeId})`;
|
|
623
|
+
case "sync:apply-failed":
|
|
624
|
+
return `sync apply failed ${event.operationId}`;
|
|
625
|
+
case "sync:apply-blocked":
|
|
626
|
+
return `sync apply blocked ${event.failure.collection}/${event.failure.recordId}`;
|
|
627
|
+
case "sync:apply-retrying":
|
|
628
|
+
return `sync apply retry ${event.failure.collection}/${event.failure.recordId}`;
|
|
629
|
+
case "sync:apply-recovered":
|
|
630
|
+
return `sync apply recovered ${event.failure.collection}/${event.failure.recordId}`;
|
|
631
|
+
case "sync:apply-abandoned":
|
|
632
|
+
return `sync apply abandoned ${event.failure.collection}/${event.failure.recordId}`;
|
|
633
|
+
case "sync:operation-rejected":
|
|
634
|
+
return `sync op rejected ${event.collection}/${event.recordId} (${event.code})`;
|
|
635
|
+
case "sync:sent":
|
|
636
|
+
return `sync sent ${event.batchSize}`;
|
|
637
|
+
case "sync:received":
|
|
638
|
+
return `sync received ${event.batchSize}`;
|
|
639
|
+
case "sync:acknowledged":
|
|
640
|
+
return `sync ack ${event.sequenceNumber}`;
|
|
641
|
+
case "sync:scope-retracted":
|
|
642
|
+
return `scope retracted ${event.collection}/${event.recordId}`;
|
|
643
|
+
case "query:subscribed":
|
|
644
|
+
return `query subscribed ${event.collection}`;
|
|
645
|
+
case "query:invalidated":
|
|
646
|
+
return `query invalidated ${event.queryId}`;
|
|
647
|
+
case "query:executed":
|
|
648
|
+
return `query executed ${event.queryId}`;
|
|
649
|
+
case "query:error":
|
|
650
|
+
return `query error ${event.collection} (${event.phase}): ${event.message}`;
|
|
651
|
+
case "storage:persistence":
|
|
652
|
+
return `storage persistence ${event.state}: ${event.persisted ? "persisted" : "best-effort"}`;
|
|
653
|
+
case "connection:quality":
|
|
654
|
+
return `connection ${event.quality}`;
|
|
655
|
+
case "sync:diagnostics":
|
|
656
|
+
return `sync diagnostics ${event.diagnostics.quality}`;
|
|
657
|
+
case "sync:bandwidth":
|
|
658
|
+
return `bandwidth ${event.direction} ${event.bytesPerSecond}B/s`;
|
|
659
|
+
case "sync:initial-sync-progress":
|
|
660
|
+
return `initial sync ${Math.round(event.progress * 100)}%`;
|
|
661
|
+
case "sync:delivery-gap":
|
|
662
|
+
return `delivery gap expected ${event.expectedBase} received ${event.receivedBase}`;
|
|
663
|
+
case "sync:delivery-stalled":
|
|
664
|
+
return `delivery unacknowledged ${event.watermark} \u2192 ${event.outstandingMaxDeliverySequence} (${event.repeatCount} repeats)`;
|
|
665
|
+
case "sync:durability-degraded":
|
|
666
|
+
return `local durability degraded after ${event.failedAttempts} failed barriers: ${event.message}`;
|
|
667
|
+
case "sync:durability-restored":
|
|
668
|
+
return "local durability restored";
|
|
669
|
+
case "awareness:updated":
|
|
670
|
+
return `awareness ${event.states.size} peers`;
|
|
671
|
+
case "state-machine:transition":
|
|
672
|
+
return `transition ${event.collection} ${event.from} \u2192 ${event.to}`;
|
|
673
|
+
case "state-machine:rejected":
|
|
674
|
+
return `transition rejected ${event.collection} ${event.from} \u2192 ${event.to}`;
|
|
675
|
+
case "store:persistence-error":
|
|
676
|
+
return `store persistence error ${event.message}`;
|
|
677
|
+
case "store:quota-exceeded":
|
|
678
|
+
return "store quota exceeded";
|
|
679
|
+
case "store:log-integrity":
|
|
680
|
+
return `log integrity: ${event.repaired} repaired, ${event.quarantined} quarantined`;
|
|
681
|
+
case "store:rematerialized":
|
|
682
|
+
return `re-materialized ${event.records} records (${event.mode}), ${event.changedRows} changed`;
|
|
683
|
+
case "store:storage-fallback":
|
|
684
|
+
return `store fallback ${event.from} \u2192 ${event.to}`;
|
|
685
|
+
case "store:opfs-unavailable":
|
|
686
|
+
return `store OPFS unavailable (${event.reason}), running in memory`;
|
|
687
|
+
case "store:durability-lost":
|
|
688
|
+
return `store durability lost (${event.reason} during ${event.phase}), writes refused`;
|
|
689
|
+
case "store:db-name-collision":
|
|
690
|
+
return `store db-name collision ${event.dbName}`;
|
|
691
|
+
case "store:storage-blocked":
|
|
692
|
+
return `store ${event.resource} ${event.state === "waiting" ? "blocked by another holder" : "unblocked"}`;
|
|
693
|
+
case "store:storage-migrated":
|
|
694
|
+
return `store migrated ${event.from} \u2192 ${event.to}`;
|
|
695
|
+
case "store:schema-ahead":
|
|
696
|
+
return `store refused: database at schema v${event.storedVersion}, code at v${event.codeVersion}`;
|
|
697
|
+
case "replay:completed":
|
|
698
|
+
return `replay ${event.operationsApplied} ops`;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function timelineColor(type) {
|
|
702
|
+
if (type.startsWith("operation:")) return "#22c55e";
|
|
703
|
+
if (type.startsWith("sync:")) return "#a855f7";
|
|
704
|
+
if (type.startsWith("merge:") || type.startsWith("constraint:")) return "#f59e0b";
|
|
705
|
+
if (type.startsWith("query:")) return "#0ea5e9";
|
|
706
|
+
return "#64748b";
|
|
707
|
+
}
|
|
708
|
+
function extractCausalDependencies(event) {
|
|
709
|
+
const operation = extractOperation(event);
|
|
710
|
+
return operation?.causalDeps ?? [];
|
|
711
|
+
}
|
|
712
|
+
function extractOperation(event) {
|
|
713
|
+
switch (event.type) {
|
|
714
|
+
case "operation:created":
|
|
715
|
+
case "operation:applied":
|
|
716
|
+
return event.operation;
|
|
717
|
+
case "query:invalidated":
|
|
718
|
+
return event.trigger;
|
|
719
|
+
default:
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// src/ui/panel.ts
|
|
725
|
+
var import_htm = __toESM(require("htm"), 1);
|
|
726
|
+
var import_preact = require("preact");
|
|
727
|
+
var import_hooks = require("preact/hooks");
|
|
728
|
+
|
|
729
|
+
// src/ui/components.ts
|
|
730
|
+
function formatTime(timestamp) {
|
|
731
|
+
const d = new Date(timestamp);
|
|
732
|
+
const h2 = String(d.getHours()).padStart(2, "0");
|
|
733
|
+
const m = String(d.getMinutes()).padStart(2, "0");
|
|
734
|
+
const s = String(d.getSeconds()).padStart(2, "0");
|
|
735
|
+
const ms = String(d.getMilliseconds()).padStart(3, "0");
|
|
736
|
+
return `${h2}:${m}:${s}.${ms}`;
|
|
737
|
+
}
|
|
738
|
+
function truncate(str, max) {
|
|
739
|
+
if (str.length <= max) return str;
|
|
740
|
+
return `${str.slice(0, max - 1)}\u2026`;
|
|
741
|
+
}
|
|
742
|
+
function formatValue(value) {
|
|
743
|
+
if (value === null || value === void 0) return "null";
|
|
744
|
+
if (typeof value === "string") return `"${value}"`;
|
|
745
|
+
if (typeof value === "object") return JSON.stringify(value, null, 2);
|
|
746
|
+
return String(value);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// src/ui/panel.ts
|
|
750
|
+
var html = import_htm.default.bind(import_preact.h);
|
|
751
|
+
var ALL_CATEGORIES = ["operation", "merge", "sync", "query", "connection"];
|
|
752
|
+
function DevToolsPanel({ events }) {
|
|
753
|
+
const [activeTab, setActiveTab] = (0, import_hooks.useState)("timeline");
|
|
754
|
+
const [search, setSearch] = (0, import_hooks.useState)("");
|
|
755
|
+
const [categories, setCategories] = (0, import_hooks.useState)(() => new Set(ALL_CATEGORIES));
|
|
756
|
+
const [expanded, setExpanded] = (0, import_hooks.useState)(() => /* @__PURE__ */ new Set());
|
|
757
|
+
const [paused, setPaused] = (0, import_hooks.useState)(false);
|
|
758
|
+
const toggleCategory = (0, import_hooks.useCallback)((cat) => {
|
|
759
|
+
setCategories((prev) => {
|
|
760
|
+
const next = new Set(prev);
|
|
761
|
+
if (next.has(cat)) next.delete(cat);
|
|
762
|
+
else next.add(cat);
|
|
763
|
+
return next;
|
|
764
|
+
});
|
|
765
|
+
}, []);
|
|
766
|
+
const toggleExpand = (0, import_hooks.useCallback)((id) => {
|
|
767
|
+
setExpanded((prev) => {
|
|
768
|
+
const next = new Set(prev);
|
|
769
|
+
if (next.has(id)) next.delete(id);
|
|
770
|
+
else next.add(id);
|
|
771
|
+
return next;
|
|
772
|
+
});
|
|
773
|
+
}, []);
|
|
774
|
+
const filtered = (0, import_hooks.useMemo)(
|
|
775
|
+
() => filterEvents(events, { categories: [...categories] }),
|
|
776
|
+
[events, categories]
|
|
777
|
+
);
|
|
778
|
+
const model = (0, import_hooks.useMemo)(() => buildPanelModel(filtered), [filtered]);
|
|
779
|
+
const stats = (0, import_hooks.useMemo)(() => computeStatistics(events), [events]);
|
|
780
|
+
return html`
|
|
781
|
+
<div class="kora-devtools">
|
|
782
|
+
<${Toolbar}
|
|
783
|
+
activeTab=${activeTab}
|
|
784
|
+
onTabChange=${setActiveTab}
|
|
785
|
+
totalEvents=${events.length}
|
|
786
|
+
conflicts=${stats.mergeConflicts}
|
|
787
|
+
search=${search}
|
|
788
|
+
onSearchChange=${setSearch}
|
|
789
|
+
paused=${paused}
|
|
790
|
+
onPauseToggle=${() => setPaused((p) => !p)}
|
|
791
|
+
onClear=${() => setExpanded(/* @__PURE__ */ new Set())}
|
|
792
|
+
categories=${categories}
|
|
793
|
+
onToggleCategory=${toggleCategory}
|
|
794
|
+
/>
|
|
795
|
+
<div class="kora-content">
|
|
796
|
+
${activeTab === "timeline" && html`<${TimelinePanel}
|
|
797
|
+
items=${model.timeline}
|
|
798
|
+
search=${search}
|
|
799
|
+
expanded=${expanded}
|
|
800
|
+
onToggle=${toggleExpand}
|
|
801
|
+
/>`}
|
|
802
|
+
${activeTab === "conflicts" && html`<${ConflictsPanel}
|
|
803
|
+
items=${model.conflicts}
|
|
804
|
+
search=${search}
|
|
805
|
+
expanded=${expanded}
|
|
806
|
+
onToggle=${toggleExpand}
|
|
807
|
+
/>`}
|
|
808
|
+
${activeTab === "operations" && html`<${OperationsPanel}
|
|
809
|
+
items=${model.operations}
|
|
810
|
+
search=${search}
|
|
811
|
+
expanded=${expanded}
|
|
812
|
+
onToggle=${toggleExpand}
|
|
813
|
+
/>`}
|
|
814
|
+
${activeTab === "network" && html`<${NetworkPanel} network=${model.network} events=${events} />`}
|
|
815
|
+
</div>
|
|
816
|
+
</div>
|
|
817
|
+
`;
|
|
818
|
+
}
|
|
819
|
+
function Toolbar({
|
|
820
|
+
activeTab,
|
|
821
|
+
onTabChange,
|
|
822
|
+
totalEvents,
|
|
823
|
+
conflicts,
|
|
824
|
+
search,
|
|
825
|
+
onSearchChange,
|
|
826
|
+
paused,
|
|
827
|
+
onPauseToggle,
|
|
828
|
+
onClear,
|
|
829
|
+
categories,
|
|
830
|
+
onToggleCategory
|
|
831
|
+
}) {
|
|
832
|
+
const tabs = [
|
|
833
|
+
{ id: "timeline", label: "Timeline", badge: totalEvents },
|
|
834
|
+
{ id: "conflicts", label: "Conflicts", badge: conflicts },
|
|
835
|
+
{ id: "operations", label: "Operations" },
|
|
836
|
+
{ id: "network", label: "Network" }
|
|
837
|
+
];
|
|
838
|
+
return html`
|
|
839
|
+
<div class="kora-toolbar">
|
|
840
|
+
<div class="kora-tabs">
|
|
841
|
+
${tabs.map(
|
|
842
|
+
(tab) => html`
|
|
843
|
+
<button
|
|
844
|
+
class="kora-tab${activeTab === tab.id ? " active" : ""}"
|
|
845
|
+
onClick=${() => onTabChange(tab.id)}
|
|
846
|
+
>
|
|
847
|
+
${tab.label}
|
|
848
|
+
${tab.badge != null && tab.badge > 0 ? html`<span class="kora-badge">${tab.badge}</span>` : null}
|
|
849
|
+
</button>
|
|
850
|
+
`
|
|
851
|
+
)}
|
|
852
|
+
</div>
|
|
853
|
+
<div class="kora-controls">
|
|
854
|
+
<input
|
|
855
|
+
type="text"
|
|
856
|
+
class="kora-search"
|
|
857
|
+
placeholder="Filter..."
|
|
858
|
+
value=${search}
|
|
859
|
+
onInput=${(e) => onSearchChange(e.target.value)}
|
|
860
|
+
/>
|
|
861
|
+
<button class="kora-btn" onClick=${onPauseToggle}>
|
|
862
|
+
${paused ? "Resume" : "Pause"}
|
|
863
|
+
</button>
|
|
864
|
+
<button class="kora-btn" onClick=${onClear}>Clear</button>
|
|
865
|
+
${ALL_CATEGORIES.map(
|
|
866
|
+
(cat) => html`
|
|
867
|
+
<button
|
|
868
|
+
class="kora-cat-toggle${categories.has(cat) ? " active" : ""}"
|
|
869
|
+
onClick=${() => onToggleCategory(cat)}
|
|
870
|
+
>
|
|
871
|
+
${cat}
|
|
872
|
+
</button>
|
|
873
|
+
`
|
|
874
|
+
)}
|
|
875
|
+
</div>
|
|
876
|
+
</div>
|
|
877
|
+
`;
|
|
878
|
+
}
|
|
879
|
+
function TimelinePanel({
|
|
880
|
+
items,
|
|
881
|
+
search,
|
|
882
|
+
expanded,
|
|
883
|
+
onToggle
|
|
884
|
+
}) {
|
|
885
|
+
const lowerSearch = search.toLowerCase();
|
|
886
|
+
const filtered = lowerSearch ? items.filter(
|
|
887
|
+
(i) => i.label.toLowerCase().includes(lowerSearch) || i.type.toLowerCase().includes(lowerSearch)
|
|
888
|
+
) : items;
|
|
889
|
+
const visible = filtered.slice(-200).reverse();
|
|
890
|
+
if (visible.length === 0) {
|
|
891
|
+
return html`<div class="kora-panel"><div class="kora-empty">No events recorded yet.</div></div>`;
|
|
892
|
+
}
|
|
893
|
+
return html`
|
|
894
|
+
<div class="kora-panel kora-timeline">
|
|
895
|
+
<div class="kora-summary">Showing ${visible.length} of ${items.length} events</div>
|
|
896
|
+
<div class="kora-event-list">
|
|
897
|
+
${visible.map(
|
|
898
|
+
(item) => html`
|
|
899
|
+
<div
|
|
900
|
+
key=${item.id}
|
|
901
|
+
class="kora-row${expanded.has(item.id) ? " expanded" : ""}"
|
|
902
|
+
onClick=${() => onToggle(item.id)}
|
|
903
|
+
>
|
|
904
|
+
<span class="kora-time">${formatTime(item.receivedAt)}</span>
|
|
905
|
+
<span class="kora-dot" style="background:${item.color}"></span>
|
|
906
|
+
<span class="kora-type">${item.type}</span>
|
|
907
|
+
<span class="kora-label">${item.label}</span>
|
|
908
|
+
${item.dependsOn.length > 0 ? html`<span class="kora-deps">deps: ${item.dependsOn.length}</span>` : null}
|
|
909
|
+
</div>
|
|
910
|
+
${expanded.has(item.id) ? html`
|
|
911
|
+
<div class="kora-detail">
|
|
912
|
+
<div>
|
|
913
|
+
<span class="kora-detail-label">Event ID:</span> ${item.id}
|
|
914
|
+
</div>
|
|
915
|
+
<div>
|
|
916
|
+
<span class="kora-detail-label">Received:</span>
|
|
917
|
+
${new Date(item.receivedAt).toISOString()}
|
|
918
|
+
</div>
|
|
919
|
+
${item.dependsOn.length > 0 ? html`<div>
|
|
920
|
+
<span class="kora-detail-label">Causal deps:</span>
|
|
921
|
+
${item.dependsOn.join(", ")}
|
|
922
|
+
</div>` : null}
|
|
923
|
+
</div>
|
|
924
|
+
` : null}
|
|
925
|
+
`
|
|
926
|
+
)}
|
|
927
|
+
</div>
|
|
928
|
+
</div>
|
|
929
|
+
`;
|
|
930
|
+
}
|
|
931
|
+
function ConflictsPanel({
|
|
932
|
+
items,
|
|
933
|
+
search,
|
|
934
|
+
expanded,
|
|
935
|
+
onToggle
|
|
936
|
+
}) {
|
|
937
|
+
const lowerSearch = search.toLowerCase();
|
|
938
|
+
const filtered = lowerSearch ? items.filter(
|
|
939
|
+
(i) => i.collection.toLowerCase().includes(lowerSearch) || i.field.toLowerCase().includes(lowerSearch) || i.strategy.toLowerCase().includes(lowerSearch)
|
|
940
|
+
) : items;
|
|
941
|
+
const visible = filtered.slice(-100).reverse();
|
|
942
|
+
if (visible.length === 0) {
|
|
943
|
+
return html`<div class="kora-panel"><div class="kora-empty">No merge conflicts detected.</div></div>`;
|
|
944
|
+
}
|
|
945
|
+
return html`
|
|
946
|
+
<div class="kora-panel kora-conflicts">
|
|
947
|
+
<div class="kora-summary">
|
|
948
|
+
${items.length} conflict${items.length !== 1 ? "s" : ""}
|
|
949
|
+
</div>
|
|
950
|
+
<table class="kora-table">
|
|
951
|
+
<thead>
|
|
952
|
+
<tr>
|
|
953
|
+
<th>Time</th>
|
|
954
|
+
<th>Collection</th>
|
|
955
|
+
<th>Field</th>
|
|
956
|
+
<th>Strategy</th>
|
|
957
|
+
<th>Tier</th>
|
|
958
|
+
<th>Result</th>
|
|
959
|
+
</tr>
|
|
960
|
+
</thead>
|
|
961
|
+
<tbody>
|
|
962
|
+
${visible.map(
|
|
963
|
+
(item) => html`
|
|
964
|
+
<tr
|
|
965
|
+
key=${item.id}
|
|
966
|
+
class="kora-conflict-row${item.constraintViolated ? " violated" : ""}${expanded.has(item.id) ? " expanded" : ""}"
|
|
967
|
+
onClick=${() => onToggle(item.id)}
|
|
968
|
+
>
|
|
969
|
+
<td>${formatTime(item.timestamp)}</td>
|
|
970
|
+
<td>${item.collection}</td>
|
|
971
|
+
<td class="kora-mono">${item.field}</td>
|
|
972
|
+
<td>
|
|
973
|
+
<span class="kora-strategy tier-${item.tier}">${item.strategy}</span>
|
|
974
|
+
</td>
|
|
975
|
+
<td>${item.tier}</td>
|
|
976
|
+
<td class="kora-mono">${truncate(String(item.output), 30)}</td>
|
|
977
|
+
</tr>
|
|
978
|
+
${expanded.has(item.id) ? html`
|
|
979
|
+
<tr class="kora-detail-row">
|
|
980
|
+
<td colspan="6">
|
|
981
|
+
<div class="kora-conflict-detail">
|
|
982
|
+
<div class="kora-comparison">
|
|
983
|
+
<div class="kora-value-box">
|
|
984
|
+
<div class="kora-value-label">Input A (local)</div>
|
|
985
|
+
<pre class="kora-value-code">
|
|
986
|
+
${formatValue(item.inputA)}</pre
|
|
987
|
+
>
|
|
988
|
+
</div>
|
|
989
|
+
<div class="kora-value-box">
|
|
990
|
+
<div class="kora-value-label">Input B (remote)</div>
|
|
991
|
+
<pre class="kora-value-code">
|
|
992
|
+
${formatValue(item.inputB)}</pre
|
|
993
|
+
>
|
|
994
|
+
</div>
|
|
995
|
+
<div class="kora-value-box result">
|
|
996
|
+
<div class="kora-value-label">Result</div>
|
|
997
|
+
<pre class="kora-value-code">
|
|
998
|
+
${formatValue(item.output)}</pre
|
|
999
|
+
>
|
|
1000
|
+
</div>
|
|
1001
|
+
</div>
|
|
1002
|
+
${item.constraintViolated ? html`<div class="kora-constraint-warning">
|
|
1003
|
+
Constraint violated: ${item.constraintViolated}
|
|
1004
|
+
</div>` : null}
|
|
1005
|
+
</div>
|
|
1006
|
+
</td>
|
|
1007
|
+
</tr>
|
|
1008
|
+
` : null}
|
|
1009
|
+
`
|
|
1010
|
+
)}
|
|
1011
|
+
</tbody>
|
|
1012
|
+
</table>
|
|
1013
|
+
</div>
|
|
1014
|
+
`;
|
|
1015
|
+
}
|
|
1016
|
+
function OperationsPanel({
|
|
1017
|
+
items,
|
|
1018
|
+
search,
|
|
1019
|
+
expanded,
|
|
1020
|
+
onToggle
|
|
1021
|
+
}) {
|
|
1022
|
+
const lowerSearch = search.toLowerCase();
|
|
1023
|
+
const filtered = lowerSearch ? items.filter(
|
|
1024
|
+
(i) => i.collection.toLowerCase().includes(lowerSearch) || i.recordId.toLowerCase().includes(lowerSearch) || i.operationId.toLowerCase().includes(lowerSearch) || i.opType.toLowerCase().includes(lowerSearch)
|
|
1025
|
+
) : items;
|
|
1026
|
+
const visible = filtered.slice(-200).reverse();
|
|
1027
|
+
if (visible.length === 0) {
|
|
1028
|
+
return html`<div class="kora-panel"><div class="kora-empty">No operations recorded yet.</div></div>`;
|
|
1029
|
+
}
|
|
1030
|
+
return html`
|
|
1031
|
+
<div class="kora-panel kora-operations">
|
|
1032
|
+
<div class="kora-summary">
|
|
1033
|
+
Showing ${visible.length} of ${items.length} operations
|
|
1034
|
+
</div>
|
|
1035
|
+
<div class="kora-op-list">
|
|
1036
|
+
${visible.map(
|
|
1037
|
+
(item) => html`
|
|
1038
|
+
<div
|
|
1039
|
+
key=${item.id}
|
|
1040
|
+
class="kora-op-row${expanded.has(item.id) ? " expanded" : ""}"
|
|
1041
|
+
onClick=${() => onToggle(item.id)}
|
|
1042
|
+
>
|
|
1043
|
+
<span class="kora-time">${formatTime(item.timestamp)}</span>
|
|
1044
|
+
<span class="kora-op-type op-${item.opType}">${item.opType}</span>
|
|
1045
|
+
<span class="kora-op-collection">${item.collection}</span>
|
|
1046
|
+
<span class="kora-op-record kora-mono"
|
|
1047
|
+
>${truncate(item.recordId, 12)}</span
|
|
1048
|
+
>
|
|
1049
|
+
<span class="kora-op-node">node:${truncate(item.nodeId, 8)}</span>
|
|
1050
|
+
<span class="kora-op-seq">#${item.sequenceNumber}</span>
|
|
1051
|
+
</div>
|
|
1052
|
+
${expanded.has(item.id) ? html`
|
|
1053
|
+
<div class="kora-op-detail">
|
|
1054
|
+
<div>
|
|
1055
|
+
<span class="kora-detail-label">Operation ID:</span>
|
|
1056
|
+
<span class="kora-mono">${item.operationId}</span>
|
|
1057
|
+
</div>
|
|
1058
|
+
<div>
|
|
1059
|
+
<span class="kora-detail-label">Node:</span>
|
|
1060
|
+
<span class="kora-mono">${item.nodeId}</span>
|
|
1061
|
+
</div>
|
|
1062
|
+
<div>
|
|
1063
|
+
<span class="kora-detail-label">Sequence:</span>
|
|
1064
|
+
${item.sequenceNumber}
|
|
1065
|
+
</div>
|
|
1066
|
+
${item.causalDeps.length > 0 ? html`<div>
|
|
1067
|
+
<span class="kora-detail-label">Causal deps:</span>
|
|
1068
|
+
<span class="kora-mono"
|
|
1069
|
+
>${item.causalDeps.join(", ")}</span
|
|
1070
|
+
>
|
|
1071
|
+
</div>` : null}
|
|
1072
|
+
${item.data ? html`<div>
|
|
1073
|
+
<div class="kora-detail-label">Data:</div>
|
|
1074
|
+
<pre class="kora-value-code">
|
|
1075
|
+
${JSON.stringify(item.data, null, 2)}</pre
|
|
1076
|
+
>
|
|
1077
|
+
</div>` : null}
|
|
1078
|
+
</div>
|
|
1079
|
+
` : null}
|
|
1080
|
+
`
|
|
1081
|
+
)}
|
|
1082
|
+
</div>
|
|
1083
|
+
</div>
|
|
1084
|
+
`;
|
|
1085
|
+
}
|
|
1086
|
+
function NetworkPanel({
|
|
1087
|
+
network,
|
|
1088
|
+
events
|
|
1089
|
+
}) {
|
|
1090
|
+
const statusClass = network.connected ? "connected" : "disconnected";
|
|
1091
|
+
const statusText = network.connected ? network.quality ? `Connected (${network.quality})` : "Connected" : "Disconnected";
|
|
1092
|
+
const syncEvents = events.filter((e) => {
|
|
1093
|
+
const cat = eventTypeToCategory(e.event.type);
|
|
1094
|
+
return cat === "sync" || cat === "connection";
|
|
1095
|
+
}).slice(-50).reverse();
|
|
1096
|
+
return html`
|
|
1097
|
+
<div class="kora-panel kora-network">
|
|
1098
|
+
<div class="kora-network-status">
|
|
1099
|
+
<div class="kora-status-indicator ${statusClass}">
|
|
1100
|
+
<span class="kora-status-dot"></span>
|
|
1101
|
+
<span class="kora-status-text">${statusText}</span>
|
|
1102
|
+
</div>
|
|
1103
|
+
</div>
|
|
1104
|
+
|
|
1105
|
+
<div class="kora-stats-grid">
|
|
1106
|
+
<${StatCard} label="Sent" value=${String(network.sentOps)} unit="ops" />
|
|
1107
|
+
<${StatCard} label="Received" value=${String(network.receivedOps)} unit="ops" />
|
|
1108
|
+
<${StatCard} label="Pending" value=${String(network.pendingAcks)} unit="acks" />
|
|
1109
|
+
<${StatCard}
|
|
1110
|
+
label="Last Sync"
|
|
1111
|
+
value=${network.lastSyncAt ? formatTime(network.lastSyncAt) : "never"}
|
|
1112
|
+
/>
|
|
1113
|
+
</div>
|
|
1114
|
+
|
|
1115
|
+
${network.versionVector.length > 0 ? html`
|
|
1116
|
+
<div class="kora-vv-section">
|
|
1117
|
+
<h3>Version Vector</h3>
|
|
1118
|
+
<table class="kora-table kora-vv-table">
|
|
1119
|
+
<thead>
|
|
1120
|
+
<tr>
|
|
1121
|
+
<th>Node</th>
|
|
1122
|
+
<th>Sequence</th>
|
|
1123
|
+
</tr>
|
|
1124
|
+
</thead>
|
|
1125
|
+
<tbody>
|
|
1126
|
+
${network.versionVector.map(
|
|
1127
|
+
(entry) => html`
|
|
1128
|
+
<tr key=${entry.nodeId}>
|
|
1129
|
+
<td class="kora-mono">${truncate(entry.nodeId, 16)}</td>
|
|
1130
|
+
<td>${entry.sequenceNumber}</td>
|
|
1131
|
+
</tr>
|
|
1132
|
+
`
|
|
1133
|
+
)}
|
|
1134
|
+
</tbody>
|
|
1135
|
+
</table>
|
|
1136
|
+
</div>
|
|
1137
|
+
` : null}
|
|
1138
|
+
|
|
1139
|
+
${syncEvents.length > 0 ? html`
|
|
1140
|
+
<div class="kora-sync-log">
|
|
1141
|
+
<h3>Recent Sync Activity</h3>
|
|
1142
|
+
<div class="kora-event-list">
|
|
1143
|
+
${syncEvents.map(
|
|
1144
|
+
(entry) => html`
|
|
1145
|
+
<div key=${entry.id} class="kora-row compact">
|
|
1146
|
+
<span class="kora-time"
|
|
1147
|
+
>${formatTime(entry.receivedAt)}</span
|
|
1148
|
+
>
|
|
1149
|
+
<span class="kora-type">${entry.event.type}</span>
|
|
1150
|
+
</div>
|
|
1151
|
+
`
|
|
1152
|
+
)}
|
|
1153
|
+
</div>
|
|
1154
|
+
</div>
|
|
1155
|
+
` : null}
|
|
1156
|
+
</div>
|
|
1157
|
+
`;
|
|
1158
|
+
}
|
|
1159
|
+
function StatCard({
|
|
1160
|
+
label,
|
|
1161
|
+
value,
|
|
1162
|
+
unit
|
|
1163
|
+
}) {
|
|
1164
|
+
return html`
|
|
1165
|
+
<div class="kora-stat-card">
|
|
1166
|
+
<div class="kora-stat-value">${value}</div>
|
|
1167
|
+
<div class="kora-stat-label">${label}${unit ? ` (${unit})` : ""}</div>
|
|
1168
|
+
</div>
|
|
1169
|
+
`;
|
|
1170
|
+
}
|
|
1171
|
+
function renderDevtoolsPanel(target, events) {
|
|
1172
|
+
(0, import_preact.render)(html`<${DevToolsPanel} events=${events} />`, target);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// src/ui/panel-styles.ts
|
|
1176
|
+
var KORA_DEVTOOLS_STYLES = "/* ================================================================\n Kora DevTools \u2014 Dark theme optimized for Chrome DevTools panels\n ================================================================ */\n\n :host,\n :root {\n --bg-primary: #1e1e2e;\n --bg-secondary: #181825;\n --bg-surface: #313244;\n --bg-hover: #45475a;\n --bg-selected: #585b70;\n --text-primary: #cdd6f4;\n --text-secondary: #a6adc8;\n --text-muted: #6c7086;\n --border: #45475a;\n\n --green: #a6e3a1;\n --red: #f38ba8;\n --yellow: #f9e2af;\n --blue: #89b4fa;\n --purple: #cba6f7;\n --teal: #94e2d5;\n --peach: #fab387;\n\n --font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;\n --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n --font-size: 12px;\n }\n\n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n\n :host,\n body {\n background: var(--bg-primary);\n color: var(--text-primary);\n font-family: var(--font-sans);\n font-size: var(--font-size);\n line-height: 1.5;\n overflow: hidden;\n }\n\n #kora-devtools-root {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n }\n\n .kora-devtools {\n display: flex;\n flex-direction: column;\n height: 100%;\n }\n\n /* ---- Toolbar ---- */\n\n .kora-toolbar {\n background: var(--bg-secondary);\n border-bottom: 1px solid var(--border);\n padding: 4px 8px;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n }\n\n .kora-tabs {\n display: flex;\n gap: 2px;\n }\n\n .kora-tab {\n background: transparent;\n color: var(--text-secondary);\n border: none;\n padding: 4px 12px;\n border-radius: 4px;\n cursor: pointer;\n font-size: var(--font-size);\n font-family: var(--font-sans);\n display: flex;\n align-items: center;\n gap: 6px;\n }\n\n .kora-tab:hover {\n background: var(--bg-hover);\n color: var(--text-primary);\n }\n\n .kora-tab.active {\n background: var(--bg-surface);\n color: var(--text-primary);\n font-weight: 600;\n }\n\n .kora-badge {\n background: var(--bg-hover);\n color: var(--text-muted);\n padding: 0 6px;\n border-radius: 10px;\n font-size: 10px;\n font-weight: 500;\n }\n\n .kora-tab.active .kora-badge {\n background: var(--blue);\n color: var(--bg-primary);\n }\n\n .kora-controls {\n display: flex;\n gap: 4px;\n align-items: center;\n margin-left: auto;\n flex-wrap: wrap;\n }\n\n .kora-search {\n background: var(--bg-surface);\n color: var(--text-primary);\n border: 1px solid var(--border);\n padding: 3px 8px;\n border-radius: 4px;\n font-size: var(--font-size);\n font-family: var(--font-sans);\n width: 140px;\n outline: none;\n }\n\n .kora-search:focus {\n border-color: var(--blue);\n }\n\n .kora-btn {\n background: var(--bg-surface);\n color: var(--text-secondary);\n border: 1px solid var(--border);\n padding: 3px 8px;\n border-radius: 4px;\n cursor: pointer;\n font-size: 11px;\n font-family: var(--font-sans);\n }\n\n .kora-btn:hover {\n background: var(--bg-hover);\n color: var(--text-primary);\n }\n\n .kora-cat-toggle {\n background: transparent;\n color: var(--text-muted);\n border: 1px solid transparent;\n padding: 2px 6px;\n border-radius: 3px;\n cursor: pointer;\n font-size: 10px;\n font-family: var(--font-sans);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n\n .kora-cat-toggle:hover {\n color: var(--text-secondary);\n }\n\n .kora-cat-toggle.active {\n background: var(--bg-surface);\n color: var(--text-primary);\n border-color: var(--border);\n }\n\n /* ---- Content ---- */\n\n .kora-content {\n flex: 1;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n }\n\n .kora-panel {\n flex: 1;\n overflow-y: auto;\n padding: 8px;\n }\n\n .kora-empty {\n color: var(--text-muted);\n text-align: center;\n padding: 40px;\n font-style: italic;\n }\n\n .kora-summary {\n color: var(--text-muted);\n font-size: 11px;\n padding: 4px 0 8px;\n border-bottom: 1px solid var(--border);\n margin-bottom: 4px;\n }\n\n /* ---- Event List (Timeline) ---- */\n\n .kora-event-list {\n display: flex;\n flex-direction: column;\n }\n\n .kora-row {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 3px 6px;\n border-radius: 3px;\n cursor: pointer;\n min-height: 24px;\n }\n\n .kora-row:hover {\n background: var(--bg-hover);\n }\n\n .kora-row.expanded,\n .kora-row.selected {\n background: var(--bg-surface);\n }\n\n .kora-row.compact {\n padding: 2px 6px;\n font-size: 11px;\n }\n\n .kora-time {\n color: var(--text-muted);\n font-family: var(--font-mono);\n font-size: 11px;\n flex-shrink: 0;\n width: 90px;\n }\n\n .kora-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex-shrink: 0;\n }\n\n .kora-type {\n color: var(--text-secondary);\n font-size: 11px;\n flex-shrink: 0;\n width: 140px;\n }\n\n .kora-label {\n color: var(--text-primary);\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .kora-deps {\n color: var(--peach);\n font-size: 10px;\n flex-shrink: 0;\n }\n\n /* ---- Detail / Expandable ---- */\n\n .kora-detail,\n .kora-op-detail {\n background: var(--bg-secondary);\n border: 1px solid var(--border);\n border-radius: 4px;\n padding: 8px 12px;\n margin: 2px 0 6px 28px;\n font-size: 11px;\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n\n .kora-detail-label {\n color: var(--text-muted);\n font-weight: 500;\n margin-right: 4px;\n }\n\n .kora-mono {\n font-family: var(--font-mono);\n font-size: 11px;\n }\n\n /* ---- Conflicts Table ---- */\n\n .kora-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 11px;\n }\n\n .kora-table th {\n text-align: left;\n color: var(--text-muted);\n font-weight: 500;\n padding: 6px 8px;\n border-bottom: 1px solid var(--border);\n text-transform: uppercase;\n font-size: 10px;\n letter-spacing: 0.5px;\n }\n\n .kora-table td {\n padding: 5px 8px;\n border-bottom: 1px solid var(--bg-surface);\n }\n\n .kora-conflict-row {\n cursor: pointer;\n }\n\n .kora-conflict-row:hover td {\n background: var(--bg-hover);\n }\n\n .kora-conflict-row.expanded td {\n background: var(--bg-surface);\n }\n\n .kora-conflict-row.violated td {\n border-left: 3px solid var(--red);\n }\n\n .kora-strategy {\n padding: 1px 6px;\n border-radius: 3px;\n font-size: 10px;\n font-weight: 500;\n }\n\n .kora-strategy.tier-1 {\n background: rgba(166, 227, 161, 0.15);\n color: var(--green);\n }\n\n .kora-strategy.tier-2 {\n background: rgba(249, 226, 175, 0.15);\n color: var(--yellow);\n }\n\n .kora-strategy.tier-3 {\n background: rgba(203, 166, 247, 0.15);\n color: var(--purple);\n }\n\n .kora-detail-row td {\n padding: 0;\n }\n\n .kora-conflict-detail {\n padding: 12px;\n }\n\n .kora-comparison {\n display: grid;\n grid-template-columns: 1fr 1fr 1fr;\n gap: 8px;\n }\n\n .kora-value-box {\n background: var(--bg-primary);\n border: 1px solid var(--border);\n border-radius: 4px;\n padding: 8px;\n }\n\n .kora-value-box.result {\n border-color: var(--green);\n }\n\n .kora-value-label {\n color: var(--text-muted);\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n margin-bottom: 4px;\n }\n\n .kora-value-code {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-primary);\n white-space: pre-wrap;\n word-break: break-all;\n margin: 0;\n }\n\n .kora-constraint-warning {\n margin-top: 8px;\n padding: 6px 10px;\n background: rgba(243, 139, 168, 0.1);\n border: 1px solid var(--red);\n border-radius: 4px;\n color: var(--red);\n font-size: 11px;\n }\n\n /* ---- Operations ---- */\n\n .kora-op-list {\n display: flex;\n flex-direction: column;\n }\n\n .kora-op-row {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 3px 6px;\n border-radius: 3px;\n cursor: pointer;\n }\n\n .kora-op-row:hover {\n background: var(--bg-hover);\n }\n\n .kora-op-row.expanded {\n background: var(--bg-surface);\n }\n\n .kora-op-type {\n padding: 1px 6px;\n border-radius: 3px;\n font-size: 10px;\n font-weight: 600;\n text-transform: uppercase;\n flex-shrink: 0;\n }\n\n .op-insert {\n background: rgba(166, 227, 161, 0.15);\n color: var(--green);\n }\n\n .op-update {\n background: rgba(137, 180, 250, 0.15);\n color: var(--blue);\n }\n\n .op-delete {\n background: rgba(243, 139, 168, 0.15);\n color: var(--red);\n }\n\n .kora-op-collection {\n color: var(--teal);\n font-weight: 500;\n flex-shrink: 0;\n }\n\n .kora-op-record {\n color: var(--text-secondary);\n flex-shrink: 0;\n }\n\n .kora-op-node {\n color: var(--text-muted);\n font-size: 10px;\n flex-shrink: 0;\n }\n\n .kora-op-seq {\n color: var(--text-muted);\n font-size: 10px;\n flex-shrink: 0;\n }\n\n /* ---- Network ---- */\n\n .kora-network-status {\n padding: 12px 0;\n }\n\n .kora-status-indicator {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 14px;\n font-weight: 600;\n }\n\n .kora-status-dot {\n width: 12px;\n height: 12px;\n border-radius: 50%;\n }\n\n .connected .kora-status-dot {\n background: var(--green);\n box-shadow: 0 0 8px rgba(166, 227, 161, 0.5);\n }\n\n .disconnected .kora-status-dot {\n background: var(--red);\n box-shadow: 0 0 8px rgba(243, 139, 168, 0.3);\n }\n\n .connected .kora-status-text {\n color: var(--green);\n }\n\n .disconnected .kora-status-text {\n color: var(--red);\n }\n\n .kora-stats-grid {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 8px;\n margin: 12px 0;\n }\n\n .kora-stat-card {\n background: var(--bg-surface);\n border: 1px solid var(--border);\n border-radius: 6px;\n padding: 12px;\n text-align: center;\n }\n\n .kora-stat-value {\n font-size: 20px;\n font-weight: 700;\n color: var(--text-primary);\n font-family: var(--font-mono);\n }\n\n .kora-stat-label {\n color: var(--text-muted);\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n margin-top: 2px;\n }\n\n .kora-vv-section,\n .kora-sync-log {\n margin-top: 16px;\n }\n\n .kora-vv-section h3,\n .kora-sync-log h3 {\n color: var(--text-secondary);\n font-size: 12px;\n font-weight: 600;\n margin-bottom: 8px;\n }\n\n .kora-vv-table {\n max-width: 400px;\n }\n\n /* ---- Scrollbar ---- */\n\n ::-webkit-scrollbar {\n width: 8px;\n }\n\n ::-webkit-scrollbar-track {\n background: var(--bg-primary);\n }\n\n ::-webkit-scrollbar-thumb {\n background: var(--bg-hover);\n border-radius: 4px;\n }\n\n ::-webkit-scrollbar-thumb:hover {\n background: var(--bg-selected);\n }";
|
|
1177
|
+
|
|
1178
|
+
// src/overlay/embed-overlay.ts
|
|
1179
|
+
var OVERLAY_HOST_ID = "kora-devtools-overlay-host";
|
|
1180
|
+
function mountKoraDevtoolsOverlay(instrumenter) {
|
|
1181
|
+
if (typeof document === "undefined") {
|
|
1182
|
+
return () => {
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
const existing = document.getElementById(OVERLAY_HOST_ID);
|
|
1186
|
+
if (existing) {
|
|
1187
|
+
existing.remove();
|
|
1188
|
+
}
|
|
1189
|
+
const host = document.createElement("div");
|
|
1190
|
+
host.id = OVERLAY_HOST_ID;
|
|
1191
|
+
host.style.cssText = "position:fixed;inset:auto 12px 12px 12px;height:42vh;z-index:2147483646;display:none;";
|
|
1192
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
1193
|
+
const style = document.createElement("style");
|
|
1194
|
+
style.textContent = `${KORA_DEVTOOLS_STYLES}
|
|
1195
|
+
:host {
|
|
1196
|
+
color-scheme: dark;
|
|
1197
|
+
background: #1e1e2e;
|
|
1198
|
+
color: #cdd6f4;
|
|
1199
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
1200
|
+
overflow: hidden;
|
|
1201
|
+
border: 1px solid #45475a;
|
|
1202
|
+
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
|
|
1203
|
+
}
|
|
1204
|
+
.kora-overlay-close {
|
|
1205
|
+
position: absolute;
|
|
1206
|
+
top: 6px;
|
|
1207
|
+
right: 8px;
|
|
1208
|
+
z-index: 1;
|
|
1209
|
+
background: #313244;
|
|
1210
|
+
color: #cdd6f4;
|
|
1211
|
+
border: 1px solid #45475a;
|
|
1212
|
+
border-radius: 4px;
|
|
1213
|
+
cursor: pointer;
|
|
1214
|
+
font: 12px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
1215
|
+
line-height: 1;
|
|
1216
|
+
padding: 4px 8px;
|
|
1217
|
+
}
|
|
1218
|
+
.kora-overlay-close:hover {
|
|
1219
|
+
background: #45475a;
|
|
1220
|
+
}
|
|
1221
|
+
`;
|
|
1222
|
+
shadow.appendChild(style);
|
|
1223
|
+
const closeButton = document.createElement("button");
|
|
1224
|
+
closeButton.type = "button";
|
|
1225
|
+
closeButton.className = "kora-overlay-close";
|
|
1226
|
+
closeButton.textContent = "Close";
|
|
1227
|
+
closeButton.setAttribute("aria-label", "Close Kora DevTools");
|
|
1228
|
+
shadow.appendChild(closeButton);
|
|
1229
|
+
const panelRoot = document.createElement("div");
|
|
1230
|
+
panelRoot.id = "kora-devtools-root";
|
|
1231
|
+
panelRoot.style.height = "100%";
|
|
1232
|
+
shadow.appendChild(panelRoot);
|
|
1233
|
+
const hint = document.createElement("div");
|
|
1234
|
+
hint.textContent = "Kora DevTools (Ctrl+Shift+K)";
|
|
1235
|
+
hint.style.cssText = "position:fixed;bottom:8px;right:12px;z-index:2147483645;font:11px sans-serif;color:#6c7086;background:#1e1e2e;padding:4px 8px;border-radius:4px;opacity:0.85;";
|
|
1236
|
+
document.body.appendChild(hint);
|
|
1237
|
+
document.body.appendChild(host);
|
|
1238
|
+
let visible = false;
|
|
1239
|
+
const setVisible = (next) => {
|
|
1240
|
+
visible = next;
|
|
1241
|
+
host.style.display = visible ? "block" : "none";
|
|
1242
|
+
hint.textContent = visible ? "Hide Kora DevTools (Esc)" : "Kora DevTools (Ctrl+Shift+K)";
|
|
1243
|
+
hint.style.display = "block";
|
|
1244
|
+
};
|
|
1245
|
+
const refresh = () => {
|
|
1246
|
+
if (!visible) return;
|
|
1247
|
+
renderDevtoolsPanel(panelRoot, instrumenter.getBuffer().getAll());
|
|
1248
|
+
};
|
|
1249
|
+
const intervalId = window.setInterval(refresh, 300);
|
|
1250
|
+
const onKeyDown = (event) => {
|
|
1251
|
+
const isToggle = event.key === "K" && event.shiftKey && (event.ctrlKey || event.metaKey);
|
|
1252
|
+
if (isToggle) {
|
|
1253
|
+
event.preventDefault();
|
|
1254
|
+
setVisible(!visible);
|
|
1255
|
+
if (visible) {
|
|
1256
|
+
refresh();
|
|
1257
|
+
}
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (event.key === "Escape" && visible) {
|
|
1261
|
+
event.preventDefault();
|
|
1262
|
+
setVisible(false);
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
const onHintClick = () => {
|
|
1266
|
+
setVisible(!visible);
|
|
1267
|
+
if (visible) {
|
|
1268
|
+
refresh();
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
const onCloseClick = () => {
|
|
1272
|
+
setVisible(false);
|
|
1273
|
+
};
|
|
1274
|
+
window.addEventListener("keydown", onKeyDown);
|
|
1275
|
+
hint.addEventListener("click", onHintClick);
|
|
1276
|
+
closeButton.addEventListener("click", onCloseClick);
|
|
1277
|
+
return () => {
|
|
1278
|
+
window.clearInterval(intervalId);
|
|
1279
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
1280
|
+
hint.removeEventListener("click", onHintClick);
|
|
1281
|
+
closeButton.removeEventListener("click", onCloseClick);
|
|
1282
|
+
host.remove();
|
|
1283
|
+
hint.remove();
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// src/extension/port-router.ts
|
|
1288
|
+
var PortRouter = class {
|
|
1289
|
+
panelClients = /* @__PURE__ */ new Map();
|
|
1290
|
+
contentClients = /* @__PURE__ */ new Map();
|
|
1291
|
+
handleConnection(port) {
|
|
1292
|
+
if (port.name === "kora-panel") {
|
|
1293
|
+
this.attachPanel(port);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
if (port.name === "kora-content") {
|
|
1297
|
+
this.attachContent(port);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
attachPanel(port) {
|
|
1301
|
+
port.onMessage.addListener((message) => {
|
|
1302
|
+
if (!isPanelInitMessage(message)) return;
|
|
1303
|
+
this.panelClients.set(message.tabId, { tabId: message.tabId, port });
|
|
1304
|
+
});
|
|
1305
|
+
port.onDisconnect.addListener(() => {
|
|
1306
|
+
for (const [tabId, client] of this.panelClients) {
|
|
1307
|
+
if (client.port === port) {
|
|
1308
|
+
this.panelClients.delete(tabId);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
attachContent(port) {
|
|
1314
|
+
const tabId = port.sender?.tab?.id;
|
|
1315
|
+
if (typeof tabId !== "number") {
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
this.contentClients.set(tabId, port);
|
|
1319
|
+
port.onMessage.addListener((message) => {
|
|
1320
|
+
if (!isContentEventMessage(message)) return;
|
|
1321
|
+
const panel = this.panelClients.get(tabId);
|
|
1322
|
+
if (!panel) return;
|
|
1323
|
+
panel.port.postMessage({ type: "kora-event", payload: message.payload });
|
|
1324
|
+
});
|
|
1325
|
+
port.onDisconnect.addListener(() => {
|
|
1326
|
+
this.contentClients.delete(tabId);
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
function isPanelInitMessage(value) {
|
|
1331
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1332
|
+
const record = value;
|
|
1333
|
+
return record.type === "panel-init" && typeof record.tabId === "number";
|
|
1334
|
+
}
|
|
1335
|
+
function isContentEventMessage(value) {
|
|
1336
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1337
|
+
const record = value;
|
|
1338
|
+
return record.type === "kora-event" && "payload" in record;
|
|
1339
|
+
}
|
|
1340
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1341
|
+
0 && (module.exports = {
|
|
1342
|
+
EventBuffer,
|
|
1343
|
+
Instrumenter,
|
|
1344
|
+
MessageBridge,
|
|
1345
|
+
PortRouter,
|
|
1346
|
+
buildPanelModel,
|
|
1347
|
+
computeStatistics,
|
|
1348
|
+
filterEvents,
|
|
1349
|
+
getEventCategory,
|
|
1350
|
+
mountKoraDevtoolsOverlay,
|
|
1351
|
+
renderDevtoolsPanel
|
|
1352
|
+
});
|
|
1353
|
+
//# sourceMappingURL=index.cjs.map
|