OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet commits / 0ef63db

Add Device Pairing UI to Identity Manager
Adds a "Device Pairing" section to the Identity Manager panel with meta key display, copy-to-clipboard, and pair-with-remote-device flow. Includes T10/T11 tests for meta.invite and meta.join RPC routes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0ef63db9c87e2019a2ab42f83e038f75fc43d058
by plankatron <fleeky@gmail.com> · 2026-02-23T00:30:24.000Z

Changed files (3) +127 -1

scenes/identity_manager_panel.tscn +34 -1
……@@ -5,7 +5,7 @@
55[node name="IdentityManagerPanel" type="Window"]
66title = "Identity Manager"
77initial_position = 2
8size = Vector2i(400, 360)
8size = Vector2i(400, 480)
99visible = false
1010wrap_controls = true
1111script = ExtResource("1_script")
……@@ -96,3 +96,36 @@ unique_name_in_owner = true
9696layout_mode = 2
9797theme_override_colors/font_color = Color(1, 0.8, 0.2, 1)
9898text = ""
99
100[node name="PairSeparator" type="HSeparator" parent="Margin/VBox"]
101layout_mode = 2
102
103[node name="PairHeader" type="Label" parent="Margin/VBox"]
104layout_mode = 2
105theme_override_colors/font_color = Color(0.6, 0.9, 0.6, 1)
106text = "Device Pairing"
107
108[node name="MetaKeyRow" type="HBoxContainer" parent="Margin/VBox"]
109layout_mode = 2
110
111[node name="MetaKeyLabel" type="Label" parent="Margin/VBox/MetaKeyRow"]
112unique_name_in_owner = true
113layout_mode = 2
114size_flags_horizontal = 3
115text = "(loading...)"
116
117[node name="CopyKeyBtn" type="Button" parent="Margin/VBox/MetaKeyRow"]
118unique_name_in_owner = true
119layout_mode = 2
120text = "Copy"
121
122[node name="PairDeviceBtn" type="Button" parent="Margin/VBox"]
123unique_name_in_owner = true
124layout_mode = 2
125text = "Pair with Remote Device"
126
127[node name="PairStatusLabel" type="Label" parent="Margin/VBox"]
128unique_name_in_owner = true
129layout_mode = 2
130theme_override_colors/font_color = Color(1, 0.8, 0.2, 1)
131text = ""
scripts/identity_manager_panel.gd +54 -0
……@@ -18,6 +18,10 @@ var _selected_idx: int = -1
1818@onready var gc_btn: Button = %GcBtn
1919@onready var diagnose_btn: Button = %DiagnoseBtn
2020@onready var status_label: Label = %StatusLabel
21@onready var meta_key_label: Label = %MetaKeyLabel
22@onready var copy_key_btn: Button = %CopyKeyBtn
23@onready var pair_device_btn: Button = %PairDeviceBtn
24@onready var pair_status_label: Label = %PairStatusLabel
2125
2226
2327func _ready() -> void:
……@@ -31,6 +35,8 @@ func _ready() -> void:
3135 gc_btn.pressed.connect(_on_gc)
3236 diagnose_btn.pressed.connect(_on_diagnose)
3337 identity_list.item_selected.connect(_on_item_selected)
38 copy_key_btn.pressed.connect(_on_copy_key)
39 pair_device_btn.pressed.connect(_on_pair_device)
3440
3541 KeetService.identity_mgr.identities_loaded.connect(_on_identities_loaded)
3642 KeetService.identity_mgr.identity_created.connect(func(_e): refresh())
……@@ -44,6 +50,7 @@ func _ready() -> void:
4450func refresh() -> void:
4551 status_label.text = ""
4652 KeetService.identity_mgr.list_identities()
53 _load_meta_key()
4754
4855
4956func _on_identities_loaded(identities: Array, active_id: String) -> void:
……@@ -272,6 +279,53 @@ func _on_import() -> void:
272279 dialog.popup_centered()
273280
274281
282# --- Device Pairing ---
283
284func _load_meta_key() -> void:
285 pair_status_label.text = ""
286 KeetService.client.call_rpc("meta.invite", {}, func(result):
287 if result is Dictionary and result.has("key"):
288 var key: String = result["key"]
289 meta_key_label.text = key.substr(0, 16) + "..."
290 meta_key_label.tooltip_text = key
291 else:
292 meta_key_label.text = "(unavailable)"
293 meta_key_label.tooltip_text = ""
294 )
295
296
297func _on_copy_key() -> void:
298 var full_key: String = meta_key_label.tooltip_text
299 if full_key != "" and full_key.length() == 64:
300 DisplayServer.clipboard_set(full_key)
301 pair_status_label.text = "Key copied"
302
303
304func _on_pair_device() -> void:
305 var dialog := _make_input_dialog("Pair Device", "Paste remote device's metadata key:")
306 dialog.confirmed.connect(func():
307 var input: LineEdit = dialog.get_node("VBox/Input")
308 var remote_key: String = input.text.strip_edges()
309 if remote_key.length() != 64:
310 pair_status_label.text = "Invalid key (need 64 hex chars)"
311 dialog.queue_free()
312 return
313 pair_status_label.text = "Pairing..."
314 pair_device_btn.disabled = true
315 KeetService.client.call_rpc("meta.join", {"key": remote_key}, func(result):
316 pair_device_btn.disabled = false
317 if result is Dictionary and result.get("ok", false):
318 pair_status_label.text = "Paired! Writable: %s" % str(result.get("writable", false))
319 _load_meta_key()
320 else:
321 pair_status_label.text = "Pair failed"
322 )
323 dialog.queue_free()
324 )
325 add_child(dialog)
326 dialog.popup_centered()
327
328
275329# --- Helper dialogs ---
276330
277331func _make_input_dialog(title_text: String, prompt: String) -> ConfirmationDialog:
tests/test-admin-channel.mjs +39 -0
……@@ -11,6 +11,8 @@
1111 * 7. meta.set for multiple rooms stores separately
1212 * 8. meta.get for unknown roomId returns empty object
1313 * 9. meta.get is consistent after multiple operations
14 * 10. meta.invite returns 64-char hex pairing key
15 * 11. meta.join rejects invalid key, self-join is no-op
1416 *
1517 * Requires: keet-daemon running on /tmp/keet-daemon.sock
1618 * Usage: bare tests/test-admin-channel.mjs
……@@ -232,6 +234,43 @@ async function run () {
232234 failed++
233235 }
234236
237 // === T10: meta.invite returns 64-char hex key ===
238 console.log('\n--- T10: meta.invite returns pairing key ---')
239 let inviteKey = null
240 try {
241 const result = await client.call('meta.invite')
242 assert(typeof result === 'object', 'meta.invite returns object')
243 assert(typeof result.key === 'string', 'has key field')
244 assert(result.key.length === 64, 'key is 64-char hex')
245 inviteKey = result.key
246 } catch (err) {
247 console.log(` FAIL: ${err.message}`)
248 failed++
249 }
250
251 // === T11: meta.join validation and self-join ===
252 console.log('\n--- T11: meta.join validation + self-join ---')
253 try {
254 // Invalid key should fail
255 let joinFailed = false
256 try {
257 await client.call('meta.join', { key: 'too-short' })
258 } catch {
259 joinFailed = true
260 }
261 assert(joinFailed, 'meta.join rejects invalid key')
262
263 // Join own key should be a no-op (already a writer)
264 if (inviteKey) {
265 const result = await client.call('meta.join', { key: inviteKey })
266 assert(typeof result === 'object', 'self-join returns object')
267 assert(result.key === inviteKey || result.ok !== undefined, 'self-join does not error')
268 }
269 } catch (err) {
270 console.log(` FAIL: ${err.message}`)
271 failed++
272 }
273
235274 // === Cleanup ===
236275 console.log('\n--- Cleanup ---')
237276 try {