Merge branch 'dev'

This commit is contained in:
Ivan
2026-05-10 06:28:26 -05:00
13 changed files with 166 additions and 18 deletions
+6
View File
@@ -27,6 +27,8 @@ All notable changes to this project will be documented in this file.
- **NomadNet file downloads (cancel)**: Fixed `AttributeError` when cancelling a download — `RequestReceipt` has no `.cancel()`; we now cancel the underlying `Resource` if present, or mark the receipt `FAILED` and remove it from the link queue.
- **NomadNet browser (links)**: Relative `/page/` and `/file/` URLs from the Micron parser (which include backtick parameters) are now parsed correctly so they no longer show "Unsupported URL".
- **NomadNet browser (hover)**: Links with `data-destination` now show the full URL including backtick parameters in the browser hover title.
- **Docker build**: `build-frontend` stage now installs `python3` so docs generation succeeds in `node:24-alpine`.
- **Docs manager**: Markdown tables in generated documentation render with proper borders and padding.
### Added
@@ -47,6 +49,9 @@ All notable changes to this project will be documented in this file.
- **NomadNet query tests**: Frontend and backend tests for `parseNomadnetworkUrl` with query strings and `downloadNomadNetFile` data payload handling.
- **Android RNode protection**: On Android, `RNodeInterface`, `RNodeIPInterface`, and `RNodeMultiInterface` entries in the Reticulum config are automatically disabled before startup to prevent crashes from missing serial/BLE support in Chaquopy.
- **Android external storage**: On Android, MeshChatX now defaults to `getExternalFilesDir()` (user-accessible via file managers) instead of private internal storage.
- **CI (Linux packages)**: AppImage, deb, and rpm release assets are now built and tested on every push to `dev` for both x64 and arm64.
- **Docker**: Added a hardened image variant (`-hardened` suffix) with non-root user, read-only rootfs, and restricted capabilities.
- **Map**: Drag-and-drop import of GeoJSON, KML, and KMZ files directly onto the map window, with localized drop hint overlays.
### Changed
@@ -61,6 +66,7 @@ All notable changes to this project will be documented in this file.
- **Sidebar order**: Reordered sidebar so **Telephone** appears directly below **Messages** for faster access.
- **Telephone announce**: Disabled by default in `config_manager`.
- **CONTRIBUTING.md**: Updated generative AI policy to emphasize local/offline models and reference the Reticulum Zen and License.
- **Dependencies**: Migrated Python dependency management from **Poetry** to **UV** (0.11.12) across all CI scripts, Dockerfiles, and dev tooling. `poetry.lock` replaced with `uv.lock`.
## [4.6.1] - 2026-05-04
+6 -3
View File
@@ -16,17 +16,19 @@ ARG PYTHON_HASH=sha256:dd4d2bd5b53d9b25a51da13addf2be586beebd5387e289e798e4083d9
# ---- STAGE 1: Frontend Build ----
FROM --platform=linux/amd64 ${NODE_IMAGE}@${NODE_HASH} AS build-frontend
WORKDIR /src
RUN apk add --no-cache git
RUN apk add --no-cache git python3
COPY package.json pnpm-lock.yaml vite.config.js ./
COPY patches ./patches
COPY scripts/fetch-micron-wasm.mjs scripts/fetch-micron-wasm.mjs
COPY scripts/micron-wasm-resolve-bundled.mjs scripts/micron-wasm-resolve-bundled.mjs
COPY scripts/micron-parser-go-version.mjs scripts/micron-parser-go-version.mjs
COPY scripts/build/fetch_reticulum_manual.py scripts/build/fetch_reticulum_manual.py
COPY meshchatx/src/frontend ./meshchatx/src/frontend
RUN npm install -g pnpm@10.33.0 && \
pnpm config set verify-store-integrity true && \
pnpm install --frozen-lockfile && \
pnpm run build-frontend
pnpm run build-frontend && \
pnpm run build-docs
# ---- STAGE 2: Python Builder ----
@@ -45,7 +47,8 @@ ENV PATH="/opt/venv/bin:$PATH"
# Install essential runtime tools in the venv (cffi verify needs setuptools on Python 3.12+)
RUN pip install --no-cache-dir --upgrade "pip>=26.0" "setuptools" "jaraco.context>=6.1.0"
COPY pyproject.toml uv.lock README.md ./
COPY pyproject.toml uv.lock README.md CHANGELOG.md ./
COPY logo ./logo
COPY vendor ./vendor
RUN uv sync --no-group dev --no-install-project && \
rm -rf /root/.cache/pip /root/.cache/uv
+6 -3
View File
@@ -13,17 +13,19 @@ ARG PYTHON_RUNTIME_IMAGE=cgr.dev/chainguard/python:latest-dev
FROM --platform=linux/amd64 ${NODE_IMAGE} AS build-frontend
USER root
WORKDIR /src
RUN apk add --no-cache git
RUN apk add --no-cache git python3
COPY package.json pnpm-lock.yaml vite.config.js ./
COPY patches ./patches
COPY scripts/fetch-micron-wasm.mjs scripts/fetch-micron-wasm.mjs
COPY scripts/micron-wasm-resolve-bundled.mjs scripts/micron-wasm-resolve-bundled.mjs
COPY scripts/micron-parser-go-version.mjs scripts/micron-parser-go-version.mjs
COPY scripts/build/fetch_reticulum_manual.py scripts/build/fetch_reticulum_manual.py
COPY meshchatx/src/frontend ./meshchatx/src/frontend
RUN npm install -g pnpm@10.33.0 && \
pnpm config set verify-store-integrity true && \
pnpm install --frozen-lockfile && \
pnpm run build-frontend
pnpm run build-frontend && \
pnpm run build-docs
FROM ${PYTHON_BUILD_IMAGE} AS builder
USER root
@@ -37,7 +39,8 @@ ENV PATH="/opt/venv/bin:$PATH"
RUN pip install --no-cache-dir --upgrade "pip>=26.0" "setuptools" "jaraco.context>=6.1.0"
COPY pyproject.toml uv.lock README.md ./
COPY pyproject.toml uv.lock README.md CHANGELOG.md ./
COPY logo ./logo
COPY vendor ./vendor
RUN uv sync --no-group dev --no-install-project && \
rm -rf /root/.cache/pip /root/.cache/uv
@@ -856,4 +856,41 @@ iframe {
.dark :deep(.max-w-none) h4 {
color: #f4f4f5; /* zinc-100 */
}
/* Markdown table styling */
:deep(.max-w-none) table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
font-size: 0.875rem;
}
:deep(.max-w-none) th,
:deep(.max-w-none) td {
border: 1px solid #d1d5db;
padding: 0.5rem 0.75rem;
text-align: left;
}
:deep(.max-w-none) th {
background-color: #f3f4f6;
font-weight: 700;
}
:deep(.max-w-none) tr:nth-child(even) {
background-color: #f9fafb;
}
.dark :deep(.max-w-none) th,
.dark :deep(.max-w-none) td {
border-color: #3f3f46;
}
.dark :deep(.max-w-none) th {
background-color: #27272a;
}
.dark :deep(.max-w-none) tr:nth-child(even) {
background-color: #18181b;
}
</style>
@@ -149,7 +149,24 @@
/>
</div>
<div ref="mapContainer" class="absolute inset-0" :class="{ 'cursor-crosshair': isExportMode }"></div>
<div
ref="mapContainer"
class="absolute inset-0"
:class="{ 'cursor-crosshair': isExportMode }"
@dragover.prevent="onMapDragOver"
@dragleave="onMapDragLeave"
@drop.prevent="onMapDrop"
></div>
<!-- Drag-and-drop file indicator -->
<div
v-if="isMapDropTarget"
class="absolute inset-0 z-40 flex flex-col items-center justify-center bg-blue-500/20 backdrop-blur-sm border-4 border-blue-500 border-dashed m-4 rounded-2xl pointer-events-none transition-opacity"
>
<MaterialDesignIcon icon-name="map-plus" class="w-16 h-16 text-blue-600 dark:text-blue-400 mb-4" />
<h3 class="text-lg font-bold text-blue-700 dark:text-blue-300">{{ $t("map.drop_geo_files") }}</h3>
<p class="text-sm text-blue-600 dark:text-blue-400 mt-1">GeoJSON, KML, or KMZ</p>
</div>
<!-- note hover tooltip -->
<div
@@ -1147,9 +1164,9 @@ import MapExportProgressPanel from "./internal/MapExportProgressPanel.vue";
import MapLoadingOverlay from "./internal/MapLoadingOverlay.vue";
import MapVectorExchangePanel from "./internal/MapVectorExchangePanel.vue";
import { buildMeshchatMapUri, buildWebHashMapUrl } from "../../js/mapLinkUtils.js";
import { writeFeaturesToGeoJson } from "../../js/mapExchange/geoJsonCodec.js";
import { writeFeaturesToKml } from "../../js/mapExchange/kmlCodec.js";
import { writeFeaturesToKmzBlob } from "../../js/mapExchange/kmzCodec.js";
import { readGeoJsonToFeatures, writeFeaturesToGeoJson } from "../../js/mapExchange/geoJsonCodec.js";
import { readKmlToFeatures, writeFeaturesToKml } from "../../js/mapExchange/kmlCodec.js";
import { readKmzToFeatures, writeFeaturesToKmzBlob } from "../../js/mapExchange/kmzCodec.js";
import { getDrawFeatureMetadataPayload, getFeatureAnchorCoordinate } from "../../js/mapExchange/metadataUtils.js";
import { styleFromMcxProperties } from "../../js/mapExchange/styleFromProperties.js";
import { computeSegmentMetrics, buildBearingOverlayHtml, buildBearingLiveTooltipHtml } from "../../js/mapGeodesy.js";
@@ -1191,6 +1208,7 @@ export default {
hasOfflineMap: false,
metadata: null,
isUploading: false,
isMapDropTarget: false,
isSettingsOpen: false,
settingsPanelPos: null,
settingsPanelDrag: null,
@@ -4308,6 +4326,71 @@ export default {
ToastUtils.error(this.$t("map.vector_import_failed"));
},
onMapDragOver(ev) {
if (ev.dataTransfer && ev.dataTransfer.types.includes("Files")) {
this.isMapDropTarget = true;
}
},
onMapDragLeave() {
this.isMapDropTarget = false;
},
async onMapDrop(ev) {
this.isMapDropTarget = false;
const files = Array.from(ev.dataTransfer?.files || []);
if (!files.length) return;
const geoFiles = files.filter((f) => {
const name = f.name.toLowerCase();
return (
name.endsWith(".geojson") ||
name.endsWith(".json") ||
name.endsWith(".kml") ||
name.endsWith(".kmz")
);
});
if (!geoFiles.length) {
ToastUtils.warning(this.$t("map.drop_no_geo_files"));
return;
}
for (const file of geoFiles) {
try {
const name = file.name.toLowerCase();
let features = [];
if (name.endsWith(".kmz")) {
const buf = await this.readFileArrayBuffer(file);
features = await readKmzToFeatures(buf, "EPSG:3857");
} else if (name.endsWith(".kml")) {
const text = await this.readFileText(file);
features = readKmlToFeatures(text, "EPSG:3857");
} else {
const text = await this.readFileText(file);
features = readGeoJsonToFeatures(text, "EPSG:3857");
}
this.onVectorExchangeImport({ features, merge: true });
} catch (e) {
console.error("Map drop import failed:", e);
ToastUtils.error(this.$t("map.vector_import_failed") + `${file.name}`);
}
}
},
readFileText(file) {
return new Promise((resolve, reject) => {
const r = new FileReader();
r.onload = () => resolve(String(r.result || ""));
r.onerror = () => reject(new Error("read failed"));
r.readAsText(file);
});
},
readFileArrayBuffer(file) {
return new Promise((resolve, reject) => {
const r = new FileReader();
r.onload = () => resolve(/** @type {ArrayBuffer} */ (r.result));
r.onerror = () => reject(new Error("read failed"));
r.readAsArrayBuffer(file);
});
},
exportVectorGeoJson() {
if (!this.drawSource || !this.hasVectorDrawFeatures) {
return;
+3 -1
View File
@@ -1131,7 +1131,9 @@
"vector_import_ok": "Imported {count} feature(s).",
"vector_import_empty": "No features found in file.",
"vector_import_failed": "Could not read vector file.",
"vector_export_ok": "Export started."
"vector_export_ok": "Export started.",
"drop_geo_files": "Kartendatei hier ablegen",
"drop_no_geo_files": "Keine GeoJSON-, KML- oder KMZ-Dateien erkannt."
},
"interface": {
"disable": "Deaktivieren",
+3 -1
View File
@@ -1079,7 +1079,9 @@
"vector_import_ok": "Imported {count} feature(s).",
"vector_import_empty": "No features found in file.",
"vector_import_failed": "Could not read vector file.",
"vector_export_ok": "Export started."
"vector_export_ok": "Export started.",
"drop_geo_files": "Drop map file here",
"drop_no_geo_files": "No GeoJSON, KML, or KMZ files detected."
},
"interface": {
"disable": "Disable",
+3 -1
View File
@@ -1079,7 +1079,9 @@
"vector_import_ok": "Imported {count} feature(s).",
"vector_import_empty": "No features found in file.",
"vector_import_failed": "Could not read vector file.",
"vector_export_ok": "Export started."
"vector_export_ok": "Export started.",
"drop_geo_files": "Suelta el archivo del mapa aquí",
"drop_no_geo_files": "No se detectaron archivos GeoJSON, KML o KMZ."
},
"interface": {
"disable": "Inhabilitación",
+3 -1
View File
@@ -1079,7 +1079,9 @@
"vector_import_ok": "Imported {count} feature(s).",
"vector_import_empty": "No features found in file.",
"vector_import_failed": "Could not read vector file.",
"vector_export_ok": "Export started."
"vector_export_ok": "Export started.",
"drop_geo_files": "Déposez le fichier carte ici",
"drop_no_geo_files": "Aucun fichier GeoJSON, KML ou KMZ détecté."
},
"interface": {
"disable": "Désactiver",
+3 -1
View File
@@ -1131,7 +1131,9 @@
"vector_import_ok": "Imported {count} feature(s).",
"vector_import_empty": "No features found in file.",
"vector_import_failed": "Could not read vector file.",
"vector_export_ok": "Export started."
"vector_export_ok": "Export started.",
"drop_geo_files": "Rilascia il file della mappa qui",
"drop_no_geo_files": "Nessun file GeoJSON, KML o KMZ rilevato."
},
"interface": {
"disable": "Disabilita",
+3 -1
View File
@@ -1079,7 +1079,9 @@
"vector_import_ok": "Imported {count} feature(s).",
"vector_import_empty": "No features found in file.",
"vector_import_failed": "Could not read vector file.",
"vector_export_ok": "Export started."
"vector_export_ok": "Export started.",
"drop_geo_files": "Sleep kaartbestand hierheen",
"drop_no_geo_files": "Geen GeoJSON-, KML- of KMZ-bestanden gedetecteerd."
},
"interface": {
"disable": "Uitschakelen",
+3 -1
View File
@@ -1131,7 +1131,9 @@
"vector_import_ok": "Imported {count} feature(s).",
"vector_import_empty": "No features found in file.",
"vector_import_failed": "Could not read vector file.",
"vector_export_ok": "Export started."
"vector_export_ok": "Export started.",
"drop_geo_files": "Перетащите файл карты сюда",
"drop_no_geo_files": "Не обнаружено файлов GeoJSON, KML или KMZ."
},
"interface": {
"disable": "Выключить",
+3 -1
View File
@@ -1079,7 +1079,9 @@
"vector_import_ok": "Imported {count} feature(s).",
"vector_import_empty": "No features found in file.",
"vector_import_failed": "Could not read vector file.",
"vector_export_ok": "Export started."
"vector_export_ok": "Export started.",
"drop_geo_files": "将地图文件拖放到此处",
"drop_no_geo_files": "未检测到 GeoJSON、KML 或 KMZ 文件。"
},
"interface": {
"disable": "禁用",