@korajs/devtools 0.4.0 → 0.6.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/dist/index.js CHANGED
@@ -1,3 +1,12 @@
1
+ import {
2
+ buildPanelModel,
3
+ computeStatistics,
4
+ filterEvents,
5
+ getEventCategory,
6
+ mountKoraDevtoolsOverlay,
7
+ renderDevtoolsPanel
8
+ } from "./chunk-N64DMGUU.js";
9
+
1
10
  // src/bridge/message-bridge.ts
2
11
  var DEFAULT_CHANNEL = "kora-devtools";
3
12
  var MessageBridge = class {
@@ -144,9 +153,18 @@ var ALL_EVENT_TYPES = [
144
153
  "constraint:violated",
145
154
  "sync:connected",
146
155
  "sync:disconnected",
156
+ "sync:schema-mismatch",
157
+ "sync:auth-failed",
158
+ "sync:apply-failed",
147
159
  "sync:sent",
148
160
  "sync:received",
149
161
  "sync:acknowledged",
162
+ "sync:diagnostics",
163
+ "sync:bandwidth",
164
+ "sync:initial-sync-progress",
165
+ "store:persistence-error",
166
+ "store:quota-exceeded",
167
+ "replay:completed",
150
168
  "query:subscribed",
151
169
  "query:invalidated",
152
170
  "query:executed",
@@ -222,779 +240,6 @@ var Instrumenter = class {
222
240
  }
223
241
  };
224
242
 
225
- // src/types.ts
226
- var EVENT_TYPE_CATEGORIES = {
227
- "operation:created": "operation",
228
- "operation:applied": "operation",
229
- "merge:started": "merge",
230
- "merge:completed": "merge",
231
- "merge:conflict": "merge",
232
- "constraint:violated": "merge",
233
- "sync:connected": "sync",
234
- "sync:disconnected": "sync",
235
- "sync:auth-failed": "sync",
236
- "sync:sent": "sync",
237
- "sync:received": "sync",
238
- "sync:acknowledged": "sync",
239
- "query:subscribed": "query",
240
- "query:invalidated": "query",
241
- "query:executed": "query",
242
- "connection:quality": "connection",
243
- "sync:diagnostics": "sync",
244
- "sync:bandwidth": "sync",
245
- "sync:initial-sync-progress": "sync",
246
- "awareness:updated": "sync",
247
- "state-machine:transition": "operation",
248
- "state-machine:rejected": "operation"
249
- };
250
- function eventTypeToCategory(type) {
251
- return EVENT_TYPE_CATEGORIES[type];
252
- }
253
-
254
- // src/filter/event-filter.ts
255
- function getEventCategory(type) {
256
- return eventTypeToCategory(type);
257
- }
258
- function extractCollection(event) {
259
- const e = event.event;
260
- switch (e.type) {
261
- case "operation:created":
262
- case "operation:applied":
263
- return e.operation.collection;
264
- case "merge:started":
265
- return e.operationA.collection;
266
- case "merge:completed":
267
- case "merge:conflict":
268
- return e.trace.operationA.collection;
269
- case "constraint:violated":
270
- return e.trace.operationA.collection;
271
- case "query:subscribed":
272
- return e.collection;
273
- case "query:invalidated":
274
- return e.trigger.collection;
275
- case "sync:sent":
276
- return e.operations[0]?.collection ?? null;
277
- case "sync:received":
278
- return e.operations[0]?.collection ?? null;
279
- default:
280
- return null;
281
- }
282
- }
283
- function filterEvents(events, criteria) {
284
- const { categories, types, timeRange, collection } = criteria;
285
- if (!categories && !types && !timeRange && !collection) {
286
- return events;
287
- }
288
- const categorySet = categories ? new Set(categories) : null;
289
- const typeSet = types ? new Set(types) : null;
290
- return events.filter((event) => {
291
- if (categorySet) {
292
- const cat = eventTypeToCategory(event.event.type);
293
- if (!categorySet.has(cat)) return false;
294
- }
295
- if (typeSet) {
296
- if (!typeSet.has(event.event.type)) return false;
297
- }
298
- if (timeRange) {
299
- if (event.receivedAt < timeRange.start || event.receivedAt > timeRange.end) {
300
- return false;
301
- }
302
- }
303
- if (collection) {
304
- const eventCollection = extractCollection(event);
305
- if (eventCollection !== collection) return false;
306
- }
307
- return true;
308
- });
309
- }
310
-
311
- // src/stats/event-stats.ts
312
- function computeStatistics(events) {
313
- const eventsByCategory = {
314
- operation: 0,
315
- merge: 0,
316
- sync: 0,
317
- query: 0,
318
- connection: 0
319
- };
320
- const eventsByType = {};
321
- let mergeConflicts = 0;
322
- let constraintViolations = 0;
323
- let mergeDurationSum = 0;
324
- let mergeDurationCount = 0;
325
- let queryDurationSum = 0;
326
- let queryDurationCount = 0;
327
- let syncOperationsSent = 0;
328
- let syncOperationsReceived = 0;
329
- for (const timestamped of events) {
330
- const e = timestamped.event;
331
- const category = eventTypeToCategory(e.type);
332
- eventsByCategory[category]++;
333
- eventsByType[e.type] = (eventsByType[e.type] ?? 0) + 1;
334
- switch (e.type) {
335
- case "merge:completed":
336
- mergeDurationSum += e.trace.duration;
337
- mergeDurationCount++;
338
- break;
339
- case "merge:conflict":
340
- mergeConflicts++;
341
- mergeDurationSum += e.trace.duration;
342
- mergeDurationCount++;
343
- break;
344
- case "constraint:violated":
345
- constraintViolations++;
346
- break;
347
- case "query:executed":
348
- queryDurationSum += e.duration;
349
- queryDurationCount++;
350
- break;
351
- case "sync:sent":
352
- syncOperationsSent += e.batchSize;
353
- break;
354
- case "sync:received":
355
- syncOperationsReceived += e.batchSize;
356
- break;
357
- }
358
- }
359
- return {
360
- totalEvents: events.length,
361
- eventsByCategory,
362
- eventsByType,
363
- mergeConflicts,
364
- constraintViolations,
365
- avgMergeDuration: mergeDurationCount > 0 ? mergeDurationSum / mergeDurationCount : null,
366
- avgQueryDuration: queryDurationCount > 0 ? queryDurationSum / queryDurationCount : null,
367
- syncOperationsSent,
368
- syncOperationsReceived
369
- };
370
- }
371
-
372
- // src/ui/panel-state.ts
373
- function buildPanelModel(events) {
374
- const timeline = events.map((entry) => ({
375
- id: entry.id,
376
- type: entry.event.type,
377
- label: timelineLabel(entry.event),
378
- color: timelineColor(entry.event.type),
379
- receivedAt: entry.receivedAt,
380
- dependsOn: extractCausalDependencies(entry.event)
381
- }));
382
- const conflicts = events.flatMap((entry) => {
383
- if (entry.event.type !== "merge:completed" && entry.event.type !== "merge:conflict" && entry.event.type !== "constraint:violated") {
384
- return [];
385
- }
386
- const trace = entry.event.trace;
387
- return [
388
- {
389
- id: entry.id,
390
- timestamp: entry.receivedAt,
391
- collection: trace.operationA.collection,
392
- field: trace.field,
393
- strategy: trace.strategy,
394
- tier: trace.tier,
395
- inputA: trace.inputA,
396
- inputB: trace.inputB,
397
- output: trace.output,
398
- constraintViolated: trace.constraintViolated
399
- }
400
- ];
401
- });
402
- const operations = events.map((entry) => {
403
- const operation = extractOperation(entry.event);
404
- if (!operation) return null;
405
- return {
406
- id: entry.id,
407
- timestamp: entry.receivedAt,
408
- operationId: operation.id,
409
- collection: operation.collection,
410
- recordId: operation.recordId,
411
- opType: operation.type,
412
- data: operation.data,
413
- causalDeps: operation.causalDeps,
414
- nodeId: operation.nodeId,
415
- sequenceNumber: operation.sequenceNumber
416
- };
417
- }).filter((item) => item !== null);
418
- const network = buildNetworkStatus(events, operations);
419
- return {
420
- timeline,
421
- conflicts,
422
- operations,
423
- network
424
- };
425
- }
426
- function buildNetworkStatus(events, operations) {
427
- let connected = false;
428
- let quality = null;
429
- let pendingAcks = 0;
430
- let lastSyncAt = null;
431
- let sentOps = 0;
432
- let receivedOps = 0;
433
- for (const entry of events) {
434
- switch (entry.event.type) {
435
- case "sync:connected":
436
- connected = true;
437
- lastSyncAt = entry.receivedAt;
438
- break;
439
- case "sync:disconnected":
440
- connected = false;
441
- break;
442
- case "connection:quality":
443
- quality = entry.event.quality;
444
- break;
445
- case "sync:sent":
446
- sentOps += entry.event.operations.length;
447
- pendingAcks += entry.event.operations.length;
448
- lastSyncAt = entry.receivedAt;
449
- break;
450
- case "sync:received":
451
- receivedOps += entry.event.operations.length;
452
- lastSyncAt = entry.receivedAt;
453
- break;
454
- case "sync:acknowledged":
455
- pendingAcks = Math.max(0, pendingAcks - 1);
456
- lastSyncAt = entry.receivedAt;
457
- break;
458
- }
459
- }
460
- const vector = /* @__PURE__ */ new Map();
461
- for (const operation of operations) {
462
- const current = vector.get(operation.nodeId) ?? 0;
463
- if (operation.sequenceNumber > current) {
464
- vector.set(operation.nodeId, operation.sequenceNumber);
465
- }
466
- }
467
- return {
468
- connected,
469
- quality,
470
- pendingAcks,
471
- lastSyncAt,
472
- sentOps,
473
- receivedOps,
474
- versionVector: [...vector.entries()].map(([nodeId, sequenceNumber]) => ({ nodeId, sequenceNumber })).sort((left, right) => left.nodeId.localeCompare(right.nodeId))
475
- };
476
- }
477
- function timelineLabel(event) {
478
- switch (event.type) {
479
- case "operation:created":
480
- case "operation:applied":
481
- return `${event.operation.type} ${event.operation.collection}/${event.operation.recordId}`;
482
- case "merge:started":
483
- return `merge start ${event.operationA.collection}`;
484
- case "merge:completed":
485
- return `merge complete ${event.trace.field}`;
486
- case "merge:conflict":
487
- return `merge conflict ${event.trace.field}`;
488
- case "constraint:violated":
489
- return `constraint ${event.constraint}`;
490
- case "sync:connected":
491
- return `sync connected ${event.nodeId}`;
492
- case "sync:disconnected":
493
- return "sync disconnected";
494
- case "sync:auth-failed":
495
- return "sync auth failed";
496
- case "sync:sent":
497
- return `sync sent ${event.batchSize}`;
498
- case "sync:received":
499
- return `sync received ${event.batchSize}`;
500
- case "sync:acknowledged":
501
- return `sync ack ${event.sequenceNumber}`;
502
- case "query:subscribed":
503
- return `query subscribed ${event.collection}`;
504
- case "query:invalidated":
505
- return `query invalidated ${event.queryId}`;
506
- case "query:executed":
507
- return `query executed ${event.queryId}`;
508
- case "connection:quality":
509
- return `connection ${event.quality}`;
510
- case "sync:diagnostics":
511
- return `sync diagnostics ${event.diagnostics.quality}`;
512
- case "sync:bandwidth":
513
- return `bandwidth ${event.direction} ${event.bytesPerSecond}B/s`;
514
- case "sync:initial-sync-progress":
515
- return `initial sync ${Math.round(event.progress * 100)}%`;
516
- case "awareness:updated":
517
- return `awareness ${event.states.size} peers`;
518
- case "state-machine:transition":
519
- return `transition ${event.collection} ${event.from} \u2192 ${event.to}`;
520
- case "state-machine:rejected":
521
- return `transition rejected ${event.collection} ${event.from} \u2192 ${event.to}`;
522
- }
523
- }
524
- function timelineColor(type) {
525
- if (type.startsWith("operation:")) return "#22c55e";
526
- if (type.startsWith("sync:")) return "#a855f7";
527
- if (type.startsWith("merge:") || type.startsWith("constraint:")) return "#f59e0b";
528
- if (type.startsWith("query:")) return "#0ea5e9";
529
- return "#64748b";
530
- }
531
- function extractCausalDependencies(event) {
532
- const operation = extractOperation(event);
533
- return operation?.causalDeps ?? [];
534
- }
535
- function extractOperation(event) {
536
- switch (event.type) {
537
- case "operation:created":
538
- case "operation:applied":
539
- return event.operation;
540
- case "query:invalidated":
541
- return event.trigger;
542
- default:
543
- return null;
544
- }
545
- }
546
-
547
- // src/ui/panel.ts
548
- import htm from "htm";
549
- import { h, render } from "preact";
550
- import { useCallback, useMemo, useState } from "preact/hooks";
551
-
552
- // src/ui/components.ts
553
- function formatTime(timestamp) {
554
- const d = new Date(timestamp);
555
- const h2 = String(d.getHours()).padStart(2, "0");
556
- const m = String(d.getMinutes()).padStart(2, "0");
557
- const s = String(d.getSeconds()).padStart(2, "0");
558
- const ms = String(d.getMilliseconds()).padStart(3, "0");
559
- return `${h2}:${m}:${s}.${ms}`;
560
- }
561
- function truncate(str, max) {
562
- if (str.length <= max) return str;
563
- return `${str.slice(0, max - 1)}\u2026`;
564
- }
565
- function formatValue(value) {
566
- if (value === null || value === void 0) return "null";
567
- if (typeof value === "string") return `"${value}"`;
568
- if (typeof value === "object") return JSON.stringify(value, null, 2);
569
- return String(value);
570
- }
571
-
572
- // src/ui/panel.ts
573
- var html = htm.bind(h);
574
- var ALL_CATEGORIES = ["operation", "merge", "sync", "query", "connection"];
575
- function DevToolsPanel({ events }) {
576
- const [activeTab, setActiveTab] = useState("timeline");
577
- const [search, setSearch] = useState("");
578
- const [categories, setCategories] = useState(() => new Set(ALL_CATEGORIES));
579
- const [expanded, setExpanded] = useState(() => /* @__PURE__ */ new Set());
580
- const [paused, setPaused] = useState(false);
581
- const toggleCategory = useCallback((cat) => {
582
- setCategories((prev) => {
583
- const next = new Set(prev);
584
- if (next.has(cat)) next.delete(cat);
585
- else next.add(cat);
586
- return next;
587
- });
588
- }, []);
589
- const toggleExpand = useCallback((id) => {
590
- setExpanded((prev) => {
591
- const next = new Set(prev);
592
- if (next.has(id)) next.delete(id);
593
- else next.add(id);
594
- return next;
595
- });
596
- }, []);
597
- const filtered = useMemo(
598
- () => filterEvents(events, { categories: [...categories] }),
599
- [events, categories]
600
- );
601
- const model = useMemo(() => buildPanelModel(filtered), [filtered]);
602
- const stats = useMemo(() => computeStatistics(events), [events]);
603
- return html`
604
- <div class="kora-devtools">
605
- <${Toolbar}
606
- activeTab=${activeTab}
607
- onTabChange=${setActiveTab}
608
- totalEvents=${events.length}
609
- conflicts=${stats.mergeConflicts}
610
- search=${search}
611
- onSearchChange=${setSearch}
612
- paused=${paused}
613
- onPauseToggle=${() => setPaused((p) => !p)}
614
- onClear=${() => setExpanded(/* @__PURE__ */ new Set())}
615
- categories=${categories}
616
- onToggleCategory=${toggleCategory}
617
- />
618
- <div class="kora-content">
619
- ${activeTab === "timeline" && html`<${TimelinePanel}
620
- items=${model.timeline}
621
- search=${search}
622
- expanded=${expanded}
623
- onToggle=${toggleExpand}
624
- />`}
625
- ${activeTab === "conflicts" && html`<${ConflictsPanel}
626
- items=${model.conflicts}
627
- search=${search}
628
- expanded=${expanded}
629
- onToggle=${toggleExpand}
630
- />`}
631
- ${activeTab === "operations" && html`<${OperationsPanel}
632
- items=${model.operations}
633
- search=${search}
634
- expanded=${expanded}
635
- onToggle=${toggleExpand}
636
- />`}
637
- ${activeTab === "network" && html`<${NetworkPanel} network=${model.network} events=${events} />`}
638
- </div>
639
- </div>
640
- `;
641
- }
642
- function Toolbar({
643
- activeTab,
644
- onTabChange,
645
- totalEvents,
646
- conflicts,
647
- search,
648
- onSearchChange,
649
- paused,
650
- onPauseToggle,
651
- onClear,
652
- categories,
653
- onToggleCategory
654
- }) {
655
- const tabs = [
656
- { id: "timeline", label: "Timeline", badge: totalEvents },
657
- { id: "conflicts", label: "Conflicts", badge: conflicts },
658
- { id: "operations", label: "Operations" },
659
- { id: "network", label: "Network" }
660
- ];
661
- return html`
662
- <div class="kora-toolbar">
663
- <div class="kora-tabs">
664
- ${tabs.map(
665
- (tab) => html`
666
- <button
667
- class="kora-tab${activeTab === tab.id ? " active" : ""}"
668
- onClick=${() => onTabChange(tab.id)}
669
- >
670
- ${tab.label}
671
- ${tab.badge != null && tab.badge > 0 ? html`<span class="kora-badge">${tab.badge}</span>` : null}
672
- </button>
673
- `
674
- )}
675
- </div>
676
- <div class="kora-controls">
677
- <input
678
- type="text"
679
- class="kora-search"
680
- placeholder="Filter..."
681
- value=${search}
682
- onInput=${(e) => onSearchChange(e.target.value)}
683
- />
684
- <button class="kora-btn" onClick=${onPauseToggle}>
685
- ${paused ? "Resume" : "Pause"}
686
- </button>
687
- <button class="kora-btn" onClick=${onClear}>Clear</button>
688
- ${ALL_CATEGORIES.map(
689
- (cat) => html`
690
- <button
691
- class="kora-cat-toggle${categories.has(cat) ? " active" : ""}"
692
- onClick=${() => onToggleCategory(cat)}
693
- >
694
- ${cat}
695
- </button>
696
- `
697
- )}
698
- </div>
699
- </div>
700
- `;
701
- }
702
- function TimelinePanel({
703
- items,
704
- search,
705
- expanded,
706
- onToggle
707
- }) {
708
- const lowerSearch = search.toLowerCase();
709
- const filtered = lowerSearch ? items.filter(
710
- (i) => i.label.toLowerCase().includes(lowerSearch) || i.type.toLowerCase().includes(lowerSearch)
711
- ) : items;
712
- const visible = filtered.slice(-200).reverse();
713
- if (visible.length === 0) {
714
- return html`<div class="kora-panel"><div class="kora-empty">No events recorded yet.</div></div>`;
715
- }
716
- return html`
717
- <div class="kora-panel kora-timeline">
718
- <div class="kora-summary">Showing ${visible.length} of ${items.length} events</div>
719
- <div class="kora-event-list">
720
- ${visible.map(
721
- (item) => html`
722
- <div
723
- key=${item.id}
724
- class="kora-row${expanded.has(item.id) ? " expanded" : ""}"
725
- onClick=${() => onToggle(item.id)}
726
- >
727
- <span class="kora-time">${formatTime(item.receivedAt)}</span>
728
- <span class="kora-dot" style="background:${item.color}"></span>
729
- <span class="kora-type">${item.type}</span>
730
- <span class="kora-label">${item.label}</span>
731
- ${item.dependsOn.length > 0 ? html`<span class="kora-deps">deps: ${item.dependsOn.length}</span>` : null}
732
- </div>
733
- ${expanded.has(item.id) ? html`
734
- <div class="kora-detail">
735
- <div>
736
- <span class="kora-detail-label">Event ID:</span> ${item.id}
737
- </div>
738
- <div>
739
- <span class="kora-detail-label">Received:</span>
740
- ${new Date(item.receivedAt).toISOString()}
741
- </div>
742
- ${item.dependsOn.length > 0 ? html`<div>
743
- <span class="kora-detail-label">Causal deps:</span>
744
- ${item.dependsOn.join(", ")}
745
- </div>` : null}
746
- </div>
747
- ` : null}
748
- `
749
- )}
750
- </div>
751
- </div>
752
- `;
753
- }
754
- function ConflictsPanel({
755
- items,
756
- search,
757
- expanded,
758
- onToggle
759
- }) {
760
- const lowerSearch = search.toLowerCase();
761
- const filtered = lowerSearch ? items.filter(
762
- (i) => i.collection.toLowerCase().includes(lowerSearch) || i.field.toLowerCase().includes(lowerSearch) || i.strategy.toLowerCase().includes(lowerSearch)
763
- ) : items;
764
- const visible = filtered.slice(-100).reverse();
765
- if (visible.length === 0) {
766
- return html`<div class="kora-panel"><div class="kora-empty">No merge conflicts detected.</div></div>`;
767
- }
768
- return html`
769
- <div class="kora-panel kora-conflicts">
770
- <div class="kora-summary">
771
- ${items.length} conflict${items.length !== 1 ? "s" : ""}
772
- </div>
773
- <table class="kora-table">
774
- <thead>
775
- <tr>
776
- <th>Time</th>
777
- <th>Collection</th>
778
- <th>Field</th>
779
- <th>Strategy</th>
780
- <th>Tier</th>
781
- <th>Result</th>
782
- </tr>
783
- </thead>
784
- <tbody>
785
- ${visible.map(
786
- (item) => html`
787
- <tr
788
- key=${item.id}
789
- class="kora-conflict-row${item.constraintViolated ? " violated" : ""}${expanded.has(item.id) ? " expanded" : ""}"
790
- onClick=${() => onToggle(item.id)}
791
- >
792
- <td>${formatTime(item.timestamp)}</td>
793
- <td>${item.collection}</td>
794
- <td class="kora-mono">${item.field}</td>
795
- <td>
796
- <span class="kora-strategy tier-${item.tier}">${item.strategy}</span>
797
- </td>
798
- <td>${item.tier}</td>
799
- <td class="kora-mono">${truncate(String(item.output), 30)}</td>
800
- </tr>
801
- ${expanded.has(item.id) ? html`
802
- <tr class="kora-detail-row">
803
- <td colspan="6">
804
- <div class="kora-conflict-detail">
805
- <div class="kora-comparison">
806
- <div class="kora-value-box">
807
- <div class="kora-value-label">Input A (local)</div>
808
- <pre class="kora-value-code">
809
- ${formatValue(item.inputA)}</pre
810
- >
811
- </div>
812
- <div class="kora-value-box">
813
- <div class="kora-value-label">Input B (remote)</div>
814
- <pre class="kora-value-code">
815
- ${formatValue(item.inputB)}</pre
816
- >
817
- </div>
818
- <div class="kora-value-box result">
819
- <div class="kora-value-label">Result</div>
820
- <pre class="kora-value-code">
821
- ${formatValue(item.output)}</pre
822
- >
823
- </div>
824
- </div>
825
- ${item.constraintViolated ? html`<div class="kora-constraint-warning">
826
- Constraint violated: ${item.constraintViolated}
827
- </div>` : null}
828
- </div>
829
- </td>
830
- </tr>
831
- ` : null}
832
- `
833
- )}
834
- </tbody>
835
- </table>
836
- </div>
837
- `;
838
- }
839
- function OperationsPanel({
840
- items,
841
- search,
842
- expanded,
843
- onToggle
844
- }) {
845
- const lowerSearch = search.toLowerCase();
846
- const filtered = lowerSearch ? items.filter(
847
- (i) => i.collection.toLowerCase().includes(lowerSearch) || i.recordId.toLowerCase().includes(lowerSearch) || i.operationId.toLowerCase().includes(lowerSearch) || i.opType.toLowerCase().includes(lowerSearch)
848
- ) : items;
849
- const visible = filtered.slice(-200).reverse();
850
- if (visible.length === 0) {
851
- return html`<div class="kora-panel"><div class="kora-empty">No operations recorded yet.</div></div>`;
852
- }
853
- return html`
854
- <div class="kora-panel kora-operations">
855
- <div class="kora-summary">
856
- Showing ${visible.length} of ${items.length} operations
857
- </div>
858
- <div class="kora-op-list">
859
- ${visible.map(
860
- (item) => html`
861
- <div
862
- key=${item.id}
863
- class="kora-op-row${expanded.has(item.id) ? " expanded" : ""}"
864
- onClick=${() => onToggle(item.id)}
865
- >
866
- <span class="kora-time">${formatTime(item.timestamp)}</span>
867
- <span class="kora-op-type op-${item.opType}">${item.opType}</span>
868
- <span class="kora-op-collection">${item.collection}</span>
869
- <span class="kora-op-record kora-mono"
870
- >${truncate(item.recordId, 12)}</span
871
- >
872
- <span class="kora-op-node">node:${truncate(item.nodeId, 8)}</span>
873
- <span class="kora-op-seq">#${item.sequenceNumber}</span>
874
- </div>
875
- ${expanded.has(item.id) ? html`
876
- <div class="kora-op-detail">
877
- <div>
878
- <span class="kora-detail-label">Operation ID:</span>
879
- <span class="kora-mono">${item.operationId}</span>
880
- </div>
881
- <div>
882
- <span class="kora-detail-label">Node:</span>
883
- <span class="kora-mono">${item.nodeId}</span>
884
- </div>
885
- <div>
886
- <span class="kora-detail-label">Sequence:</span>
887
- ${item.sequenceNumber}
888
- </div>
889
- ${item.causalDeps.length > 0 ? html`<div>
890
- <span class="kora-detail-label">Causal deps:</span>
891
- <span class="kora-mono"
892
- >${item.causalDeps.join(", ")}</span
893
- >
894
- </div>` : null}
895
- ${item.data ? html`<div>
896
- <div class="kora-detail-label">Data:</div>
897
- <pre class="kora-value-code">
898
- ${JSON.stringify(item.data, null, 2)}</pre
899
- >
900
- </div>` : null}
901
- </div>
902
- ` : null}
903
- `
904
- )}
905
- </div>
906
- </div>
907
- `;
908
- }
909
- function NetworkPanel({
910
- network,
911
- events
912
- }) {
913
- const statusClass = network.connected ? "connected" : "disconnected";
914
- const statusText = network.connected ? network.quality ? `Connected (${network.quality})` : "Connected" : "Disconnected";
915
- const syncEvents = events.filter((e) => {
916
- const cat = eventTypeToCategory(e.event.type);
917
- return cat === "sync" || cat === "connection";
918
- }).slice(-50).reverse();
919
- return html`
920
- <div class="kora-panel kora-network">
921
- <div class="kora-network-status">
922
- <div class="kora-status-indicator ${statusClass}">
923
- <span class="kora-status-dot"></span>
924
- <span class="kora-status-text">${statusText}</span>
925
- </div>
926
- </div>
927
-
928
- <div class="kora-stats-grid">
929
- <${StatCard} label="Sent" value=${String(network.sentOps)} unit="ops" />
930
- <${StatCard} label="Received" value=${String(network.receivedOps)} unit="ops" />
931
- <${StatCard} label="Pending" value=${String(network.pendingAcks)} unit="acks" />
932
- <${StatCard}
933
- label="Last Sync"
934
- value=${network.lastSyncAt ? formatTime(network.lastSyncAt) : "never"}
935
- />
936
- </div>
937
-
938
- ${network.versionVector.length > 0 ? html`
939
- <div class="kora-vv-section">
940
- <h3>Version Vector</h3>
941
- <table class="kora-table kora-vv-table">
942
- <thead>
943
- <tr>
944
- <th>Node</th>
945
- <th>Sequence</th>
946
- </tr>
947
- </thead>
948
- <tbody>
949
- ${network.versionVector.map(
950
- (entry) => html`
951
- <tr key=${entry.nodeId}>
952
- <td class="kora-mono">${truncate(entry.nodeId, 16)}</td>
953
- <td>${entry.sequenceNumber}</td>
954
- </tr>
955
- `
956
- )}
957
- </tbody>
958
- </table>
959
- </div>
960
- ` : null}
961
-
962
- ${syncEvents.length > 0 ? html`
963
- <div class="kora-sync-log">
964
- <h3>Recent Sync Activity</h3>
965
- <div class="kora-event-list">
966
- ${syncEvents.map(
967
- (entry) => html`
968
- <div key=${entry.id} class="kora-row compact">
969
- <span class="kora-time"
970
- >${formatTime(entry.receivedAt)}</span
971
- >
972
- <span class="kora-type">${entry.event.type}</span>
973
- </div>
974
- `
975
- )}
976
- </div>
977
- </div>
978
- ` : null}
979
- </div>
980
- `;
981
- }
982
- function StatCard({
983
- label,
984
- value,
985
- unit
986
- }) {
987
- return html`
988
- <div class="kora-stat-card">
989
- <div class="kora-stat-value">${value}</div>
990
- <div class="kora-stat-label">${label}${unit ? ` (${unit})` : ""}</div>
991
- </div>
992
- `;
993
- }
994
- function renderDevtoolsPanel(target, events) {
995
- render(html`<${DevToolsPanel} events=${events} />`, target);
996
- }
997
-
998
243
  // src/extension/port-router.ts
999
244
  var PortRouter = class {
1000
245
  panelClients = /* @__PURE__ */ new Map();
@@ -1057,6 +302,7 @@ export {
1057
302
  computeStatistics,
1058
303
  filterEvents,
1059
304
  getEventCategory,
305
+ mountKoraDevtoolsOverlay,
1060
306
  renderDevtoolsPanel
1061
307
  };
1062
308
  //# sourceMappingURL=index.js.map