extends Window
## Identity manager panel: create, switch, import, export, delete identities.
signal closed
var _identities: Array = []
var _active_id: String = ""
var _selected_idx: int = -1
@onready var active_label: Label = %ActiveLabel
@onready var identity_list: ItemList = %IdentityList
@onready var create_btn: Button = %CreateBtn
@onready var import_btn: Button = %ImportBtn
@onready var export_btn: Button = %ExportBtn
@onready var switch_btn: Button = %SwitchBtn
@onready var delete_btn: Button = %DeleteBtn
@onready var repair_btn: Button = %RepairBtn
@onready var gc_btn: Button = %GcBtn
@onready var diagnose_btn: Button = %DiagnoseBtn
@onready var status_label: Label = %StatusLabel
@onready var meta_key_label: Label = %MetaKeyLabel
@onready var copy_key_btn: Button = %CopyKeyBtn
@onready var pair_device_btn: Button = %PairDeviceBtn
@onready var pair_status_label: Label = %PairStatusLabel
func _ready() -> void:
close_requested.connect(_on_close_requested)
create_btn.pressed.connect(_on_create)
import_btn.pressed.connect(_on_import)
export_btn.pressed.connect(_on_export)
switch_btn.pressed.connect(_on_switch)
delete_btn.pressed.connect(_on_delete)
repair_btn.pressed.connect(_on_repair)
gc_btn.pressed.connect(_on_gc)
diagnose_btn.pressed.connect(_on_diagnose)
identity_list.item_selected.connect(_on_item_selected)
copy_key_btn.pressed.connect(_on_copy_key)
pair_device_btn.pressed.connect(_on_pair_device)
KeetService.identity_mgr.identities_loaded.connect(_on_identities_loaded)
KeetService.identity_mgr.identity_created.connect(func(_e): refresh())
KeetService.identity_mgr.identity_switched.connect(func(_e): refresh())
KeetService.identity_mgr.identity_deleted.connect(func(_id): refresh())
_update_buttons()
refresh()
func refresh() -> void:
status_label.text = ""
KeetService.identity_mgr.list_identities()
_load_meta_key()
func _on_identities_loaded(identities: Array, active_id: String) -> void:
_identities = identities
_active_id = active_id
_selected_idx = -1
identity_list.clear()
for entry: Variant in identities:
if entry is Dictionary:
var id: String = entry.get("id", "")
var label_name: String = entry.get("name", id)
var username: String = entry.get("username", "")
var label_text: String = label_name
if username != "":
label_text += " (@%s)" % username
if id == active_id:
label_text += " [active]"
identity_list.add_item(label_text)
# Update active label
for entry: Variant in identities:
if entry is Dictionary and entry.get("id", "") == active_id:
var name_str: String = entry.get("name", active_id)
var uname: String = entry.get("username", "")
if uname != "":
active_label.text = "Active: %s (@%s)" % [name_str, uname]
else:
active_label.text = "Active: %s" % name_str
break
_update_buttons()
func _on_item_selected(idx: int) -> void:
_selected_idx = idx
_update_buttons()
func _update_buttons() -> void:
var has_selection: bool = _selected_idx >= 0 and _selected_idx < _identities.size()
var is_active: bool = false
if has_selection:
var entry: Variant = _identities[_selected_idx]
if entry is Dictionary:
is_active = entry.get("id", "") == _active_id
switch_btn.disabled = not has_selection or is_active
delete_btn.disabled = not has_selection or is_active
export_btn.disabled = not has_selection
func _get_selected_id() -> String:
if _selected_idx < 0 or _selected_idx >= _identities.size():
return ""
var entry: Variant = _identities[_selected_idx]
if entry is Dictionary:
return entry.get("id", "")
return ""
func _on_close_requested() -> void:
closed.emit()
hide()
# --- Actions ---
func _on_create() -> void:
var dialog := _make_input_dialog("Create Identity", "Identity name:")
dialog.confirmed.connect(func():
var input: LineEdit = dialog.get_node("VBox/Input")
var identity_name: String = input.text.strip_edges()
if identity_name == "":
return
status_label.text = "Creating..."
KeetService.identity_mgr.create_identity(identity_name, func(_entry):
status_label.text = "Created"
)
dialog.queue_free()
)
add_child(dialog)
dialog.popup_centered()
func _on_switch() -> void:
var id: String = _get_selected_id()
if id == "" or id == _active_id:
return
status_label.text = "Switching..."
switch_btn.disabled = true
KeetService.identity_mgr.switch_identity(id, func(_result):
status_label.text = "Switched"
)
func _on_delete() -> void:
var id: String = _get_selected_id()
if id == "" or id == _active_id:
return
var dialog := _make_confirm_dialog("Delete Identity", "Delete identity '%s'?\nThis removes all data permanently." % id)
dialog.confirmed.connect(func():
status_label.text = "Deleting..."
KeetService.identity_mgr.delete_identity(id, true, func(_result):
status_label.text = "Deleted"
)
dialog.queue_free()
)
add_child(dialog)
dialog.popup_centered()
func _on_export() -> void:
var id: String = _get_selected_id()
if id == "":
return
var dialog := _make_password_dialog("Export Identity", "Enter vault password to decrypt:")
dialog.confirmed.connect(func():
var input: LineEdit = dialog.get_node("VBox/Input")
var password: String = input.text
if password == "":
return
status_label.text = "Decrypting..."
KeetService.identity_mgr.export_identity(id, password, func(data):
if data is Dictionary and data.has("phrase"):
var phrase_dialog := _make_phrase_dialog("Recovery Phrase", data["phrase"])
add_child(phrase_dialog)
phrase_dialog.popup_centered()
status_label.text = ""
else:
status_label.text = "Export failed"
)
dialog.queue_free()
)
add_child(dialog)
dialog.popup_centered()
func _on_repair() -> void:
status_label.text = "Repairing..."
repair_btn.disabled = true
KeetService.identity_mgr.repair_identity(func(result):
repair_btn.disabled = false
if result is Dictionary and result.has("log"):
var log: Array = result["log"]
var summary: String = ""
for entry: Variant in log:
if entry is Dictionary:
var step: String = entry.get("step", "?")
var ok: bool = entry.get("ok", false)
var detail: String = entry.get("error", entry.get("result", "")) if not ok else "ok"
summary += "%s: %s " % [step, detail if not ok else "ok"]
status_label.text = summary if summary != "" else "Repair done"
else:
status_label.text = "Repair done"
)
func _on_gc() -> void:
var dialog := _make_confirm_dialog("GC Broken Rooms", "Remove broken rooms and restart core?\nRooms where the writer key doesn't match will be cleaned up.")
dialog.confirmed.connect(func():
status_label.text = "Running GC..."
gc_btn.disabled = true
KeetService.identity_mgr.gc_identity(func(result):
gc_btn.disabled = false
if result is Dictionary and result.has("log"):
var log: Array = result["log"]
var summary: String = ""
for entry: Variant in log:
if entry is Dictionary:
var step: String = entry.get("step", "?")
if step == "broken":
summary += "broken:%s " % entry.get("room", "?")
elif step == "deleted":
summary += "cleaned:%s " % entry.get("room", "?")
elif step == "afterGc":
summary += "remaining:%s" % str(entry.get("roomCount", "?"))
status_label.text = summary if summary != "" else "GC: no broken rooms"
# Refresh room list after GC
KeetService.room_mgr.list_rooms()
else:
status_label.text = "GC done"
)
dialog.queue_free()
)
add_child(dialog)
dialog.popup_centered()
func _on_diagnose() -> void:
status_label.text = "Diagnosing..."
diagnose_btn.disabled = true
KeetService.identity_mgr.diagnose(func(result):
diagnose_btn.disabled = false
if result is Dictionary:
# Show full report in a scrollable dialog
var d := AcceptDialog.new()
d.title = "Diagnostics Report"
d.min_size = Vector2i(500, 400)
var te := TextEdit.new()
te.text = JSON.stringify(result, " ")
te.editable = false
te.custom_minimum_size = Vector2(480, 350)
d.add_child(te)
add_child(d)
d.popup_centered()
status_label.text = ""
else:
status_label.text = "Diagnose failed"
)
func _on_import() -> void:
var dialog := _make_import_dialog()
dialog.confirmed.connect(func():
var name_input: LineEdit = dialog.get_node("VBox/NameInput")
var phrase_input: LineEdit = dialog.get_node("VBox/PhraseInput")
var pass_input: LineEdit = dialog.get_node("VBox/PassInput")
var identity_name: String = name_input.text.strip_edges()
var phrase: String = phrase_input.text.strip_edges()
var password: String = pass_input.text
if identity_name == "" or phrase == "" or password == "":
return
status_label.text = "Importing..."
KeetService.identity_mgr.import_identity(phrase, password, identity_name, func(_result):
status_label.text = "Imported"
)
dialog.queue_free()
)
add_child(dialog)
dialog.popup_centered()
# --- Device Pairing ---
func _load_meta_key() -> void:
pair_status_label.text = ""
KeetService.client.call_rpc("meta.invite", {}, func(result):
if result is Dictionary and result.has("key"):
var key: String = result["key"]
meta_key_label.text = key.substr(0, 16) + "..."
meta_key_label.tooltip_text = key
else:
meta_key_label.text = "(unavailable)"
meta_key_label.tooltip_text = ""
)
func _on_copy_key() -> void:
var full_key: String = meta_key_label.tooltip_text
if full_key != "" and full_key.length() == 64:
DisplayServer.clipboard_set(full_key)
pair_status_label.text = "Key copied"
func _on_pair_device() -> void:
var dialog := _make_input_dialog("Pair Device", "Paste remote device's metadata key:")
dialog.confirmed.connect(func():
var input: LineEdit = dialog.get_node("VBox/Input")
var remote_key: String = input.text.strip_edges()
if remote_key.length() != 64:
pair_status_label.text = "Invalid key (need 64 hex chars)"
dialog.queue_free()
return
pair_status_label.text = "Pairing..."
pair_device_btn.disabled = true
KeetService.client.call_rpc("meta.join", {"key": remote_key}, func(result):
pair_device_btn.disabled = false
if result is Dictionary and result.get("ok", false):
pair_status_label.text = "Paired! Writable: %s" % str(result.get("writable", false))
_load_meta_key()
else:
pair_status_label.text = "Pair failed"
)
dialog.queue_free()
)
add_child(dialog)
dialog.popup_centered()
# --- Helper dialogs ---
func _make_input_dialog(title_text: String, prompt: String) -> ConfirmationDialog:
var d := ConfirmationDialog.new()
d.title = title_text
d.min_size = Vector2i(300, 120)
var vbox := VBoxContainer.new()
vbox.name = "VBox"
var label := Label.new()
label.text = prompt
vbox.add_child(label)
var input := LineEdit.new()
input.name = "Input"
vbox.add_child(input)
d.add_child(vbox)
return d
func _make_confirm_dialog(title_text: String, message: String) -> ConfirmationDialog:
var d := ConfirmationDialog.new()
d.title = title_text
d.dialog_text = message
d.min_size = Vector2i(300, 100)
return d
func _make_password_dialog(title_text: String, prompt: String) -> ConfirmationDialog:
var d := ConfirmationDialog.new()
d.title = title_text
d.min_size = Vector2i(300, 120)
var vbox := VBoxContainer.new()
vbox.name = "VBox"
var label := Label.new()
label.text = prompt
vbox.add_child(label)
var input := LineEdit.new()
input.name = "Input"
input.secret = true
input.placeholder_text = "Password"
vbox.add_child(input)
d.add_child(vbox)
return d
func _make_phrase_dialog(title_text: String, phrase: String) -> AcceptDialog:
var d := AcceptDialog.new()
d.title = title_text
d.min_size = Vector2i(400, 150)
var vbox := VBoxContainer.new()
var label := Label.new()
label.text = "Copy this phrase and store it safely:"
vbox.add_child(label)
var te := TextEdit.new()
te.text = phrase
te.editable = false
te.custom_minimum_size = Vector2(380, 60)
vbox.add_child(te)
d.add_child(vbox)
return d
func _make_import_dialog() -> ConfirmationDialog:
var d := ConfirmationDialog.new()
d.title = "Import Identity"
d.min_size = Vector2i(350, 200)
var vbox := VBoxContainer.new()
vbox.name = "VBox"
var name_label := Label.new()
name_label.text = "Identity name:"
vbox.add_child(name_label)
var name_input := LineEdit.new()
name_input.name = "NameInput"
name_input.placeholder_text = "e.g. Work"
vbox.add_child(name_input)
var phrase_label := Label.new()
phrase_label.text = "Recovery phrase (12 words):"
vbox.add_child(phrase_label)
var phrase_input := LineEdit.new()
phrase_input.name = "PhraseInput"
phrase_input.placeholder_text = "word1 word2 word3 ..."
vbox.add_child(phrase_input)
var pass_label := Label.new()
pass_label.text = "Password for vault:"
vbox.add_child(pass_label)
var pass_input := LineEdit.new()
pass_input.name = "PassInput"
pass_input.secret = true
pass_input.placeholder_text = "Password"
vbox.add_child(pass_input)
d.add_child(vbox)
return d