feat: improve propagation node management with clipboard support and localization adjustments

This commit is contained in:
Ivan 2026-08-12 20:44:46 -05:00
parent 1648955cab
commit 230bed861a
No known key found for this signature in database
11 changed files with 38 additions and 10 deletions

Binary file not shown.

View file

@ -923,6 +923,7 @@ export default {
return;
}
this.manualHashDraft = parsed;
await this.usePropagationNode(parsed);
},
async copyPreferredHash() {
const hash = this.config.lxmf_preferred_propagation_node_destination_hash;

View file

@ -4337,6 +4337,7 @@ export default {
}
event.preventDefault();
this.config.lxmf_preferred_propagation_node_destination_hash = parsed;
this.savePreferredPropagationNodeHash(true);
},
async pastePreferredPropagationNodeHash() {
const result = await readTextFromClipboard();

View file

@ -16,7 +16,12 @@ class Utils {
if (raw == null) {
return null;
}
const compact = String(raw).replace(/[^0-9a-fA-F]/g, "");
let text = String(raw).trim();
if (!text) {
return null;
}
text = text.replace(/^(lxmf|lxma|lxm|rns):\/\//i, "");
const compact = text.replace(/[^0-9a-fA-F]/g, "");
if (compact.length < 32) {
return null;
}

View file

@ -2493,7 +2493,7 @@
"preferred_cleared": "Voorkeurs-propagatieknooppunt gewist",
"invalid_hash": "Voer een bestemmings-hash van 32 tekens in",
"manual_placeholder": "Plak bestemmings-hash, <hash> of lxmf://...",
"manual_hint": "Haakjes, spaties en lxmf://-links zijn in orde. Een hash uitzetten schakelt automatische selectie uit.",
"manual_hint": "Haakjes, spaties en lxmf://-links zijn in orde. Een hash instellen schakelt automatische selectie uit.",
"set_preferred": "Instellen",
"paste_hash": "Plakken",
"clear_preferred": "Wissen",

View file

@ -53,7 +53,7 @@ record_hit() {
# Do not use a bare /(^|/)android(/|$)/ pattern: LXST ships
# LXST/Platforms/android for mobile hosts. That path must remain allowed in
# APKs and must not false-fail Docker scans of site-packages.
COMMON_DENY_RE='(^|/)\.git(/|$)|(^|/)node_modules(/|$)|(^|/)\.pnpm-store(/|$)|(^|/)\.venv(/|$)|(^|/)vendor/offline(/|$)|(^|/)vendor/lxmfy/tests(/|$)|(^|/)vendor/lxmfy/docs(/|$)|(^|/)vendor/lxmfy/docker(/|$)|(^|/)vendor/rns_filesync/tests(/|$)|(^|/)vendor/rns_filesync/docs(/|$)|(^|/)vendor/rns_filesync/docker(/|$)|(^|/)vendor/rns_filesync/packaging(/|$)|(^|/)vendor/rns_filesync/sideband(/|$)|(^|/)vendor/rns_filesync/man(/|$)|(^|/)vendor/rns_filesync/scripts(/|$)|(^|/)\.github(/|$)|(^|/)docs/agents(/|$)|(^|/)screenshots(/|$)|(^|/)\.pytest_cache(/|$)|(^|/)mutants(/|$)|(^|/)coverage(/|$)'
COMMON_DENY_RE='(^|/)\.git(/|$)|(^|/)node_modules(/|$)|(^|/)\.pnpm-store(/|$)|(^|/)\.venv(/|$)|(^|/)vendor/offline(/|$)|(^|/)vendor/lxmfy/tests(/|$)|(^|/)vendor/lxmfy/docs(/|$)|(^|/)vendor/lxmfy/docker(/|$)|(^|/)vendor/rns_filesync/tests(/|$)|(^|/)vendor/rns_filesync/docs(/|$)|(^|/)vendor/rns_filesync/docker(/|$)|(^|/)vendor/rns_filesync/packaging(/|$)|(^|/)vendor/rns_filesync/sideband(/|$)|(^|/)vendor/rns_filesync/man(/|$)|(^|/)vendor/rns_filesync/scripts(/|$)|(^|/)\.github(/|$)|(^|/)\.agents(/|$)|(^|/)screenshots(/|$)|(^|/)\.pytest_cache(/|$)|(^|/)mutants(/|$)|(^|/)coverage(/|$)'
BYTECODE_DENY_RE='(^|/)__pycache__(/|$)'

View file

@ -1,7 +1,7 @@
/**
* Copy docs/ tree into meshchatx/src/frontend/public/meshchatx-docs/ for in-app serving.
* Source of truth: docs/ at repo root.
* Skips docs/agents/ (contributor/agent guidance, not end-user docs).
* Agent guidance lives under .agents/ at repo root, not under docs/.
*/
const fs = require("fs");
@ -12,7 +12,7 @@ const srcDir = path.join(root, "docs");
const destDir = path.join(root, "meshchatx", "src", "frontend", "public", "meshchatx-docs");
const COPY_EXTENSIONS = new Set([".md", ".txt", ".json"]);
const SKIP_TOP_LEVEL_DIRS = new Set(["agents"]);
const SKIP_TOP_LEVEL_DIRS = new Set();
function walkSync(dir, callback, relBase = "") {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {

View file

@ -2,7 +2,7 @@
"""Helpers for the backend module ownership contract.
Parses the "Backend" table in docs/agents/module-ownership.md into a plain
Parses the "Backend" table in .agents/module-ownership.md into a plain
row structure, and resolves the backtick-quoted spans in that table to
on-disk paths under meshchatx/src/backend/ or
meshchatx/src/frontend/components/. Prose cells without backtick spans

View file

@ -1,6 +1,6 @@
# SPDX-License-Identifier: 0BSD
"""Contract test: docs/agents/module-ownership.md Backend table vs files on disk.
"""Contract test: .agents/module-ownership.md Backend table vs files on disk.
Parses the Backend table directly from the doc so the checked-in fixture
drifts loudly (via a failing diff) whenever a row is added, removed, or
@ -28,7 +28,7 @@ from tests.backend.module_ownership_contract_helpers import (
)
_REPO_ROOT = Path(__file__).resolve().parents[2]
_DOC_PATH = _REPO_ROOT / "docs" / "agents" / "module-ownership.md"
_DOC_PATH = _REPO_ROOT / ".agents" / "module-ownership.md"
_FIXTURE = (
Path(__file__).resolve().parent / "fixtures" / "backend_module_ownership.json"
)
@ -45,7 +45,7 @@ def test_backend_ownership_table_matches_fixture():
expected = load_ownership_fixture(_FIXTURE)
assert parsed == expected, (
"Backend module ownership table drifted from the checked-in fixture. "
"If the docs/agents/module-ownership.md Backend table changed on purpose, run: "
"If the .agents/module-ownership.md Backend table changed on purpose, run: "
"UPDATE_BACKEND_MODULE_OWNERSHIP=1 uv run pytest tests/backend/test_module_ownership_contract.py -k "
"backend_ownership_table_matches_fixture"
)

View file

@ -28,7 +28,9 @@ vi.mock("../../meshchatx/src/frontend/js/Utils", () => ({
formatTimeAgo: (d) => "1h ago",
formatDestinationHash: (h) => (h && h.length >= 8 ? h.slice(0, 8) + "…" : h),
parseDestinationHash: (h) => {
const compact = String(h || "").replace(/[^0-9a-fA-F]/g, "");
let text = String(h || "").trim();
text = text.replace(/^(lxmf|lxma|lxm|rns):\/\//i, "");
const compact = text.replace(/[^0-9a-fA-F]/g, "");
return compact.length >= 32 ? compact.slice(0, 32).toLowerCase() : null;
},
},

View file

@ -24,6 +24,7 @@ describe("PropagationNodesPage", () => {
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
});
it("finds local propagation node from list", () => {
@ -253,6 +254,24 @@ describe("PropagationNodesPage", () => {
expect(ctx.manualHashDraft).toBe("a39610c89d18bb48c73e429582423c24");
});
it("pastePreferredHash sets the preferred node from clipboard text", async () => {
const ctx = {
manualHashDraft: "",
usePropagationNode: vi.fn().mockResolvedValue(true),
$t: (k) => k,
};
vi.stubGlobal("navigator", {
clipboard: {
readText: vi.fn().mockResolvedValue("<A39610C89D18BB48C73E429582423C24>"),
},
});
Object.defineProperty(window, "isSecureContext", { value: true, configurable: true });
await PropagationNodesPage.methods.pastePreferredHash.call(ctx);
expect(ctx.manualHashDraft).toBe("a39610c89d18bb48c73e429582423c24");
expect(ctx.usePropagationNode).toHaveBeenCalledWith("a39610c89d18bb48c73e429582423c24");
vi.unstubAllGlobals();
});
it("clears preferred node and draft", async () => {
const ctx = {
manualHashDraft: "a39610c89d18bb48c73e429582423c24",