refactor(docs): update Reticulum manual fetching process to remove GitHub update functionality and ensure build-time fetching with --force option

This commit is contained in:
Ivan 2026-07-10 13:02:33 -05:00
parent 0c80fef6e9
commit f62b203beb
No known key found for this signature in database
19 changed files with 59 additions and 280 deletions

View file

@ -126,13 +126,13 @@ CLI flags override environment variables when both are set.
## Reticulum manual bundle
The Reticulum HTML manual is fetched at build time. After cloning the repository, run:
The Reticulum HTML manual is fetched from the upstream website **master** branch at build time. There is no in-app clearnet refresh. After cloning the repository, or before packaging a release, run:
```bash
pnpm run build-docs
```
This populates `meshchatx/public/reticulum-docs-bundled/current/`. Without that step the Reticulum tab may show an upload prompt until you build docs or upload a manual ZIP.
That command always re-fetches (`--force`) into `meshchatx/public/reticulum-docs-bundled/current/`. CI release builds run the same step. Without a bundled copy the Reticulum tab may show an upload prompt until you build docs or upload a manual ZIP offline.
## Identity bootstrap

View file

@ -7808,27 +7808,6 @@ class ReticulumMeshChat:
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# pull latest Reticulum manual from GitHub
@routes.post("/api/v1/docs/update-from-github")
async def docs_update_from_github(request):
try:
self._require_outbound_http("Reticulum docs update")
if not self.docs_manager:
return web.json_response(
{"error": "Documentation manager is unavailable"},
status=503,
)
loop = asyncio.get_running_loop()
success, version = await loop.run_in_executor(
None,
self.docs_manager.update_from_github,
)
return web.json_response({"success": success, "version": version})
except OutboundHttpBlockedError as e:
return web.json_response({"error": str(e)}, status=403)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# switch docs version
@routes.post("/api/v1/docs/switch")
async def docs_switch(request):

View file

@ -7,10 +7,6 @@ import logging
import os
import re
import shutil
import time
import urllib.error
import urllib.parse
import urllib.request
import zipfile
from meshchatx.src.backend.markdown_renderer import MarkdownRenderer
@ -18,16 +14,6 @@ from meshchatx.src.backend.markdown_renderer import MarkdownRenderer
BUNDLED_DOCS_SUBDIR = os.path.join("reticulum-docs-bundled", "current")
MANIFEST_FILENAME = "manifest.json"
DOC_FILE_SUFFIXES = (".md", ".txt")
RETICULUM_DOCS_GITHUB_URL = (
"https://github.com/markqvist/reticulum_website/archive/refs/heads/main.zip"
)
RETICULUM_DOCS_ALLOWED_HOSTS = frozenset(
{
"github.com",
"codeload.github.com",
"objects.githubusercontent.com",
}
)
class DocsManager:
@ -37,9 +23,8 @@ class DocsManager:
``<public_dir>/reticulum-docs-bundled/current``. Users may upload a
replacement archive which is extracted into ``<storage_dir>/reticulum-docs``
and takes precedence at request time. Removing the user upload restores the
bundled copy. Users can also pull the latest upstream ZIP from GitHub at
runtime (blocked when privacy mode is enabled). Build-time staging still
uses ``scripts/build/fetch_reticulum_manual.py``.
bundled copy. There is no runtime download path. Fresh manuals are staged at
build time with ``scripts/build/fetch_reticulum_manual.py`` (``pnpm run build-docs``).
"""
def __init__(self, config, public_dir, project_root=None, storage_dir=None):
@ -838,50 +823,6 @@ class DocsManager:
logging.exception(f"Failed to upload docs: {e}")
return False
@staticmethod
def _resolve_docs_source_url(source_url=None):
url = (
source_url
or os.environ.get("MESHCHATX_RETICULUM_DOCS_URL")
or RETICULUM_DOCS_GITHUB_URL
).strip()
if not url.lower().startswith("https://"):
raise ValueError("docs source URL must be https")
host = urllib.parse.urlparse(url).hostname or ""
host = host.lower()
if host not in RETICULUM_DOCS_ALLOWED_HOSTS and not host.endswith(
".githubusercontent.com"
):
raise ValueError(f"docs source host not allowed: {host}")
return url
def update_from_github(self, version=None, source_url=None, timeout=120.0):
"""Download the Reticulum website docs ZIP and install it as a version."""
url = self._resolve_docs_source_url(source_url)
if not version:
version = f"github-{time.strftime('%Y%m%d-%H%M%S')}"
self.upload_status = "downloading"
self.upload_progress = 0
self.last_error = None
try:
req = urllib.request.Request(
url,
headers={"User-Agent": "MeshChatX-docs-update"},
)
with urllib.request.urlopen(req, timeout=timeout) as response:
zip_bytes = response.read()
if not zip_bytes:
raise ValueError("downloaded archive is empty")
success = self.upload_zip(zip_bytes, version)
return success, version
except Exception as e:
self.last_error = str(e)
self.upload_status = "error"
logging.exception(f"Failed to update docs from GitHub: {e}")
raise
def _extract_docs(self, zip_path, version):
safe_version = os.path.basename(version)
if not safe_version or safe_version in (".", ".."):

View file

@ -186,22 +186,6 @@
<MaterialDesignIcon icon-name="download" class="w-4 h-4 md:w-5 md:h-5" />
</button>
<!-- Update from GitHub -->
<button
type="button"
class="p-1.5 text-gray-500 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded-lg transition-colors"
:class="{ 'opacity-50 pointer-events-none': docsBusy }"
:title="$t('docs.btn_update_github')"
:disabled="docsBusy"
@click="updateFromGithub"
>
<MaterialDesignIcon
:icon-name="docsBusy ? 'loading' : 'update'"
:class="{ 'animate-spin': docsBusy }"
class="w-4 h-4 md:w-5 md:h-5"
/>
</button>
<!-- Share Reticulum Manual (re-uploadable ZIP) -->
<button
v-if="status.has_docs"
@ -214,16 +198,22 @@
<!-- Upload Custom Manual -->
<label
:class="{ 'opacity-50 pointer-events-none': docsBusy }"
:class="{ 'opacity-50 pointer-events-none': status.status === 'extracting' }"
class="p-1.5 text-gray-500 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded-lg transition-colors cursor-pointer"
:title="$t('docs.btn_upload')"
>
<MaterialDesignIcon
:icon-name="docsBusy ? 'loading' : 'upload'"
:class="{ 'animate-spin': docsBusy }"
:icon-name="status.status === 'extracting' ? 'loading' : 'upload'"
:class="{ 'animate-spin': status.status === 'extracting' }"
class="w-4 h-4 md:w-5 md:h-5"
/>
<input type="file" accept=".zip" class="hidden" :disabled="docsBusy" @change="handleZipUpload" />
<input
type="file"
accept=".zip"
class="hidden"
:disabled="status.status === 'extracting'"
@change="handleZipUpload"
/>
</label>
<!-- Open External -->
@ -301,7 +291,10 @@
</div>
<!-- Progress Bar -->
<div v-if="docsBusy" class="w-full h-1 bg-gray-200 dark:bg-zinc-800 overflow-hidden relative">
<div
v-if="status.status === 'extracting'"
class="w-full h-1 bg-gray-200 dark:bg-zinc-800 overflow-hidden relative"
>
<div class="bg-blue-500 h-full transition-all duration-300" :style="{ width: status.progress + '%' }"></div>
<div class="absolute inset-0 bg-blue-500/30 animate-pulse"></div>
</div>
@ -422,7 +415,7 @@
</div>
<div
v-if="docsBusy"
v-if="status.status === 'extracting'"
class="absolute inset-0 z-10 flex flex-col items-center justify-center bg-white/80 dark:bg-zinc-900/80 backdrop-blur-md"
>
<div class="relative w-24 h-24 mb-6">
@ -434,13 +427,13 @@
></div>
<div class="absolute inset-0 flex items-center justify-center">
<MaterialDesignIcon
:icon-name="status.status === 'downloading' ? 'cloud-download' : 'folder-zip-outline'"
icon-name="folder-zip-outline"
class="w-10 h-10 text-blue-600 animate-bounce"
/>
</div>
</div>
<h3 class="text-lg font-bold text-gray-900 dark:text-zinc-100 mb-1">
{{ status.status === "downloading" ? $t("docs.status_downloading") : $t("docs.status_extracting") }}
{{ $t("docs.status_extracting") }}
</h3>
<p class="text-sm text-gray-500 dark:text-zinc-400">
{{ $t("docs.complete_percent", { percent: status.progress }) }}
@ -617,7 +610,9 @@
></iframe>
<div
v-else-if="activeTab === 'reticulum' && !status.has_docs && !docsBusy && !searchQuery"
v-else-if="
activeTab === 'reticulum' && !status.has_docs && status.status !== 'extracting' && !searchQuery
"
class="h-full flex flex-col items-center justify-center p-8 text-center space-y-4"
>
<div class="w-16 h-16 bg-gray-50 dark:bg-zinc-800/50 rounded-full flex items-center justify-center">
@ -631,24 +626,13 @@
{{ $t("docs.empty_state_hint") }}
</p>
</div>
<div class="flex flex-col sm:flex-row gap-2">
<button
type="button"
class="px-6 py-2 bg-blue-600 text-white rounded-full text-xs font-bold hover:bg-blue-700 transition-colors shadow-lg shadow-blue-500/20 flex items-center justify-center gap-2"
:disabled="docsBusy"
@click="updateFromGithub"
>
<MaterialDesignIcon icon-name="update" class="w-3.5 h-3.5" />
<span>{{ $t("docs.btn_update_github") }}</span>
</button>
<label
class="px-6 py-2 bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 rounded-full text-xs font-bold hover:opacity-90 transition-opacity cursor-pointer flex items-center justify-center gap-2"
>
<MaterialDesignIcon icon-name="upload" class="w-3.5 h-3.5" />
<span>{{ $t("docs.btn_upload") }}</span>
<input type="file" accept=".zip" class="hidden" @change="handleZipUpload" />
</label>
</div>
<label
class="px-6 py-2 bg-blue-600 text-white rounded-full text-xs font-bold hover:bg-blue-700 transition-colors shadow-lg shadow-blue-500/20 cursor-pointer flex items-center gap-2"
>
<MaterialDesignIcon icon-name="upload" class="w-3.5 h-3.5" />
<span>{{ $t("docs.btn_upload") }}</span>
<input type="file" accept=".zip" class="hidden" @change="handleZipUpload" />
</label>
</div>
</div>
</div>
@ -699,7 +683,7 @@ export default {
selectedDocPath: null,
selectedDocContent: null,
selectedReticulumPath: null,
githubUpdatePending: false,
reticulumDocsCacheBust: 0,
languages: {
en: "English",
de: "Deutsch",
@ -719,10 +703,17 @@ export default {
return this.$i18n.locale;
},
localDocsUrl() {
let path;
if (this.selectedReticulumPath) {
return `/reticulum-docs/${this.selectedReticulumPath}`;
path = `/reticulum-docs/${this.selectedReticulumPath}`;
} else {
path = bundledReticulumDocsUrl(this.currentLang);
}
return bundledReticulumDocsUrl(this.currentLang);
if (this.reticulumDocsCacheBust) {
const sep = path.includes("?") ? "&" : "?";
return `${path}${sep}v=${this.reticulumDocsCacheBust}`;
}
return path;
},
allLanguages() {
return Object.entries(this.languages).map(([code, name]) => ({
@ -737,11 +728,6 @@ export default {
reticulumDocsQueryParam() {
return this.$route?.query?.reticulum;
},
docsBusy() {
return (
this.githubUpdatePending || this.status.status === "downloading" || this.status.status === "extracting"
);
},
visibleDocSections() {
const lang = this.meshchatxDocsLang;
const fallback = this.defaultDocsLanguage || "en";
@ -963,6 +949,7 @@ export default {
}
);
this.fetchStatus();
this.reticulumDocsCacheBust = Date.now();
ToastUtils.success(this.$t("docs.upload_success"));
} catch (error) {
console.error("Failed to upload docs zip:", error);
@ -970,37 +957,6 @@ export default {
DialogUtils.alert(this.$t("docs.failed_upload_alert", { message }), "error");
}
},
async updateFromGithub() {
if (this.docsBusy) {
return;
}
this.githubUpdatePending = true;
this.status = {
...this.status,
status: "downloading",
progress: 0,
last_error: null,
};
try {
const response = await window.api.post("/api/v1/docs/update-from-github");
const version = response.data?.version;
await this.fetchStatus();
this.activeTab = "reticulum";
this.selectedReticulumPath = null;
ToastUtils.success(
version
? this.$t("docs.update_github_success_version", { version })
: this.$t("docs.update_github_success")
);
} catch (error) {
console.error("Failed to update docs from GitHub:", error);
const message = error.response?.data?.error || error.message || "";
DialogUtils.alert(this.$t("docs.failed_update_github", { message }), "error");
await this.fetchStatus();
} finally {
this.githubUpdatePending = false;
}
},
async exportDocs() {
window.location.href = "/api/v1/docs/export";
},

View file

@ -1967,19 +1967,14 @@
"prompt_version_name": "Versionsname für diesen Upload eingeben:",
"status_title": "Dokumentationsstatus",
"status_extracting": "Wird entpackt...",
"status_downloading": "Dokumentation wird heruntergeladen...",
"status_available": "Offline-Handbuch verfügbar",
"status_not_available": "Handbuch nicht verfügbar",
"btn_upload": "Handbuch hochladen",
"btn_update_github": "Von GitHub aktualisieren",
"btn_share": "Reticulum-Handbuch als wieder hochladbares ZIP teilen",
"empty_state_hint": "Lade das neueste Reticulum-Handbuch von GitHub, oder lade ein ZIP hoch, um es offline anzuzeigen.",
"empty_state_hint": "Lade ein ZIP des Reticulum-Handbuchs hoch, um es offline anzuzeigen. Releases enthalten eine gebündelte Offline-Kopie aus dem Build.",
"error": "Fehler",
"failed_upload_docs": "Hochladen der Dokumentation fehlgeschlagen",
"upload_success": "Dokumentation hochgeladen",
"update_github_success": "Reticulum-Handbuch von GitHub aktualisiert",
"update_github_success_version": "Reticulum-Handbuch aktualisiert ({version})",
"failed_update_github": "Aktualisierung von GitHub fehlgeschlagen: {message}",
"docs_link_copied": "Dokumentationslink in Zwischenablage kopiert",
"failed_copy_link": "Link kopieren fehlgeschlagen",
"load_list_failed": "Anleitungsliste konnte nicht geladen werden. Bitte später erneut versuchen.",

View file

@ -2087,23 +2087,14 @@
"status_available": "Offline Manual Available",
"status_not_available": "Manual Not Available",
"btn_upload": "Upload Manual",
"btn_update_github": "Update from GitHub",
"btn_share": "Share Reticulum manual as a re-uploadable ZIP",
"empty_state_hint": "Pull the latest Reticulum manual from GitHub, or upload a ZIP to view it offline.",
"error": "Error",
"failed_upload_docs": "Failed to upload documentation",
"upload_success": "Documentation uploaded",
"update_github_success": "Reticulum manual updated from GitHub",
"update_github_success_version": "Reticulum manual updated ({version})",
"failed_update_github": "Failed to update from GitHub: {message}",
"status_downloading": "Downloading Documentation...",
"docs_link_copied": "Documentation link copied to clipboard",
"failed_copy_link": "Failed to copy link",
"load_list_failed": "Could not load the guide list. Try again in a moment.",
"load_doc_failed": "Could not load this guide.",
"search_failed": "Search failed. Check your connection and try again.",
"manifest_warning": "The documentation index file could not be read. Showing available files without section grouping.",
"failed_upload_alert": "Failed to upload documentation: {message}"
"failed_upload_alert": "Failed to upload documentation: {message}",
"empty_state_hint": "Upload a Reticulum manual ZIP to view it offline. Releases ship a bundled offline copy built at packaging time."
},
"licenses": {
"section_label": "Legal",

View file

@ -2084,19 +2084,14 @@
"prompt_version_name": "Introduce un nombre de versión para esta subida:",
"status_title": "Situación de la documentación",
"status_extracting": "Extrayendo...",
"status_downloading": "Descargando documentación...",
"status_available": "Manual disponible",
"status_not_available": "Manual no disponible",
"btn_upload": "Subir Manual",
"btn_update_github": "Actualizar desde GitHub",
"btn_share": "Compartir el manual de Reticulum como ZIP reimportable",
"empty_state_hint": "Descarga el manual de Reticulum más reciente desde GitHub, o sube un ZIP para verlo sin conexión.",
"empty_state_hint": "Sube un ZIP del manual de Reticulum para consultarlo sin conexión. Las versiones incluyen una copia sin conexión empaquetada en el build.",
"error": "Error",
"failed_upload_docs": "Error al subir la documentación",
"upload_success": "Documentación subida",
"update_github_success": "Manual de Reticulum actualizado desde GitHub",
"update_github_success_version": "Manual de Reticulum actualizado ({version})",
"failed_update_github": "Error al actualizar desde GitHub: {message}",
"docs_link_copied": "Enlace de documentación copiado al portapapeles",
"failed_copy_link": "No se pudo copiar el enlace",
"load_list_failed": "No se pudo cargar la lista de guías. Inténtalo de nuevo en un momento.",

View file

@ -2084,19 +2084,14 @@
"prompt_version_name": "Anna tälle lataukselle version nimi:",
"status_title": "Dokumentaation tila",
"status_extracting": "Viedään dokumentaatiota...",
"status_downloading": "Ladataan dokumentaatiota...",
"status_available": "Paikallinen ohjekirja saatavilla",
"status_not_available": "Ohjekirja ei ole saatavilla",
"btn_upload": "Tuo ohjekirja",
"btn_update_github": "Päivitä GitHubista",
"btn_share": "Jaa Reticulum-ohjekirja ZIP-tiedostona, jonka voi myöhemmin tuoda sovellukseen",
"empty_state_hint": "Hae uusin Reticulum-ohjekirja GitHubista tai tuo ZIP paikallista käyttöä varten.",
"empty_state_hint": "Tuo Reticulum-ohjekirjan ZIP paikallista käyttöä varten. Julkaisut sisältävät buildissa pakatun offline-kopion.",
"error": "Virhe",
"failed_upload_docs": "Dokumentaation tuominen epäonnistui",
"upload_success": "Dokumentaatio ladattu",
"update_github_success": "Reticulum-ohjekirja päivitetty GitHubista",
"update_github_success_version": "Reticulum-ohjekirja päivitetty ({version})",
"failed_update_github": "Päivitys GitHubista epäonnistui: {message}",
"docs_link_copied": "Dokumentaation linkki kopioitu leikepöydälle",
"failed_copy_link": "Linkin kopiointi epäonnistui",
"load_list_failed": "Ohjeluetteloa ei voitu ladata. Yritä hetken kuluttua uudelleen.",

View file

@ -2084,19 +2084,14 @@
"prompt_version_name": "Entrez un nom de version pour ce téléversement :",
"status_title": "État de la documentation",
"status_extracting": "Extraction...",
"status_downloading": "Téléchargement de la documentation...",
"status_available": "Manuel hors ligne disponible",
"status_not_available": "Manuel non disponible",
"btn_upload": "Téléverser le manuel",
"btn_update_github": "Mettre à jour depuis GitHub",
"btn_share": "Partager le manuel Reticulum sous forme de ZIP réimportable",
"empty_state_hint": "Récupérez le dernier manuel Reticulum depuis GitHub, ou téléversez une archive ZIP pour le consulter hors ligne.",
"empty_state_hint": "Téléversez une archive ZIP du manuel Reticulum pour la consulter hors ligne. Les versions livrent une copie hors ligne intégrée au build.",
"error": "Erreur",
"failed_upload_docs": "Échec du téléversement de la documentation",
"upload_success": "Documentation téléversée",
"update_github_success": "Manuel Reticulum mis à jour depuis GitHub",
"update_github_success_version": "Manuel Reticulum mis à jour ({version})",
"failed_update_github": "Échec de la mise à jour depuis GitHub : {message}",
"docs_link_copied": "Lien de documentation copié dans le presse-papiers",
"failed_copy_link": "Impossible de copier le lien",
"load_list_failed": "Impossible de charger la liste des guides. Réessayez dans un instant.",

View file

@ -2136,19 +2136,14 @@
"prompt_version_name": "Inserisci un nome di versione per questo caricamento:",
"status_title": "Stato Documentazione",
"status_extracting": "Estrazione in corso...",
"status_downloading": "Download della documentazione...",
"status_available": "Manuale Offline Disponibile",
"status_not_available": "Manuale Non Disponibile",
"btn_upload": "Carica Manuale",
"btn_update_github": "Aggiorna da GitHub",
"btn_share": "Condividi il manuale Reticulum come ZIP ricaricabile",
"empty_state_hint": "Scarica l'ultimo manuale Reticulum da GitHub, oppure carica uno ZIP per consultarlo offline.",
"empty_state_hint": "Carica uno ZIP del manuale Reticulum per consultarlo offline. Le release includono una copia offline inclusa nel build.",
"error": "Errore",
"failed_upload_docs": "Caricamento della documentazione non riuscito",
"upload_success": "Documentazione caricata",
"update_github_success": "Manuale Reticulum aggiornato da GitHub",
"update_github_success_version": "Manuale Reticulum aggiornato ({version})",
"failed_update_github": "Aggiornamento da GitHub non riuscito: {message}",
"docs_link_copied": "Link alla documentazione copiato negli appunti",
"failed_copy_link": "Impossibile copiare il link",
"load_list_failed": "Impossibile caricare l'elenco delle guide. Riprova tra un momento.",

View file

@ -2084,19 +2084,14 @@
"prompt_version_name": "Voer een versienaam in voor deze upload:",
"status_title": "Documentatiestatus",
"status_extracting": "Uitpakken...",
"status_downloading": "Documentatie downloaden...",
"status_available": "Offline handleiding beschikbaar",
"status_not_available": "Handleiding niet beschikbaar",
"btn_upload": "Handleiding uploaden",
"btn_update_github": "Bijwerken vanaf GitHub",
"btn_share": "Reticulum-handleiding delen als opnieuw uploadbare ZIP",
"empty_state_hint": "Haal de nieuwste Reticulum-handleiding van GitHub, of upload een ZIP om deze offline te bekijken.",
"empty_state_hint": "Upload een ZIP van de Reticulum-handleiding om deze offline te bekijken. Releases bevatten een gebundelde offline kopie uit de build.",
"error": "Fout",
"failed_upload_docs": "Documentatie uploaden mislukt",
"upload_success": "Documentatie geüpload",
"update_github_success": "Reticulum-handleiding bijgewerkt vanaf GitHub",
"update_github_success_version": "Reticulum-handleiding bijgewerkt ({version})",
"failed_update_github": "Bijwerken vanaf GitHub mislukt: {message}",
"docs_link_copied": "Documentatiekoppeling gekopieerd naar klembord",
"failed_copy_link": "Kopiëren van verwijzing is mislukt",
"load_list_failed": "Kan de gidsenlijst niet laden. Probeer het zo opnieuw.",

View file

@ -1967,19 +1967,14 @@
"prompt_version_name": "Введите имя версии для этой загрузки:",
"status_title": "Статус документации",
"status_extracting": "Извлечение...",
"status_downloading": "Загрузка документации...",
"status_available": "Доступно офлайн",
"status_not_available": "Руководство недоступно",
"btn_upload": "Загрузить руководство",
"btn_update_github": "Обновить с GitHub",
"btn_share": "Поделиться руководством Reticulum как загружаемым ZIP",
"empty_state_hint": "Загрузите актуальное руководство Reticulum с GitHub или загрузите ZIP для офлайн-просмотра.",
"empty_state_hint": "Загрузите ZIP руководства Reticulum для офлайн-просмотра. В релизах есть встроенная офлайн-копия из сборки.",
"error": "Ошибка",
"failed_upload_docs": "Не удалось загрузить документацию",
"upload_success": "Документация загружена",
"update_github_success": "Руководство Reticulum обновлено с GitHub",
"update_github_success_version": "Руководство Reticulum обновлено ({version})",
"failed_update_github": "Не удалось обновить с GitHub: {message}",
"docs_link_copied": "Ссылка на документацию скопирована в буфер обмена",
"failed_copy_link": "Не удалось скопировать ссылку",
"load_list_failed": "Не удалось загрузить список справок. Повторите попытку позже.",

View file

@ -2084,19 +2084,14 @@
"prompt_version_name": "为此上传输入版本名称:",
"status_title": "文件状况",
"status_extracting": "正在解压...",
"status_downloading": "正在下载文档...",
"status_available": "离线手册可用",
"status_not_available": "手册不可用",
"btn_upload": "上传手册",
"btn_update_github": "从 GitHub 更新",
"btn_share": "以可重新上传的 ZIP 共享 Reticulum 手册",
"empty_state_hint": "从 GitHub 拉取最新 Reticulum 手册,或上传 ZIP 以便离线查看。",
"empty_state_hint": "上传 Reticulum 手册 ZIP 以便离线查看。发行版在构建时打包离线副本。",
"error": "错误",
"failed_upload_docs": "上传文档失败",
"upload_success": "文档已上传",
"update_github_success": "已从 GitHub 更新 Reticulum 手册",
"update_github_success_version": "已更新 Reticulum 手册({version}",
"failed_update_github": "从 GitHub 更新失败:{message}",
"docs_link_copied": "复制到剪贴板的文档链接",
"failed_copy_link": "复制链接失败",
"load_list_failed": "无法加载指南列表。请稍后再试。",

View file

@ -126,13 +126,13 @@ CLI flags override environment variables when both are set.
## Reticulum manual bundle
The Reticulum HTML manual is fetched at build time. After cloning the repository, run:
The Reticulum HTML manual is fetched from the upstream website **master** branch at build time. There is no in-app clearnet refresh. After cloning the repository, or before packaging a release, run:
```bash
pnpm run build-docs
```
This populates `meshchatx/public/reticulum-docs-bundled/current/`. Without that step the Reticulum tab may show an upload prompt until you build docs or upload a manual ZIP.
That command always re-fetches (`--force`) into `meshchatx/public/reticulum-docs-bundled/current/`. CI release builds run the same step. Without a bundled copy the Reticulum tab may show an upload prompt until you build docs or upload a manual ZIP offline.
## Identity bootstrap

View file

@ -15,7 +15,7 @@
"predev": "node scripts/sync-meshchatx-docs.js",
"build-frontend": "vite build",
"build-backend": "node scripts/build-backend.js",
"build-docs": "python3 scripts/build/fetch_reticulum_manual.py",
"build-docs": "python3 scripts/build/fetch_reticulum_manual.py --force",
"build-repository-wheels": "python3 scripts/build/fetch_repository_wheels.py",
"version:sync": "node scripts/sync_version.js",
"build": "pnpm run version:sync && pnpm run build-frontend && pnpm run build-docs && pnpm run build-repository-wheels && pnpm run build-backend",

View file

@ -39,7 +39,7 @@ from datetime import UTC, datetime
from pathlib import Path
DEFAULT_SOURCES = (
"https://github.com/markqvist/reticulum_website/archive/refs/heads/main.zip",
"https://codeload.github.com/markqvist/reticulum_website/zip/refs/heads/master",
)
DEFAULT_DEST = (

View file

@ -1,7 +1,7 @@
{
"dest": "meshchatx/public/reticulum-docs-bundled/current",
"fetched_utc": "2026-07-07T05:31:03Z",
"html_files": 201,
"fetched_utc": "2026-07-10T17:45:25Z",
"html_files": 206,
"skipped_binary_files": 2,
"source_url": "https://github.com/markqvist/reticulum_website/archive/refs/heads/main.zip"
"source_url": "https://codeload.github.com/markqvist/reticulum_website/zip/refs/heads/master"
}

View file

@ -268,10 +268,6 @@
"method": "POST",
"path": "/api/v1/docs/upload"
},
{
"method": "POST",
"path": "/api/v1/docs/update-from-github"
},
{
"method": "DELETE",
"path": "/api/v1/docs/version/{version}"

View file

@ -544,47 +544,3 @@ def test_get_doc_content_returns_none_on_read_error(tmp_path, monkeypatch):
monkeypatch.setattr("builtins.open", fail_open)
assert dm.get_doc_content("en/guide.md") is None
def test_resolve_docs_source_url_rejects_non_https():
with pytest.raises(ValueError, match="https"):
DocsManager._resolve_docs_source_url(
"http://github.com/markqvist/reticulum_website/archive/refs/heads/main.zip"
)
def test_resolve_docs_source_url_rejects_unknown_host():
with pytest.raises(ValueError, match="not allowed"):
DocsManager._resolve_docs_source_url("https://evil.example/docs.zip")
def test_update_from_github_downloads_and_installs(docs_manager, monkeypatch):
payload = _make_docs_zip(
files={
"reticulum_website-main/docs/index.html": "<html>github</html>",
},
)
class FakeResponse:
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def read(self):
return payload
monkeypatch.setattr(
"meshchatx.src.backend.docs_manager.urllib.request.urlopen",
lambda *_args, **_kwargs: FakeResponse(),
)
success, version = docs_manager.update_from_github(version="github-test")
assert success is True
assert version == "github-test"
assert docs_manager.upload_status == "completed"
resolved = docs_manager.find_docs_file("index.html")
assert resolved is not None
with open(resolved) as fh:
assert "github" in fh.read()