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; return;
} }
this.manualHashDraft = parsed; this.manualHashDraft = parsed;
await this.usePropagationNode(parsed);
}, },
async copyPreferredHash() { async copyPreferredHash() {
const hash = this.config.lxmf_preferred_propagation_node_destination_hash; const hash = this.config.lxmf_preferred_propagation_node_destination_hash;

View file

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

View file

@ -16,7 +16,12 @@ class Utils {
if (raw == null) { if (raw == null) {
return 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) { if (compact.length < 32) {
return null; return null;
} }

View file

@ -2493,7 +2493,7 @@
"preferred_cleared": "Voorkeurs-propagatieknooppunt gewist", "preferred_cleared": "Voorkeurs-propagatieknooppunt gewist",
"invalid_hash": "Voer een bestemmings-hash van 32 tekens in", "invalid_hash": "Voer een bestemmings-hash van 32 tekens in",
"manual_placeholder": "Plak bestemmings-hash, <hash> of lxmf://...", "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", "set_preferred": "Instellen",
"paste_hash": "Plakken", "paste_hash": "Plakken",
"clear_preferred": "Wissen", "clear_preferred": "Wissen",

View file

@ -53,7 +53,7 @@ record_hit() {
# Do not use a bare /(^|/)android(/|$)/ pattern: LXST ships # Do not use a bare /(^|/)android(/|$)/ pattern: LXST ships
# LXST/Platforms/android for mobile hosts. That path must remain allowed in # LXST/Platforms/android for mobile hosts. That path must remain allowed in
# APKs and must not false-fail Docker scans of site-packages. # 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__(/|$)' BYTECODE_DENY_RE='(^|/)__pycache__(/|$)'

View file

@ -1,7 +1,7 @@
/** /**
* Copy docs/ tree into meshchatx/src/frontend/public/meshchatx-docs/ for in-app serving. * Copy docs/ tree into meshchatx/src/frontend/public/meshchatx-docs/ for in-app serving.
* Source of truth: docs/ at repo root. * 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"); 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 destDir = path.join(root, "meshchatx", "src", "frontend", "public", "meshchatx-docs");
const COPY_EXTENSIONS = new Set([".md", ".txt", ".json"]); 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 = "") { function walkSync(dir, callback, relBase = "") {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {

View file

@ -2,7 +2,7 @@
"""Helpers for the backend module ownership contract. """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 row structure, and resolves the backtick-quoted spans in that table to
on-disk paths under meshchatx/src/backend/ or on-disk paths under meshchatx/src/backend/ or
meshchatx/src/frontend/components/. Prose cells without backtick spans meshchatx/src/frontend/components/. Prose cells without backtick spans

View file

@ -1,6 +1,6 @@
# SPDX-License-Identifier: 0BSD # 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 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 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] _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 = ( _FIXTURE = (
Path(__file__).resolve().parent / "fixtures" / "backend_module_ownership.json" 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) expected = load_ownership_fixture(_FIXTURE)
assert parsed == expected, ( assert parsed == expected, (
"Backend module ownership table drifted from the checked-in fixture. " "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 " "UPDATE_BACKEND_MODULE_OWNERSHIP=1 uv run pytest tests/backend/test_module_ownership_contract.py -k "
"backend_ownership_table_matches_fixture" "backend_ownership_table_matches_fixture"
) )

View file

@ -28,7 +28,9 @@ vi.mock("../../meshchatx/src/frontend/js/Utils", () => ({
formatTimeAgo: (d) => "1h ago", formatTimeAgo: (d) => "1h ago",
formatDestinationHash: (h) => (h && h.length >= 8 ? h.slice(0, 8) + "…" : h), formatDestinationHash: (h) => (h && h.length >= 8 ? h.slice(0, 8) + "…" : h),
parseDestinationHash: (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; return compact.length >= 32 ? compact.slice(0, 32).toLowerCase() : null;
}, },
}, },

View file

@ -24,6 +24,7 @@ describe("PropagationNodesPage", () => {
afterEach(() => { afterEach(() => {
vi.useRealTimers(); vi.useRealTimers();
vi.unstubAllGlobals();
}); });
it("finds local propagation node from list", () => { it("finds local propagation node from list", () => {
@ -253,6 +254,24 @@ describe("PropagationNodesPage", () => {
expect(ctx.manualHashDraft).toBe("a39610c89d18bb48c73e429582423c24"); 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 () => { it("clears preferred node and draft", async () => {
const ctx = { const ctx = {
manualHashDraft: "a39610c89d18bb48c73e429582423c24", manualHashDraft: "a39610c89d18bb48c73e429582423c24",