bitchat-node 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -23
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +143 -28
- package/dist/client.js.map +1 -1
- package/dist/debug.d.ts +39 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +89 -0
- package/dist/debug.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mesh/router.d.ts.map +1 -1
- package/dist/mesh/router.js +9 -1
- package/dist/mesh/router.js.map +1 -1
- package/dist/protocol/binary.d.ts +1 -1
- package/dist/protocol/binary.d.ts.map +1 -1
- package/dist/protocol/binary.js +2 -2
- package/dist/protocol/binary.js.map +1 -1
- package/dist/session/manager.d.ts.map +1 -1
- package/dist/session/manager.js +18 -1
- package/dist/session/manager.js.map +1 -1
- package/dist/transport/ble.d.ts +15 -0
- package/dist/transport/ble.d.ts.map +1 -1
- package/dist/transport/ble.js +63 -16
- package/dist/transport/ble.js.map +1 -1
- package/dist/ui/html.d.ts +5 -0
- package/dist/ui/html.d.ts.map +1 -0
- package/dist/ui/html.js +506 -0
- package/dist/ui/html.js.map +1 -0
- package/dist/ui/server.d.ts +5 -1
- package/dist/ui/server.d.ts.map +1 -1
- package/dist/ui/server.js +61 -255
- package/dist/ui/server.js.map +1 -1
- package/package.json +2 -1
- package/src/client.ts +159 -34
- package/src/debug.ts +119 -0
- package/src/index.ts +11 -0
- package/src/mesh/router.ts +11 -1
- package/src/protocol/binary.ts +2 -2
- package/src/session/manager.ts +19 -1
- package/src/transport/ble.ts +70 -16
- package/src/ui/html.ts +506 -0
- package/src/ui/server.ts +78 -258
package/src/transport/ble.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
SERVICE_UUID_TESTNET,
|
|
16
16
|
} from '../protocol/constants.js';
|
|
17
17
|
import type { PeerID } from '../protocol/types.js';
|
|
18
|
+
import { debugLog } from '../debug.js';
|
|
18
19
|
|
|
19
20
|
// Types for noble
|
|
20
21
|
type NobleModule = typeof import('@abandonware/noble');
|
|
@@ -91,22 +92,17 @@ export class BLELink implements Link {
|
|
|
91
92
|
throw new Error('Message too large for MTU, fragmentation not yet implemented');
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
console.log('[BLE] First 50 bytes:', Buffer.from(data.slice(0, 50)).toString('hex'));
|
|
96
|
-
console.log('[BLE] Last 20 bytes:', Buffer.from(data.slice(-20)).toString('hex'));
|
|
95
|
+
debugLog.bleTx(data, `→ ${this.id.slice(0, 8)} (${data.length}b)`);
|
|
97
96
|
|
|
98
97
|
const buf = Buffer.from(data);
|
|
99
|
-
console.log('[BLE] Buffer check - is Buffer:', Buffer.isBuffer(buf), 'length:', buf.length);
|
|
100
98
|
|
|
101
|
-
//
|
|
102
|
-
// This uses a different BLE write type that might work better
|
|
99
|
+
// Use regular write WITH response (withoutResponse = false) to match iOS behavior
|
|
103
100
|
return new Promise((resolve, reject) => {
|
|
104
|
-
this.characteristic.write(buf,
|
|
101
|
+
this.characteristic.write(buf, false, (error) => {
|
|
105
102
|
if (error) {
|
|
106
|
-
|
|
103
|
+
debugLog.error('BLE TX', error);
|
|
107
104
|
reject(error);
|
|
108
105
|
} else {
|
|
109
|
-
console.log('[BLE] Send success (without response)');
|
|
110
106
|
resolve();
|
|
111
107
|
}
|
|
112
108
|
});
|
|
@@ -118,6 +114,39 @@ export class BLELink implements Link {
|
|
|
118
114
|
}
|
|
119
115
|
}
|
|
120
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Peripheral Link - represents a central (e.g., iOS) connected to us
|
|
119
|
+
*/
|
|
120
|
+
export class PeripheralLink implements Link {
|
|
121
|
+
readonly id: string;
|
|
122
|
+
private _peerID?: PeerID;
|
|
123
|
+
private notifyCallback: (data: Buffer) => void;
|
|
124
|
+
|
|
125
|
+
constructor(centralId: string, notifyCallback: (data: Buffer) => void) {
|
|
126
|
+
this.id = centralId;
|
|
127
|
+
this.notifyCallback = notifyCallback;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
get peerID(): PeerID | undefined {
|
|
131
|
+
return this._peerID;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
setPeerID(peerID: PeerID): void {
|
|
135
|
+
this._peerID = peerID;
|
|
136
|
+
console.log(`[BLE] PeripheralLink ${this.id.slice(0,8)} now associated with peer ${peerID.toHex().slice(0,8)}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async send(data: Uint8Array): Promise<void> {
|
|
140
|
+
debugLog.bleTx(data, `→ notify (${data.length}b)`);
|
|
141
|
+
const buf = Buffer.from(data);
|
|
142
|
+
this.notifyCallback(buf);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
get isConnected(): boolean {
|
|
146
|
+
return true; // Peripheral link is always "connected" while active
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
121
150
|
/**
|
|
122
151
|
* BLE Transport - handles Bluetooth mesh networking
|
|
123
152
|
*/
|
|
@@ -134,6 +163,8 @@ export class BLETransport extends EventEmitter {
|
|
|
134
163
|
// Subscribed centrals (peripheral mode)
|
|
135
164
|
private subscribedCentrals = new Set<string>();
|
|
136
165
|
private notifyCallback?: (data: Buffer) => void;
|
|
166
|
+
private peripheralLink?: PeripheralLink;
|
|
167
|
+
private lastCentralAddress = 'unknown-central';
|
|
137
168
|
|
|
138
169
|
constructor(config: Partial<BLETransportConfig> = {}) {
|
|
139
170
|
super();
|
|
@@ -354,9 +385,7 @@ export class BLETransport extends EventEmitter {
|
|
|
354
385
|
await peripheral.connectAsync();
|
|
355
386
|
console.log('[BLE] Connected to', peripheral.uuid);
|
|
356
387
|
|
|
357
|
-
//
|
|
358
|
-
this.connectingPeripherals.delete(peripheral.uuid);
|
|
359
|
-
this.failedConnections.delete(peripheral.uuid);
|
|
388
|
+
// NOTE: Keep in connectingPeripherals until fully set up to prevent race conditions
|
|
360
389
|
|
|
361
390
|
// Discover services and characteristics
|
|
362
391
|
console.log('[BLE] Discovering services for', peripheral.uuid);
|
|
@@ -393,6 +422,7 @@ export class BLETransport extends EventEmitter {
|
|
|
393
422
|
// Subscribe to notifications
|
|
394
423
|
await characteristic.subscribeAsync();
|
|
395
424
|
characteristic.on('data', (data: Buffer) => {
|
|
425
|
+
debugLog.bleRx(new Uint8Array(data), `← ${peripheral.uuid.slice(0, 8)} (${data.length}b)`);
|
|
396
426
|
this.emit('data', new Uint8Array(data), link);
|
|
397
427
|
});
|
|
398
428
|
|
|
@@ -401,7 +431,12 @@ export class BLETransport extends EventEmitter {
|
|
|
401
431
|
this.onDisconnect(peripheral.uuid);
|
|
402
432
|
});
|
|
403
433
|
|
|
434
|
+
// Now safe to clear connecting state - link is fully established
|
|
435
|
+
this.connectingPeripherals.delete(peripheral.uuid);
|
|
436
|
+
this.failedConnections.delete(peripheral.uuid);
|
|
437
|
+
|
|
404
438
|
this.emit('link:connected', link);
|
|
439
|
+
console.log('[BLE] Link established to', peripheral.uuid);
|
|
405
440
|
} catch (error) {
|
|
406
441
|
// Clear connecting state
|
|
407
442
|
this.connectingPeripherals.delete(peripheral.uuid);
|
|
@@ -468,17 +503,28 @@ export class BLETransport extends EventEmitter {
|
|
|
468
503
|
callback: (result: number) => void
|
|
469
504
|
) => {
|
|
470
505
|
// Handle incoming data from central
|
|
471
|
-
|
|
506
|
+
debugLog.bleRx(new Uint8Array(data), `← central (${data.length}b)`);
|
|
507
|
+
// Pass the peripheral link if available
|
|
508
|
+
this.emit('data', new Uint8Array(data), this.peripheralLink ?? (null as any));
|
|
472
509
|
callback(CharacteristicClass.RESULT_SUCCESS);
|
|
473
510
|
},
|
|
474
511
|
onSubscribe: (_maxValueSize: number, updateValueCallback: (data: Buffer) => void) => {
|
|
475
|
-
// Central subscribed to notifications - store callback
|
|
512
|
+
// Central subscribed to notifications - store callback and create link
|
|
476
513
|
this.notifyCallback = updateValueCallback;
|
|
514
|
+
// Create a peripheral link for the connected central
|
|
515
|
+
this.peripheralLink = new PeripheralLink(this.lastCentralAddress, updateValueCallback);
|
|
477
516
|
console.log('Central subscribed to notifications');
|
|
517
|
+
console.log('[BLE] Created peripheral link for central:', this.lastCentralAddress);
|
|
518
|
+
// Emit link:connected so router can use this link
|
|
519
|
+
this.emit('link:connected', this.peripheralLink);
|
|
478
520
|
},
|
|
479
521
|
onUnsubscribe: () => {
|
|
480
522
|
// Central unsubscribed
|
|
523
|
+
if (this.peripheralLink) {
|
|
524
|
+
this.emit('link:disconnected', this.peripheralLink.id);
|
|
525
|
+
}
|
|
481
526
|
this.notifyCallback = undefined;
|
|
527
|
+
this.peripheralLink = undefined;
|
|
482
528
|
console.log('Central unsubscribed');
|
|
483
529
|
},
|
|
484
530
|
});
|
|
@@ -496,10 +542,18 @@ export class BLETransport extends EventEmitter {
|
|
|
496
542
|
// Listen for central connection events
|
|
497
543
|
const bleno = this.bleno as any;
|
|
498
544
|
bleno.on('accept', (clientAddress: string) => {
|
|
499
|
-
console.log('[BLE] Central
|
|
545
|
+
console.log('[BLE] ⬅ Central connected:', clientAddress);
|
|
546
|
+
this.lastCentralAddress = clientAddress;
|
|
547
|
+
this.subscribedCentrals.add(clientAddress);
|
|
500
548
|
});
|
|
501
549
|
bleno.on('disconnect', (clientAddress: string) => {
|
|
502
|
-
console.log('[BLE] Central disconnected:', clientAddress);
|
|
550
|
+
console.log('[BLE] ⬅ Central disconnected:', clientAddress);
|
|
551
|
+
this.subscribedCentrals.delete(clientAddress);
|
|
552
|
+
// Clean up peripheral link if this central disconnected
|
|
553
|
+
if (this.peripheralLink && this.peripheralLink.id === clientAddress) {
|
|
554
|
+
this.emit('link:disconnected', this.peripheralLink.id);
|
|
555
|
+
this.peripheralLink = undefined;
|
|
556
|
+
}
|
|
503
557
|
});
|
|
504
558
|
bleno.on('servicesSet', (error?: Error) => {
|
|
505
559
|
console.log('[BLE] Services set, error:', error);
|
package/src/ui/html.ts
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Bitchat Web UI with Debug Panel
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const HTML = `<!DOCTYPE html>
|
|
6
|
+
<html>
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="UTF-8">
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
10
|
+
<title>Bitchat Node</title>
|
|
11
|
+
<style>
|
|
12
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
13
|
+
body {
|
|
14
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
15
|
+
background: #0f0f1a; color: #eee; height: 100vh; display: flex; flex-direction: column;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Header */
|
|
19
|
+
header {
|
|
20
|
+
background: #1a1a2e; padding: 12px 20px; border-bottom: 1px solid #2a2a4a;
|
|
21
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
22
|
+
}
|
|
23
|
+
header h1 { font-size: 16px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
|
|
24
|
+
header h1 .logo { font-size: 20px; }
|
|
25
|
+
.header-info { display: flex; align-items: center; gap: 16px; }
|
|
26
|
+
.status {
|
|
27
|
+
font-size: 11px; padding: 4px 10px; border-radius: 12px;
|
|
28
|
+
background: #2a2a4a; color: #888;
|
|
29
|
+
}
|
|
30
|
+
.status.connected { background: #064e3b; color: #34d399; }
|
|
31
|
+
.peer-count { font-size: 11px; color: #60a5fa; }
|
|
32
|
+
.my-info { font-size: 11px; color: #888; font-family: monospace; }
|
|
33
|
+
|
|
34
|
+
/* Tab Navigation */
|
|
35
|
+
.tabs {
|
|
36
|
+
background: #1a1a2e; display: flex; border-bottom: 1px solid #2a2a4a;
|
|
37
|
+
}
|
|
38
|
+
.tab {
|
|
39
|
+
padding: 10px 20px; cursor: pointer; font-size: 13px; color: #888;
|
|
40
|
+
border-bottom: 2px solid transparent; transition: all 0.2s;
|
|
41
|
+
}
|
|
42
|
+
.tab:hover { color: #ccc; }
|
|
43
|
+
.tab.active { color: #60a5fa; border-bottom-color: #60a5fa; }
|
|
44
|
+
.tab .badge {
|
|
45
|
+
background: #dc2626; color: white; font-size: 10px; padding: 2px 6px;
|
|
46
|
+
border-radius: 10px; margin-left: 6px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Main Layout */
|
|
50
|
+
main { flex: 1; display: flex; overflow: hidden; }
|
|
51
|
+
.panel { display: none; flex: 1; flex-direction: column; overflow: hidden; }
|
|
52
|
+
.panel.active { display: flex; }
|
|
53
|
+
|
|
54
|
+
/* Chat Panel */
|
|
55
|
+
.chat-container { display: flex; flex: 1; overflow: hidden; }
|
|
56
|
+
.messages-area { flex: 1; display: flex; flex-direction: column; }
|
|
57
|
+
.messages {
|
|
58
|
+
flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 8px;
|
|
59
|
+
}
|
|
60
|
+
.message {
|
|
61
|
+
background: #1a1a2e; padding: 10px 14px; border-radius: 12px; max-width: 75%;
|
|
62
|
+
animation: fadeIn 0.15s ease;
|
|
63
|
+
}
|
|
64
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } }
|
|
65
|
+
.message.mine { background: #1e3a5f; align-self: flex-end; }
|
|
66
|
+
.message.private { border-left: 3px solid #ec4899; }
|
|
67
|
+
.message .meta { font-size: 10px; color: #666; margin-bottom: 4px; }
|
|
68
|
+
.message .meta .nickname { color: #60a5fa; font-weight: 500; }
|
|
69
|
+
.message .meta .private-tag { color: #ec4899; margin-left: 6px; }
|
|
70
|
+
.message .content { line-height: 1.4; word-wrap: break-word; font-size: 14px; }
|
|
71
|
+
.system { text-align: center; font-size: 11px; color: #555; padding: 6px; }
|
|
72
|
+
|
|
73
|
+
/* Input Area */
|
|
74
|
+
.input-area { background: #1a1a2e; padding: 12px 16px; border-top: 1px solid #2a2a4a; }
|
|
75
|
+
.dm-indicator {
|
|
76
|
+
font-size: 11px; color: #ec4899; margin-bottom: 8px;
|
|
77
|
+
display: flex; align-items: center; gap: 8px;
|
|
78
|
+
}
|
|
79
|
+
.dm-indicator .clear { cursor: pointer; color: #888; }
|
|
80
|
+
.dm-indicator .clear:hover { color: #fff; }
|
|
81
|
+
.input-row { display: flex; gap: 8px; }
|
|
82
|
+
input[type="text"] {
|
|
83
|
+
flex: 1; background: #0f0f1a; border: 1px solid #2a2a4a; border-radius: 8px;
|
|
84
|
+
padding: 10px 14px; color: #eee; font-size: 14px; outline: none;
|
|
85
|
+
}
|
|
86
|
+
input[type="text"]:focus { border-color: #3b82f6; }
|
|
87
|
+
input[type="text"]::placeholder { color: #444; }
|
|
88
|
+
button {
|
|
89
|
+
background: #3b82f6; color: white; border: none; border-radius: 8px;
|
|
90
|
+
padding: 10px 20px; font-size: 13px; font-weight: 500; cursor: pointer;
|
|
91
|
+
}
|
|
92
|
+
button:hover { background: #2563eb; }
|
|
93
|
+
button:disabled { background: #374151; cursor: not-allowed; }
|
|
94
|
+
button.secondary { background: #2a2a4a; }
|
|
95
|
+
button.secondary:hover { background: #3a3a5a; }
|
|
96
|
+
|
|
97
|
+
/* Peers Sidebar */
|
|
98
|
+
.peers-panel {
|
|
99
|
+
width: 180px; background: #1a1a2e; border-left: 1px solid #2a2a4a;
|
|
100
|
+
padding: 12px; overflow-y: auto;
|
|
101
|
+
}
|
|
102
|
+
.peers-panel h3 {
|
|
103
|
+
font-size: 10px; color: #666; margin-bottom: 10px;
|
|
104
|
+
text-transform: uppercase; letter-spacing: 1px;
|
|
105
|
+
}
|
|
106
|
+
.peer {
|
|
107
|
+
padding: 8px 10px; background: #0f0f1a; border-radius: 6px; margin-bottom: 6px;
|
|
108
|
+
font-size: 12px; cursor: pointer; transition: all 0.15s;
|
|
109
|
+
}
|
|
110
|
+
.peer:hover { background: #1e3a5f; }
|
|
111
|
+
.peer.selected { background: #3b82f6; }
|
|
112
|
+
.peer .peer-nick { font-weight: 500; }
|
|
113
|
+
.peer .peer-id { font-size: 9px; color: #555; margin-top: 2px; font-family: monospace; }
|
|
114
|
+
|
|
115
|
+
/* Debug Panel */
|
|
116
|
+
.debug-panel { padding: 0; display: flex; flex-direction: column; }
|
|
117
|
+
.debug-toolbar {
|
|
118
|
+
background: #1a1a2e; padding: 10px 16px; border-bottom: 1px solid #2a2a4a;
|
|
119
|
+
display: flex; gap: 8px; align-items: center;
|
|
120
|
+
}
|
|
121
|
+
.debug-toolbar label { font-size: 11px; color: #888; display: flex; align-items: center; gap: 4px; }
|
|
122
|
+
.debug-toolbar input[type="checkbox"] { accent-color: #3b82f6; }
|
|
123
|
+
.debug-toolbar select {
|
|
124
|
+
background: #0f0f1a; border: 1px solid #2a2a4a; border-radius: 4px;
|
|
125
|
+
padding: 4px 8px; color: #eee; font-size: 11px;
|
|
126
|
+
}
|
|
127
|
+
.debug-log {
|
|
128
|
+
flex: 1; overflow-y: auto; padding: 8px; font-family: 'SF Mono', Monaco, monospace;
|
|
129
|
+
font-size: 11px; line-height: 1.6; background: #0a0a12;
|
|
130
|
+
}
|
|
131
|
+
.debug-entry { padding: 4px 8px; border-radius: 4px; margin-bottom: 2px; }
|
|
132
|
+
.debug-entry.ble-rx { background: #1e293b; border-left: 3px solid #3b82f6; }
|
|
133
|
+
.debug-entry.ble-tx { background: #1e2d1e; border-left: 3px solid #22c55e; }
|
|
134
|
+
.debug-entry.session { background: #2d1e2d; border-left: 3px solid #a855f7; }
|
|
135
|
+
.debug-entry.error { background: #2d1e1e; border-left: 3px solid #ef4444; }
|
|
136
|
+
.debug-entry .time { color: #555; }
|
|
137
|
+
.debug-entry .tag {
|
|
138
|
+
font-weight: 600; padding: 1px 4px; border-radius: 3px; margin: 0 4px;
|
|
139
|
+
}
|
|
140
|
+
.debug-entry.ble-rx .tag { color: #60a5fa; }
|
|
141
|
+
.debug-entry.ble-tx .tag { color: #4ade80; }
|
|
142
|
+
.debug-entry.session .tag { color: #c084fc; }
|
|
143
|
+
.debug-entry.error .tag { color: #f87171; }
|
|
144
|
+
.debug-entry .hex { color: #fbbf24; word-break: break-all; }
|
|
145
|
+
.debug-entry .decoded { color: #94a3b8; margin-top: 2px; font-size: 10px; }
|
|
146
|
+
|
|
147
|
+
/* Sessions Panel */
|
|
148
|
+
.sessions-panel { padding: 16px; }
|
|
149
|
+
.session-card {
|
|
150
|
+
background: #1a1a2e; border-radius: 8px; padding: 12px 16px; margin-bottom: 12px;
|
|
151
|
+
}
|
|
152
|
+
.session-card h4 { font-size: 13px; color: #60a5fa; margin-bottom: 8px; font-family: monospace; }
|
|
153
|
+
.session-card .status-row {
|
|
154
|
+
display: flex; justify-content: space-between; font-size: 11px;
|
|
155
|
+
color: #888; margin-bottom: 4px;
|
|
156
|
+
}
|
|
157
|
+
.session-card .status-value { color: #eee; }
|
|
158
|
+
.session-card .status-value.success { color: #4ade80; }
|
|
159
|
+
.session-card .status-value.pending { color: #fbbf24; }
|
|
160
|
+
.session-card .status-value.error { color: #f87171; }
|
|
161
|
+
</style>
|
|
162
|
+
</head>
|
|
163
|
+
<body>
|
|
164
|
+
<header>
|
|
165
|
+
<div>
|
|
166
|
+
<h1><span class="logo">⚡</span> Bitchat Node</h1>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="header-info">
|
|
169
|
+
<span class="peer-count" id="peer-count">0 peers</span>
|
|
170
|
+
<span class="status" id="status">Connecting...</span>
|
|
171
|
+
<span class="my-info" id="my-info"></span>
|
|
172
|
+
</div>
|
|
173
|
+
</header>
|
|
174
|
+
|
|
175
|
+
<div class="tabs">
|
|
176
|
+
<div class="tab active" data-panel="chat">💬 Chat</div>
|
|
177
|
+
<div class="tab" data-panel="debug">🔍 Debug <span class="badge" id="debug-count" style="display:none">0</span></div>
|
|
178
|
+
<div class="tab" data-panel="sessions">🔐 Sessions</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<main>
|
|
182
|
+
<!-- Chat Panel -->
|
|
183
|
+
<div class="panel active" id="panel-chat">
|
|
184
|
+
<div class="chat-container">
|
|
185
|
+
<div class="messages-area">
|
|
186
|
+
<div class="messages" id="messages"></div>
|
|
187
|
+
<div class="input-area">
|
|
188
|
+
<div class="dm-indicator" id="dm-indicator" style="display:none">
|
|
189
|
+
Sending to: <strong id="dm-target"></strong>
|
|
190
|
+
<span class="clear" onclick="clearDM()">✕ clear</span>
|
|
191
|
+
</div>
|
|
192
|
+
<div class="input-row">
|
|
193
|
+
<input type="text" id="input" placeholder="Type a message..." autocomplete="off">
|
|
194
|
+
<button id="send">Send</button>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="peers-panel">
|
|
199
|
+
<h3>Peers</h3>
|
|
200
|
+
<div id="peers-list"></div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<!-- Debug Panel -->
|
|
206
|
+
<div class="panel" id="panel-debug">
|
|
207
|
+
<div class="debug-panel">
|
|
208
|
+
<div class="debug-toolbar">
|
|
209
|
+
<label><input type="checkbox" id="filter-ble" checked> BLE</label>
|
|
210
|
+
<label><input type="checkbox" id="filter-session" checked> Session</label>
|
|
211
|
+
<label><input type="checkbox" id="filter-error" checked> Errors</label>
|
|
212
|
+
<select id="filter-direction">
|
|
213
|
+
<option value="all">All directions</option>
|
|
214
|
+
<option value="rx">RX only</option>
|
|
215
|
+
<option value="tx">TX only</option>
|
|
216
|
+
</select>
|
|
217
|
+
<button class="secondary" onclick="clearDebug()">Clear</button>
|
|
218
|
+
<label style="margin-left:auto"><input type="checkbox" id="auto-scroll" checked> Auto-scroll</label>
|
|
219
|
+
</div>
|
|
220
|
+
<div class="debug-log" id="debug-log"></div>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<!-- Sessions Panel -->
|
|
225
|
+
<div class="panel" id="panel-sessions">
|
|
226
|
+
<div class="sessions-panel" id="sessions-list">
|
|
227
|
+
<p style="color:#666;font-size:12px">No active sessions</p>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</main>
|
|
231
|
+
|
|
232
|
+
<script>
|
|
233
|
+
const messagesEl = document.getElementById('messages');
|
|
234
|
+
const inputEl = document.getElementById('input');
|
|
235
|
+
const sendBtn = document.getElementById('send');
|
|
236
|
+
const statusEl = document.getElementById('status');
|
|
237
|
+
const peerCountEl = document.getElementById('peer-count');
|
|
238
|
+
const peersListEl = document.getElementById('peers-list');
|
|
239
|
+
const myInfoEl = document.getElementById('my-info');
|
|
240
|
+
const debugLogEl = document.getElementById('debug-log');
|
|
241
|
+
const debugCountEl = document.getElementById('debug-count');
|
|
242
|
+
const dmIndicatorEl = document.getElementById('dm-indicator');
|
|
243
|
+
const dmTargetEl = document.getElementById('dm-target');
|
|
244
|
+
const sessionsListEl = document.getElementById('sessions-list');
|
|
245
|
+
|
|
246
|
+
let ws;
|
|
247
|
+
let myPeerID = '';
|
|
248
|
+
let selectedPeer = null;
|
|
249
|
+
let debugCount = 0;
|
|
250
|
+
const peers = new Map();
|
|
251
|
+
const sessions = new Map();
|
|
252
|
+
|
|
253
|
+
// Tab switching
|
|
254
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
255
|
+
tab.addEventListener('click', () => {
|
|
256
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
257
|
+
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
|
258
|
+
tab.classList.add('active');
|
|
259
|
+
document.getElementById('panel-' + tab.dataset.panel).classList.add('active');
|
|
260
|
+
if (tab.dataset.panel === 'debug') {
|
|
261
|
+
debugCount = 0;
|
|
262
|
+
debugCountEl.style.display = 'none';
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
function connect() {
|
|
268
|
+
ws = new WebSocket('ws://' + location.host + '/ws');
|
|
269
|
+
|
|
270
|
+
ws.onopen = () => {
|
|
271
|
+
statusEl.textContent = 'Connected';
|
|
272
|
+
statusEl.className = 'status connected';
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
ws.onclose = () => {
|
|
276
|
+
statusEl.textContent = 'Disconnected';
|
|
277
|
+
statusEl.className = 'status';
|
|
278
|
+
setTimeout(connect, 2000);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
ws.onmessage = (e) => {
|
|
282
|
+
const msg = JSON.parse(e.data);
|
|
283
|
+
handleMessage(msg);
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function handleMessage(msg) {
|
|
288
|
+
switch (msg.type) {
|
|
289
|
+
case 'init':
|
|
290
|
+
myPeerID = msg.peerID;
|
|
291
|
+
myInfoEl.textContent = msg.nickname + ' · ' + msg.peerID.slice(0, 8) + '...';
|
|
292
|
+
break;
|
|
293
|
+
|
|
294
|
+
case 'message':
|
|
295
|
+
addMessage(msg.message);
|
|
296
|
+
break;
|
|
297
|
+
|
|
298
|
+
case 'peer:connected':
|
|
299
|
+
peers.set(msg.peer.peerID, msg.peer);
|
|
300
|
+
updatePeersList();
|
|
301
|
+
addSystem(msg.peer.nickname + ' joined');
|
|
302
|
+
break;
|
|
303
|
+
|
|
304
|
+
case 'peer:disconnected':
|
|
305
|
+
const peer = peers.get(msg.peerID);
|
|
306
|
+
peers.delete(msg.peerID);
|
|
307
|
+
updatePeersList();
|
|
308
|
+
if (peer) addSystem(peer.nickname + ' left');
|
|
309
|
+
break;
|
|
310
|
+
|
|
311
|
+
case 'peers':
|
|
312
|
+
peers.clear();
|
|
313
|
+
msg.peers.forEach(p => peers.set(p.peerID, p));
|
|
314
|
+
updatePeersList();
|
|
315
|
+
break;
|
|
316
|
+
|
|
317
|
+
case 'debug':
|
|
318
|
+
addDebugEntry(msg);
|
|
319
|
+
break;
|
|
320
|
+
|
|
321
|
+
case 'session':
|
|
322
|
+
updateSession(msg);
|
|
323
|
+
break;
|
|
324
|
+
|
|
325
|
+
case 'error':
|
|
326
|
+
addSystem('Error: ' + msg.error);
|
|
327
|
+
addDebugEntry({ category: 'error', text: msg.error, time: Date.now() });
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function addMessage(msg) {
|
|
333
|
+
const div = document.createElement('div');
|
|
334
|
+
div.className = 'message' + (msg.sender === myPeerID ? ' mine' : '') + (msg.isPrivate ? ' private' : '');
|
|
335
|
+
div.innerHTML =
|
|
336
|
+
'<div class="meta">' +
|
|
337
|
+
'<span class="nickname">' + escapeHtml(msg.senderNickname || 'unknown') + '</span> · ' +
|
|
338
|
+
new Date(msg.timestamp).toLocaleTimeString() +
|
|
339
|
+
(msg.isPrivate ? '<span class="private-tag">🔒 private</span>' : '') +
|
|
340
|
+
'</div>' +
|
|
341
|
+
'<div class="content">' + escapeHtml(msg.content || msg.text) + '</div>';
|
|
342
|
+
messagesEl.appendChild(div);
|
|
343
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function addSystem(text) {
|
|
347
|
+
const div = document.createElement('div');
|
|
348
|
+
div.className = 'system';
|
|
349
|
+
div.textContent = '— ' + text + ' —';
|
|
350
|
+
messagesEl.appendChild(div);
|
|
351
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function addDebugEntry(entry) {
|
|
355
|
+
const categoryClass = entry.category || 'ble-rx';
|
|
356
|
+
const filterCheckbox = document.getElementById('filter-' + (categoryClass.split('-')[0] || 'ble'));
|
|
357
|
+
const directionFilter = document.getElementById('filter-direction').value;
|
|
358
|
+
|
|
359
|
+
// Apply filters
|
|
360
|
+
if (filterCheckbox && !filterCheckbox.checked) return;
|
|
361
|
+
if (directionFilter === 'rx' && categoryClass === 'ble-tx') return;
|
|
362
|
+
if (directionFilter === 'tx' && categoryClass === 'ble-rx') return;
|
|
363
|
+
|
|
364
|
+
const div = document.createElement('div');
|
|
365
|
+
div.className = 'debug-entry ' + categoryClass;
|
|
366
|
+
|
|
367
|
+
const time = new Date(entry.time || Date.now()).toLocaleTimeString();
|
|
368
|
+
const tag = entry.tag || categoryClass.toUpperCase();
|
|
369
|
+
|
|
370
|
+
let content = '<span class="time">' + time + '</span>' +
|
|
371
|
+
'<span class="tag">[' + tag + ']</span> ';
|
|
372
|
+
|
|
373
|
+
if (entry.hex) {
|
|
374
|
+
content += '<span class="hex">' + entry.hex + '</span>';
|
|
375
|
+
}
|
|
376
|
+
if (entry.text) {
|
|
377
|
+
content += escapeHtml(entry.text);
|
|
378
|
+
}
|
|
379
|
+
if (entry.decoded) {
|
|
380
|
+
content += '<div class="decoded">↳ ' + escapeHtml(entry.decoded) + '</div>';
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
div.innerHTML = content;
|
|
384
|
+
debugLogEl.appendChild(div);
|
|
385
|
+
|
|
386
|
+
// Update badge if not viewing debug
|
|
387
|
+
if (!document.getElementById('panel-debug').classList.contains('active')) {
|
|
388
|
+
debugCount++;
|
|
389
|
+
debugCountEl.textContent = debugCount > 99 ? '99+' : debugCount;
|
|
390
|
+
debugCountEl.style.display = 'inline';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (document.getElementById('auto-scroll').checked) {
|
|
394
|
+
debugLogEl.scrollTop = debugLogEl.scrollHeight;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function clearDebug() {
|
|
399
|
+
debugLogEl.innerHTML = '';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function updateSession(msg) {
|
|
403
|
+
sessions.set(msg.peerID, msg);
|
|
404
|
+
renderSessions();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function renderSessions() {
|
|
408
|
+
if (sessions.size === 0) {
|
|
409
|
+
sessionsListEl.innerHTML = '<p style="color:#666;font-size:12px">No active sessions</p>';
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
sessionsListEl.innerHTML = '';
|
|
414
|
+
sessions.forEach((session, peerID) => {
|
|
415
|
+
const card = document.createElement('div');
|
|
416
|
+
card.className = 'session-card';
|
|
417
|
+
|
|
418
|
+
const statusClass = session.established ? 'success' : (session.error ? 'error' : 'pending');
|
|
419
|
+
const statusText = session.established ? 'Established' : (session.error || 'Handshaking...');
|
|
420
|
+
|
|
421
|
+
card.innerHTML =
|
|
422
|
+
'<h4>' + peerID.slice(0, 16) + '...</h4>' +
|
|
423
|
+
'<div class="status-row"><span>Status:</span><span class="status-value ' + statusClass + '">' + statusText + '</span></div>' +
|
|
424
|
+
'<div class="status-row"><span>Direction:</span><span class="status-value">' + (session.initiator ? 'Initiator' : 'Responder') + '</span></div>' +
|
|
425
|
+
'<div class="status-row"><span>TX Nonce:</span><span class="status-value">' + (session.txNonce ?? '—') + '</span></div>' +
|
|
426
|
+
'<div class="status-row"><span>RX Nonce:</span><span class="status-value">' + (session.rxNonce ?? '—') + '</span></div>';
|
|
427
|
+
|
|
428
|
+
sessionsListEl.appendChild(card);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function updatePeersList() {
|
|
433
|
+
peerCountEl.textContent = peers.size + ' peer' + (peers.size !== 1 ? 's' : '');
|
|
434
|
+
peersListEl.innerHTML = '';
|
|
435
|
+
|
|
436
|
+
if (peers.size === 0) {
|
|
437
|
+
peersListEl.innerHTML = '<p style="color:#555;font-size:11px;padding:8px">No peers nearby</p>';
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
peers.forEach((peer, id) => {
|
|
442
|
+
const div = document.createElement('div');
|
|
443
|
+
div.className = 'peer' + (selectedPeer === id ? ' selected' : '');
|
|
444
|
+
div.innerHTML =
|
|
445
|
+
'<div class="peer-nick">' + escapeHtml(peer.nickname) + '</div>' +
|
|
446
|
+
'<div class="peer-id">' + id.slice(0, 12) + '...</div>';
|
|
447
|
+
div.onclick = () => selectPeer(id, peer.nickname);
|
|
448
|
+
peersListEl.appendChild(div);
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function selectPeer(id, nickname) {
|
|
453
|
+
if (selectedPeer === id) {
|
|
454
|
+
clearDM();
|
|
455
|
+
} else {
|
|
456
|
+
selectedPeer = id;
|
|
457
|
+
dmTargetEl.textContent = nickname;
|
|
458
|
+
dmIndicatorEl.style.display = 'flex';
|
|
459
|
+
inputEl.placeholder = 'Private message to ' + nickname + '...';
|
|
460
|
+
updatePeersList();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function clearDM() {
|
|
465
|
+
selectedPeer = null;
|
|
466
|
+
dmIndicatorEl.style.display = 'none';
|
|
467
|
+
inputEl.placeholder = 'Type a message...';
|
|
468
|
+
updatePeersList();
|
|
469
|
+
}
|
|
470
|
+
window.clearDM = clearDM;
|
|
471
|
+
|
|
472
|
+
function send() {
|
|
473
|
+
const text = inputEl.value.trim();
|
|
474
|
+
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
|
|
475
|
+
|
|
476
|
+
addMessage({
|
|
477
|
+
id: Date.now().toString(),
|
|
478
|
+
sender: myPeerID,
|
|
479
|
+
senderNickname: 'me',
|
|
480
|
+
content: text,
|
|
481
|
+
timestamp: new Date().toISOString(),
|
|
482
|
+
isPrivate: !!selectedPeer
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
ws.send(JSON.stringify({
|
|
486
|
+
type: 'send',
|
|
487
|
+
text: text,
|
|
488
|
+
to: selectedPeer || null
|
|
489
|
+
}));
|
|
490
|
+
|
|
491
|
+
inputEl.value = '';
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function escapeHtml(text) {
|
|
495
|
+
const div = document.createElement('div');
|
|
496
|
+
div.textContent = text;
|
|
497
|
+
return div.innerHTML;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
sendBtn.onclick = send;
|
|
501
|
+
inputEl.onkeydown = (e) => { if (e.key === 'Enter') send(); };
|
|
502
|
+
|
|
503
|
+
connect();
|
|
504
|
+
</script>
|
|
505
|
+
</body>
|
|
506
|
+
</html>`;
|