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:
2026-01-07 19:46:01 -06:00
parent f717d501d3
commit 192ac21fb0
4 changed files with 144 additions and 12 deletions

View File

@@ -3753,6 +3753,31 @@ class ReticulumMeshChat:
except Exception as e:
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
@routes.get("/api/v1/docs/search")
async def docs_search(request):
@@ -8901,7 +8926,7 @@ class ReticulumMeshChat:
if "telemetry_enabled" in data:
self.config.telemetry_enabled.set(
self._parse_bool(data["telemetry_enabled"])
self._parse_bool(data["telemetry_enabled"]),
)
# update banishment settings
@@ -10285,7 +10310,7 @@ class ReticulumMeshChat:
commands.extend(val)
elif isinstance(val, dict):
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]
if isinstance(val, list):
commands.extend(val)
@@ -10321,11 +10346,11 @@ class ReticulumMeshChat:
else:
# Check if peer is trusted
contact = ctx.database.contacts.get_contact_by_identity_hash(
source_hash
source_hash,
)
if not contact or not contact.get("is_telemetry_trusted"):
print(
f"Telemetry request from untrusted peer {source_hash}, ignoring"
f"Telemetry request from untrusted peer {source_hash}, ignoring",
)
else:
print(f"Responding to telemetry request from {source_hash}")
@@ -10342,7 +10367,7 @@ class ReticulumMeshChat:
"remote_identity_name": source_hash[:8],
"lxmf_message": convert_db_lxmf_message_to_dict(
ctx.database.messages.get_lxmf_message_by_hash(
lxmf_message.hash.hex()
lxmf_message.hash.hex(),
),
include_attachments=False,
),

View File

@@ -134,6 +134,63 @@ class DocsManager:
return True
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):
"""Populates meshchatx-docs from the project's docs folder."""
# Try to find docs folder in several places

View File

@@ -114,7 +114,7 @@
<button
v-for="version in status.versions"
: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="
status.current_version === version
? 'text-blue-600 dark:text-blue-400 font-bold'
@@ -122,12 +122,23 @@
"
@click="switchVersion(version)"
>
<span>{{ version }}</span>
<MaterialDesignIcon
v-if="status.current_version === version"
icon-name="check"
class="w-3.5 h-3.5"
/>
<span class="truncate">{{ version }}</span>
<div class="flex items-center space-x-1">
<MaterialDesignIcon
v-if="status.current_version === version"
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>
<div
v-if="status.versions.length === 0"
@@ -715,6 +726,20 @@ export default {
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) {
const file = event.target.files[0];
if (!file) return;

View File

@@ -303,6 +303,22 @@
</div>
</div>
</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 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"));
}
},
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() {
try {
const response = await window.axios.get("/api/v1/maintenance/messages/export");