OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet / scripts / identity_manager_panel.gd

425 lines · gd ref:
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