| … | … | @@ -5,7 +5,7 @@ |
| 5 | 5 | [node name="IdentityManagerPanel" type="Window"] |
| 6 | 6 | title = "Identity Manager" |
| 7 | 7 | initial_position = 2 |
| 8 | size = Vector2i(400, 360) | |
| 8 | size = Vector2i(400, 480) | |
| 9 | 9 | visible = false |
| 10 | 10 | wrap_controls = true |
| 11 | 11 | script = ExtResource("1_script") |
| … | … | @@ -96,3 +96,36 @@ unique_name_in_owner = true |
| 96 | 96 | layout_mode = 2 |
| 97 | 97 | theme_override_colors/font_color = Color(1, 0.8, 0.2, 1) |
| 98 | 98 | text = "" |
| 99 | ||
| 100 | [node name="PairSeparator" type="HSeparator" parent="Margin/VBox"] | |
| 101 | layout_mode = 2 | |
| 102 | ||
| 103 | [node name="PairHeader" type="Label" parent="Margin/VBox"] | |
| 104 | layout_mode = 2 | |
| 105 | theme_override_colors/font_color = Color(0.6, 0.9, 0.6, 1) | |
| 106 | text = "Device Pairing" | |
| 107 | ||
| 108 | [node name="MetaKeyRow" type="HBoxContainer" parent="Margin/VBox"] | |
| 109 | layout_mode = 2 | |
| 110 | ||
| 111 | [node name="MetaKeyLabel" type="Label" parent="Margin/VBox/MetaKeyRow"] | |
| 112 | unique_name_in_owner = true | |
| 113 | layout_mode = 2 | |
| 114 | size_flags_horizontal = 3 | |
| 115 | text = "(loading...)" | |
| 116 | ||
| 117 | [node name="CopyKeyBtn" type="Button" parent="Margin/VBox/MetaKeyRow"] | |
| 118 | unique_name_in_owner = true | |
| 119 | layout_mode = 2 | |
| 120 | text = "Copy" | |
| 121 | ||
| 122 | [node name="PairDeviceBtn" type="Button" parent="Margin/VBox"] | |
| 123 | unique_name_in_owner = true | |
| 124 | layout_mode = 2 | |
| 125 | text = "Pair with Remote Device" | |
| 126 | ||
| 127 | [node name="PairStatusLabel" type="Label" parent="Margin/VBox"] | |
| 128 | unique_name_in_owner = true | |
| 129 | layout_mode = 2 | |
| 130 | theme_override_colors/font_color = Color(1, 0.8, 0.2, 1) | |
| 131 | text = "" |
| … | … | @@ -18,6 +18,10 @@ var _selected_idx: int = -1 |
| 18 | 18 | @onready var gc_btn: Button = %GcBtn |
| 19 | 19 | @onready var diagnose_btn: Button = %DiagnoseBtn |
| 20 | 20 | @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 | |
| 21 | 25 | |
| 22 | 26 | |
| 23 | 27 | func _ready() -> void: |
| … | … | @@ -31,6 +35,8 @@ func _ready() -> void: |
| 31 | 35 | gc_btn.pressed.connect(_on_gc) |
| 32 | 36 | diagnose_btn.pressed.connect(_on_diagnose) |
| 33 | 37 | 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) | |
| 34 | 40 | |
| 35 | 41 | KeetService.identity_mgr.identities_loaded.connect(_on_identities_loaded) |
| 36 | 42 | KeetService.identity_mgr.identity_created.connect(func(_e): refresh()) |
| … | … | @@ -44,6 +50,7 @@ func _ready() -> void: |
| 44 | 50 | func refresh() -> void: |
| 45 | 51 | status_label.text = "" |
| 46 | 52 | KeetService.identity_mgr.list_identities() |
| 53 | _load_meta_key() | |
| 47 | 54 | |
| 48 | 55 | |
| 49 | 56 | func _on_identities_loaded(identities: Array, active_id: String) -> void: |
| … | … | @@ -272,6 +279,53 @@ func _on_import() -> void: |
| 272 | 279 | dialog.popup_centered() |
| 273 | 280 | |
| 274 | 281 | |
| 282 | # --- Device Pairing --- | |
| 283 | ||
| 284 | func _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 | ||
| 297 | func _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 | ||
| 304 | func _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 | ||
| 275 | 329 | # --- Helper dialogs --- |
| 276 | 330 | |
| 277 | 331 | func _make_input_dialog(title_text: String, prompt: String) -> ConfirmationDialog: |
| … | … | @@ -11,6 +11,8 @@ |
| 11 | 11 | * 7. meta.set for multiple rooms stores separately |
| 12 | 12 | * 8. meta.get for unknown roomId returns empty object |
| 13 | 13 | * 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 | |
| 14 | 16 | * |
| 15 | 17 | * Requires: keet-daemon running on /tmp/keet-daemon.sock |
| 16 | 18 | * Usage: bare tests/test-admin-channel.mjs |
| … | … | @@ -232,6 +234,43 @@ async function run () { |
| 232 | 234 | failed++ |
| 233 | 235 | } |
| 234 | 236 | |
| 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 | ||
| 235 | 274 | // === Cleanup === |
| 236 | 275 | console.log('\n--- Cleanup ---') |
| 237 | 276 | try { |