feat(docs): add API endpoints for deleting documentation versions and clearing Reticulum docs
- Implemented DELETE endpoints to allow users to delete specific documentation versions and clear all Reticulum documentation. - Enhanced the DocsManager class with methods for version deletion and clearing documentation, including error handling and logging. - Updated frontend components to support version deletion and clearing of Reticulum docs with user confirmation dialogs.
This commit is contained in:
@@ -3753,6 +3753,31 @@ class ReticulumMeshChat:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return web.json_response({"error": str(e)}, status=500)
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
# delete docs version
|
||||||
|
@routes.delete("/api/v1/docs/version/{version}")
|
||||||
|
async def docs_delete_version(request):
|
||||||
|
try:
|
||||||
|
version = request.match_info.get("version")
|
||||||
|
if not version:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "No version provided"},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
success = self.docs_manager.delete_version(version)
|
||||||
|
return web.json_response({"success": success})
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
# clear reticulum docs
|
||||||
|
@routes.delete("/api/v1/maintenance/docs/reticulum")
|
||||||
|
async def docs_clear(request):
|
||||||
|
try:
|
||||||
|
success = self.docs_manager.clear_reticulum_docs()
|
||||||
|
return web.json_response({"success": success})
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
# search docs
|
# search docs
|
||||||
@routes.get("/api/v1/docs/search")
|
@routes.get("/api/v1/docs/search")
|
||||||
async def docs_search(request):
|
async def docs_search(request):
|
||||||
@@ -8901,7 +8926,7 @@ class ReticulumMeshChat:
|
|||||||
|
|
||||||
if "telemetry_enabled" in data:
|
if "telemetry_enabled" in data:
|
||||||
self.config.telemetry_enabled.set(
|
self.config.telemetry_enabled.set(
|
||||||
self._parse_bool(data["telemetry_enabled"])
|
self._parse_bool(data["telemetry_enabled"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
# update banishment settings
|
# update banishment settings
|
||||||
@@ -10285,7 +10310,7 @@ class ReticulumMeshChat:
|
|||||||
commands.extend(val)
|
commands.extend(val)
|
||||||
elif isinstance(val, dict):
|
elif isinstance(val, dict):
|
||||||
commands.append(val)
|
commands.append(val)
|
||||||
if 0x01 in lxmf_fields and 0x01 != LXMF.FIELD_COMMANDS:
|
if 0x01 in lxmf_fields and LXMF.FIELD_COMMANDS != 0x01:
|
||||||
val = lxmf_fields[0x01]
|
val = lxmf_fields[0x01]
|
||||||
if isinstance(val, list):
|
if isinstance(val, list):
|
||||||
commands.extend(val)
|
commands.extend(val)
|
||||||
@@ -10321,11 +10346,11 @@ class ReticulumMeshChat:
|
|||||||
else:
|
else:
|
||||||
# Check if peer is trusted
|
# Check if peer is trusted
|
||||||
contact = ctx.database.contacts.get_contact_by_identity_hash(
|
contact = ctx.database.contacts.get_contact_by_identity_hash(
|
||||||
source_hash
|
source_hash,
|
||||||
)
|
)
|
||||||
if not contact or not contact.get("is_telemetry_trusted"):
|
if not contact or not contact.get("is_telemetry_trusted"):
|
||||||
print(
|
print(
|
||||||
f"Telemetry request from untrusted peer {source_hash}, ignoring"
|
f"Telemetry request from untrusted peer {source_hash}, ignoring",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(f"Responding to telemetry request from {source_hash}")
|
print(f"Responding to telemetry request from {source_hash}")
|
||||||
@@ -10342,7 +10367,7 @@ class ReticulumMeshChat:
|
|||||||
"remote_identity_name": source_hash[:8],
|
"remote_identity_name": source_hash[:8],
|
||||||
"lxmf_message": convert_db_lxmf_message_to_dict(
|
"lxmf_message": convert_db_lxmf_message_to_dict(
|
||||||
ctx.database.messages.get_lxmf_message_by_hash(
|
ctx.database.messages.get_lxmf_message_by_hash(
|
||||||
lxmf_message.hash.hex()
|
lxmf_message.hash.hex(),
|
||||||
),
|
),
|
||||||
include_attachments=False,
|
include_attachments=False,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -134,6 +134,63 @@ class DocsManager:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def delete_version(self, version):
|
||||||
|
"""Deletes a specific version of documentation."""
|
||||||
|
if version not in self.get_available_versions():
|
||||||
|
return False
|
||||||
|
|
||||||
|
version_path = os.path.join(self.versions_dir, version)
|
||||||
|
if not os.path.exists(version_path):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# If the deleted version is the current one, unlink 'current' first
|
||||||
|
current_version = self.get_current_version()
|
||||||
|
if current_version == version:
|
||||||
|
if os.path.exists(self.docs_dir):
|
||||||
|
if os.path.islink(self.docs_dir):
|
||||||
|
os.unlink(self.docs_dir)
|
||||||
|
else:
|
||||||
|
shutil.rmtree(self.docs_dir)
|
||||||
|
|
||||||
|
shutil.rmtree(version_path)
|
||||||
|
|
||||||
|
# If we just deleted the current version, try to pick another one as current
|
||||||
|
if current_version == version:
|
||||||
|
self._update_current_link()
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"Failed to delete docs version {version}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def clear_reticulum_docs(self):
|
||||||
|
"""Clears all Reticulum documentation and versions."""
|
||||||
|
try:
|
||||||
|
if os.path.exists(self.docs_base_dir):
|
||||||
|
# We don't want to delete the base dir itself, just its contents
|
||||||
|
# except possibly some metadata if we added any.
|
||||||
|
# Actually, deleting everything inside reticulum-docs is fine.
|
||||||
|
for item in os.listdir(self.docs_base_dir):
|
||||||
|
item_path = os.path.join(self.docs_base_dir, item)
|
||||||
|
if os.path.islink(item_path):
|
||||||
|
os.unlink(item_path)
|
||||||
|
elif os.path.isdir(item_path):
|
||||||
|
shutil.rmtree(item_path)
|
||||||
|
else:
|
||||||
|
os.remove(item_path)
|
||||||
|
|
||||||
|
# Re-create required subdirectories
|
||||||
|
for d in [self.versions_dir, self.docs_dir]:
|
||||||
|
if not os.path.exists(d):
|
||||||
|
os.makedirs(d)
|
||||||
|
|
||||||
|
self.config.docs_downloaded.set(False)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"Failed to clear Reticulum docs: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def populate_meshchatx_docs(self):
|
def populate_meshchatx_docs(self):
|
||||||
"""Populates meshchatx-docs from the project's docs folder."""
|
"""Populates meshchatx-docs from the project's docs folder."""
|
||||||
# Try to find docs folder in several places
|
# Try to find docs folder in several places
|
||||||
|
|||||||
@@ -114,7 +114,7 @@
|
|||||||
<button
|
<button
|
||||||
v-for="version in status.versions"
|
v-for="version in status.versions"
|
||||||
:key="version"
|
:key="version"
|
||||||
class="w-full px-4 py-2 text-left text-[11px] hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors flex items-center justify-between"
|
class="w-full px-4 py-2 text-left text-[11px] hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors flex items-center justify-between group"
|
||||||
:class="
|
:class="
|
||||||
status.current_version === version
|
status.current_version === version
|
||||||
? 'text-blue-600 dark:text-blue-400 font-bold'
|
? 'text-blue-600 dark:text-blue-400 font-bold'
|
||||||
@@ -122,12 +122,23 @@
|
|||||||
"
|
"
|
||||||
@click="switchVersion(version)"
|
@click="switchVersion(version)"
|
||||||
>
|
>
|
||||||
<span>{{ version }}</span>
|
<span class="truncate">{{ version }}</span>
|
||||||
<MaterialDesignIcon
|
<div class="flex items-center space-x-1">
|
||||||
v-if="status.current_version === version"
|
<MaterialDesignIcon
|
||||||
icon-name="check"
|
v-if="status.current_version === version"
|
||||||
class="w-3.5 h-3.5"
|
icon-name="check"
|
||||||
/>
|
class="w-3.5 h-3.5"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="status.versions.length > 1"
|
||||||
|
type="button"
|
||||||
|
class="p-1 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
title="Delete this version"
|
||||||
|
@click.stop="deleteVersion(version)"
|
||||||
|
>
|
||||||
|
<MaterialDesignIcon icon-name="delete" class="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-if="status.versions.length === 0"
|
v-if="status.versions.length === 0"
|
||||||
@@ -715,6 +726,20 @@ export default {
|
|||||||
console.error("Failed to switch docs version:", error);
|
console.error("Failed to switch docs version:", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async deleteVersion(version) {
|
||||||
|
if (!confirm(`Are you sure you want to delete version "${version}"?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.axios.delete(`/api/v1/docs/version/${encodeURIComponent(version)}`);
|
||||||
|
this.fetchStatus();
|
||||||
|
ToastUtils.success(`Version ${version} deleted`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete docs version:", error);
|
||||||
|
ToastUtils.error("Failed to delete version: " + (error.response?.data?.error || error.message));
|
||||||
|
}
|
||||||
|
},
|
||||||
async handleZipUpload(event) {
|
async handleZipUpload(event) {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|||||||
@@ -303,6 +303,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-maintenance border-orange-200 dark:border-orange-900/30 text-orange-700 dark:text-orange-300 bg-orange-50 dark:bg-orange-900/10 hover:bg-orange-100 dark:hover:bg-orange-900/20"
|
||||||
|
@click="clearReticulumDocs"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-start text-left">
|
||||||
|
<div class="font-bold flex items-center gap-2">
|
||||||
|
<MaterialDesignIcon icon-name="book-remove" class="size-4" />
|
||||||
|
{{ $t("maintenance.clear_reticulum_docs") }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs opacity-80">
|
||||||
|
{{ $t("maintenance.clear_reticulum_docs_desc") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2 pt-2 border-t border-gray-100 dark:border-zinc-800">
|
<div class="space-y-2 pt-2 border-t border-gray-100 dark:border-zinc-800">
|
||||||
@@ -2218,6 +2234,15 @@ export default {
|
|||||||
ToastUtils.error(this.$t("common.error"));
|
ToastUtils.error(this.$t("common.error"));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async clearReticulumDocs() {
|
||||||
|
if (!(await DialogUtils.confirm(this.$t("maintenance.clear_confirm")))) return;
|
||||||
|
try {
|
||||||
|
await window.axios.delete("/api/v1/maintenance/docs/reticulum");
|
||||||
|
ToastUtils.success(this.$t("maintenance.docs_cleared"));
|
||||||
|
} catch {
|
||||||
|
ToastUtils.error(this.$t("common.error"));
|
||||||
|
}
|
||||||
|
},
|
||||||
async exportMessages() {
|
async exportMessages() {
|
||||||
try {
|
try {
|
||||||
const response = await window.axios.get("/api/v1/maintenance/messages/export");
|
const response = await window.axios.get("/api/v1/maintenance/messages/export");
|
||||||
|
|||||||
Reference in New Issue
Block a user