From a488757f3c0ce8d138379753dab5c023f6a2ba37 Mon Sep 17 00:00:00 2001 From: Eric Moore Date: Thu, 27 Aug 2026 17:58:09 -0500 Subject: [PATCH 1/5] test(e2e): start the app, against the four nodes it claims to serve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI compiled this client, unit-tested :shared, built a jar and a wheel, and checked facts about PyPI. It never once started the app. Every defect this repo has handed the server team lived past all of that, because it only exists once the app is RUNNING: a Reset that exits instead of returning to the wizard, an element that registers a handler but not itself, an export written where no file manager can see it. This is the missing half. The SERVER side of the automation surface was already ours — TestAutomationServer on desktop, Android and iOS, and ours is ahead of CIRISAgent's (drift #19). What lived only over there was the thing that drives it: 191 lines of `curl | grep` against five of the sixteen routes. Taken over, and made a library. THE MATRIX, because "local and/or remote, carrying brains or not" is two axes and a harness that points the app at one auto-started node tests one corner: local-node the app launches it self-launch + the claim-PIN read the shipped wheel needs remote-node pre-started, CIRIS_API_URL that it does NOT start a node it was not asked for remote-agent brain folded, answering the agent gate remote-undetermined folded, NOT answering that it does not LATCH Location is not "which URL": startServer() probes first and only launches if nothing answers, so the axis is who started the node — and the remote corners assert the negative by session id, not port liveness, because they run a real node themselves as the facade's substrate and "a node is answering" is true for a blameless client. A real bare node gives NODE for free (boots in ~2s). A real agent needs an LLM bill and still cannot produce `undetermined` on demand, because that state is a race — so BrainFacade rewrites only the routes the mode gate reads and passes everything else through untouched. /state is new. Inferring node-vs-agent from which widgets are on screen asserts the LAYOUT, and passes a client that draws agent affordances against a bare node. The app publishes its own clientMode instead, and `unset` is published rather than defaulted, because undetermined must stay visible. THE FACADE PRESENTS COHERENT NODES, and that is load-bearing. Both the agent and undetermined corners first failed a BLAMELESS client: clientModeFrom demotes an answering brain to NODE when it reports itself unconfigured (CIRISAgent#1075), and `undetermined` requires !brainUnconfigured — so folding a brain onto a node whose /v1/setup/status still said setup_required was not presenting those states at all. I read the contract before believing my own red. WHAT MAKES THE GREEN WORTH ANYTHING is that it goes red on demand: a node binary that cannot start turns the local corner red (startup never leaves Startup, no claim PIN), and a brain that answers when the corner says it should not turns the latch detector red. Both run and both were checked. The scar tissue is in the code. `curl -s` on a dead server prints nothing and exits 0, so the script this replaces read a missing app as screen "" and walked on — every call here raises with route, status and body. The fixture refuses to adopt a node it did not start, because a leftover on the fixed port answers exactly like a fresh one. An explicit --node-bin that does not exist is fatal rather than falling back to PATH: there are two different programs called ciris-server here, and resolving to the wrong one runs the whole suite and reports on it as though it were right. The app is launched in its own session so its whole tree dies with the runner — a leak left an app holding 9091 that the next run then correctly refused to touch. Each of those was found by this harness's own runs, not reasoned about. ONE CORNER IS NOT COVERED AND IS NOT FAKED: local x agent. The released node binds 4242/4243 with no port override, so the facade cannot sit where a self-launched node must be. That is the downstream mobile/manual test, and run_e2e prints every case it skips rather than dropping it silently. Four corners green in ~4min; the undetermined corner takes ~145s because the client spends its full 60s retry budget on the probe, which is the behaviour under test. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01EGE52kPzjGFiPzGcs63ZC1 --- .github/workflows/build.yml | 103 ++++ client/VENDORING.md | 2 +- .../desktop/testing/TestAutomationServer.kt | 10 + .../kotlin/ai/ciris/mobile/shared/CIRISApp.kt | 13 + .../shared/testing/TestAutomationState.kt | 12 + .../mobile/shared/testing/TestServerModels.kt | 23 + testing/README.md | 128 +++++ testing/__init__.py | 0 testing/cases.py | 240 ++++++++ testing/driver.py | 221 ++++++++ testing/node_fixture.py | 353 ++++++++++++ testing/run_e2e.py | 516 ++++++++++++++++++ 12 files changed, 1620 insertions(+), 1 deletion(-) create mode 100644 testing/README.md create mode 100644 testing/__init__.py create mode 100644 testing/cases.py create mode 100644 testing/driver.py create mode 100644 testing/node_fixture.py create mode 100644 testing/run_e2e.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bada1a1..3e580ce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -193,6 +193,109 @@ jobs: if-no-files-found: warn retention-days: 7 + # ── Drive the actual app against actual nodes ─────────────────────────────── + # + # Everything above this line can pass for a client that never starts. The jobs + # compile it, unit test :shared, build a jar and a wheel, and check facts about + # PyPI -- and every defect this repo has shipped to the server team lived past + # all of that, because it only exists once the app is RUNNING and talking to a + # node. This job starts it, four times, against the four node configurations a + # universal client has to be complete against. + # + # See testing/README.md for the matrix and for the one corner it cannot cover. + e2e-desktop: + name: e2e (desktop) + needs: [gradle] + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + + - name: apt (hardened) + uses: ./.github/actions/apt + with: + # Compose Desktop needs a display. There is no X server on a runner. + # xvfb-run needs xauth; without it it fails with a bare "xauth: not found". + packages: xvfb xauth + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + - uses: actions/download-artifact@v4 + with: + name: gradle-client + path: jars + + - name: The node this client will be a client of + id: node + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + want="v$(cat VERSION)" + asset_for() { echo "ciris-server-$1-x86_64-unknown-linux-gnu.tar.gz"; } + mkdir -p node + # The pinned version first. Client and server versions are gated to + # move together, so that is the pairing this tree claims to work with. + if gh release download "$want" -R CIRISAI/CIRISServer \ + -p "$(asset_for "$want")" -D node 2>/dev/null; then + used="$want" + else + # NOT a silent fallback. The client can legitimately cut a version + # before the server tags the matching one, and testing against no + # node at all would be worse -- but which node ran is a fact the + # report has to carry, not a detail the log buries. + used=$(gh release view -R CIRISAI/CIRISServer --json tagName -q .tagName) + echo "::notice::CIRISServer $want is not released yet; the walk ran against $used" + gh release download "$used" -R CIRISAI/CIRISServer -p "$(asset_for "$used")" -D node + fi + tar xzf node/ciris-server-*.tar.gz -C node + chmod +x node/ciris-server + echo "version=$used" >> "$GITHUB_OUTPUT" + echo "Client $(cat VERSION) walking against node $used" + + - name: Walk the matrix + run: | + set -euo pipefail + jar=$(find jars -name '*.jar' -size +1M | head -1) + test -n "$jar" || { echo "::error::no uber-jar in the gradle artifact"; exit 1; } + python3 -m testing.run_e2e \ + --corner all \ + --jar "$jar" \ + --node-bin node/ciris-server \ + --report e2e-report.json \ + --workdir e2e-work + + - name: What the walk saw + if: always() + run: | + test -f e2e-report.json || { echo "no report was written"; exit 0; } + python3 - <<'EOF' + import json + r = json.load(open("e2e-report.json")) + for c in r["corners"]: + print(f"{c['corner']:24s} {c['status']:8s} {c['seconds']}s screen={c['screen']!r}") + for case in c["cases"]: + if case["status"] == "failed": + print(f" FAILED {case['name']}: {case['detail']}") + EOF + + - uses: actions/upload-artifact@v4 + if: always() + with: + # Screenshots, node logs and app logs for every corner. A failed walk + # is only useful if you can see what the app was showing. + name: e2e-evidence + path: | + e2e-report.json + e2e-work/*/app.log + e2e-work/*/node.log + e2e-work/*/failure.png + if-no-files-found: warn + retention-days: 14 + # ── Package. Runs whatever gradle managed, and says so ────────────────────── wheels: name: wheels diff --git a/client/VENDORING.md b/client/VENDORING.md index 6da0b6b..c314f71 100644 --- a/client/VENDORING.md +++ b/client/VENDORING.md @@ -40,7 +40,7 @@ source is the pair a bisect wants: The tree's current recorded state — sha256-of-sha256s over every git-tracked file under `client/` except this one: -**state digest:** `52941ae71c8cedf8b134f6464698fa82432b2e17700aff972aa02ac3a1e9030a` +**state digest:** `84344374e09b7a6014bc29f7dcc396e2beea14635bae26e3edcaf04ba3316a9e` `packaging/check_vendoring.py` asserts it on every push, and refuses any tracked file matching a §2 never-vendor class. **Any commit that touches diff --git a/client/desktopApp/src/main/kotlin/ai/ciris/desktop/testing/TestAutomationServer.kt b/client/desktopApp/src/main/kotlin/ai/ciris/desktop/testing/TestAutomationServer.kt index 02507f3..d9a34f6 100644 --- a/client/desktopApp/src/main/kotlin/ai/ciris/desktop/testing/TestAutomationServer.kt +++ b/client/desktopApp/src/main/kotlin/ai/ciris/desktop/testing/TestAutomationServer.kt @@ -224,6 +224,16 @@ class TestAutomationServer( )) } + // The app's own account of its gates -- see StateResponse. + get("/state") { + call.respond(ai.ciris.mobile.shared.testing.StateResponse( + screen = currentScreen, + testMode = true, + clientMode = ai.ciris.mobile.shared.testing.TestAutomationState.clientMode, + nodeUrl = ai.ciris.mobile.shared.testing.TestAutomationState.nodeUrl + )) + } + // Get current screen get("/screen") { call.respond(ScreenResponse(screen = currentScreen)) diff --git a/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/CIRISApp.kt b/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/CIRISApp.kt index b9b304c..88645c0 100644 --- a/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/CIRISApp.kt +++ b/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/CIRISApp.kt @@ -458,6 +458,19 @@ fun CIRISApp( TestAutomation.setCurrentScreen(currentScreen::class.simpleName ?: "unknown") } + // Publish the node-vs-agent gate to test automation. + // + // A walk-test cannot assert this from the element tree without asserting + // the layout instead of the gate -- it would pass a client that draws agent + // affordances against a bare node. `null` is published as "unset" rather + // than defaulted to NODE, because undetermined (a folded brain that is not + // answering) is a distinct state the client must retry out of, and a + // harness that cannot see it cannot catch a client that latches. + LaunchedEffect(clientMode, nodeBaseUrl) { + ai.ciris.mobile.shared.testing.TestAutomationState.clientMode = clientMode?.name ?: "unset" + ai.ciris.mobile.shared.testing.TestAutomationState.nodeUrl = nodeBaseUrl + } + // Handle system back button - navigate back to appropriate parent screen // homeTarget (the probed landing), not Screen.Interact: on the node client the landing surface is diff --git a/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/testing/TestAutomationState.kt b/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/testing/TestAutomationState.kt index fd876d7..76987c5 100644 --- a/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/testing/TestAutomationState.kt +++ b/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/testing/TestAutomationState.kt @@ -19,6 +19,18 @@ object TestAutomationState { var currentScreen: String = "unknown" var isEnabled: Boolean = false + /** + * The node-vs-agent gate, as the app has actually derived it. + * + * `"unset"` is meaningful: it is the state a folded-but-unreachable brain + * must leave the client in, pending retry. Written by `CIRISApp` wherever + * `clientMode` is assigned; read by the test server's `/state`. + */ + var clientMode: String = "unset" + + /** The node URL the app settled on -- local default, or a remote override. */ + var nodeUrl: String = "" + // Window position offset (desktop only, for converting to screen coords) var windowX: Int = 0 var windowY: Int = 0 diff --git a/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/testing/TestServerModels.kt b/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/testing/TestServerModels.kt index 7b3de11..571d032 100644 --- a/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/testing/TestServerModels.kt +++ b/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/testing/TestServerModels.kt @@ -28,6 +28,29 @@ data class TreeResponse(val screen: String, val elements: List, val @Serializable data class ScreenResponse(val screen: String) +/** + * The app's own account of the gates a walk-test needs to assert. + * + * `/screen` and `/tree` can only tell a harness what is DRAWN, and the two + * things a client of a federation node must get right are not drawings: which + * node it is talking to, and whether that node is carrying a brain. Inferring + * the mode from which widgets happen to be on screen makes the assertion a + * restatement of the layout -- it goes green for a client that renders agent + * affordances against a bare node, which is the bug. + * + * [clientMode] is `ClientMode.name`, or `"unset"` while the probe is still + * undetermined -- which is a REAL state, not a missing value: a folded brain + * that is not answering must leave the gate unset and be retried, never latched + * (CIRISServer#390). A harness has to be able to see the difference. + */ +@Serializable +data class StateResponse( + val screen: String, + val testMode: Boolean, + val clientMode: String, + val nodeUrl: String +) + @Serializable data class ClickRequest(val testTag: String) diff --git a/testing/README.md b/testing/README.md new file mode 100644 index 0000000..922a58e --- /dev/null +++ b/testing/README.md @@ -0,0 +1,128 @@ +# The walk tests + +CI compiled this client, unit-tested `:shared`, built a jar and a wheel, checked +facts about PyPI — and never once started the app. + +Every defect this repo has handed to the server team lived past all of that. A +Reset that exits instead of returning to the wizard. An Android element that +registered a click handler but not itself, so it was invisible to automation +while looking fine to a human. A debug export written where no file manager on +the platform can see it. A `PlatformLogger` that never fed `DebugLogBuffer`. +None of those are visible to a compiler, and all of them are obvious within +three seconds of the app running. + +This is the missing half: it starts the real app against real nodes and drives +it through the automation server the client already ships. + +## The matrix + +The client is meant to be complete against a node that is **local or remote**, +and that is **carrying a brain or not**. Those are independent axes resolved by +two different mechanisms, so a harness that points the app at one auto-started +node tests one corner and calls it done. + +| corner | node | brain | what only this corner sees | +|---|---|---|---| +| `local-node` | the app launches it | none | the self-launch path, and the claim-PIN read the shipped wheel depends on | +| `remote-node` | pre-started, via `CIRIS_API_URL` | none | that the client does **not** quietly start a local node it wasn't asked for | +| `remote-agent` | pre-started | folded, answering | the agent surface against a node that declares one | +| `remote-undetermined` | pre-started | folded, **not** answering | that the client does not *latch* — undetermined is a retry signal, not a verdict | + +**Location** is not "which URL". `PythonRuntime.desktop.startServer()` probes +its configured URL and only launches `ciris-server` if nothing answers, so the +axis is *who started the node* — and the remote corners assert the negative by +session id, not by port liveness (they run a real node themselves as the +facade's substrate, so "a node is answering" is true for a blameless client). + +**Brain** is `ClientMode`, derived from `/v1/system/health` (CIRISServer#390). +A real bare node gives NODE for free — the released binary boots in ~2s. A real +agent would need a brain and an LLM bill, and still could not produce +`undetermined` on demand, because that state is a race. So the brain axis is +served by `BrainFacade`: a proxy that rewrites **only** the routes the mode gate +reads, exactly as the contract documents them, and passes everything else +through to the real node untouched. Nothing the client actually calls is +stubbed. + +The facade presents *coherent* nodes, and that is load-bearing rather than +tidiness. `clientModeFrom` demotes an answering brain to NODE when the brain +reports itself unconfigured (CIRISAgent#1075), and `undetermined` requires +`!brainUnconfigured`. Both the agent and undetermined corners initially failed a +**blameless client** because the facade folded a brain onto a node whose +`/v1/setup/status` still said `setup_required: true`. An agent presents both, or +it is not an agent. + +### The corner this cannot cover + +**local × agent.** The released node binds 4242/4243 with no port override — +`ciris-server [--home ] [--key-id ]` is the entire usage — so the +facade cannot sit where a self-launched node must be. Folding a real brain onto +a local node is the downstream mobile/manual test. It is not faked here, and it +is not silently absent: `run_e2e.py` prints every case it skips. + +## Running it + +```bash +# one corner, against a node you already have +python3 -m testing.run_e2e --corner remote-agent \ + --jar client/desktopApp/build/compose/jars/CIRIS-linux-x64-*.jar + +# the whole matrix, with the node CI uses +gh release download v0.5.190 -R CIRISAI/CIRISServer \ + -p 'ciris-server-*-x86_64-unknown-linux-gnu.tar.gz' -D node +tar xzf node/ciris-server-*.tar.gz -C node && chmod +x node/ciris-server +python3 -m testing.run_e2e --corner all --node-bin node/ciris-server \ + --jar client/desktopApp/build/compose/jars/CIRIS-linux-x64-*.jar \ + --report e2e-report.json +``` + +Needs a display. With `DISPLAY` set it uses it; without one it wraps the app in +`xvfb-run`, which is how CI runs (`xvfb` **and** `xauth` — `xvfb-run` fails with +a bare "xauth: not found" otherwise). + +`--reclaim` kills a leftover **test-mode** app holding the test port. That is +safe by construction — a test-mode app is a previous run's artefact — and +nothing else on the port is ever killed; the run stops instead. + +Whole matrix: about four minutes. Three corners take ~5s each; the undetermined +corner takes ~145s because the client spends its full 60s retry budget on the +probe, which is the behaviour under test. + +## The pieces + +| file | what it is | +|---|---| +| `driver.py` | the automation-server client — `/health`, `/tree`, `/screen`, `/state`, `/click`, `/input`, `/screenshot`. stdlib only | +| `node_fixture.py` | `RealNode` (a released `ciris-server`) and `BrainFacade` (the brain axis) | +| `cases.py` | the assertions, each declaring the corners it applies to | +| `run_e2e.py` | stands up each corner, launches the app, drives it, writes the report | + +The **server** side of the automation surface was already ours +(`client/desktopApp/.../testing/TestAutomationServer.kt`, and the Android and +iOS actuals). What lived only in CIRISAgent was the thing that drives it: +`tools/test_desktop_wipe_setup.sh`, 191 lines of `curl | grep` against five of +the sixteen routes. This is that, taken over and made a library. + +`/state` is new here. Inferring node-vs-agent from which widgets are on screen +asserts the *layout* rather than the *gate*, and passes a client that renders +agent affordances against a bare node — so the app publishes its own account of +`clientMode` and the node URL it settled on, and the harness asserts that. + +## How it fails + +Loudly, with evidence. A failing case captures a screenshot, the element tree, +the app log and the node log into the report. A corner that could not be stood +up is an **error**, never a skip. + +Some of that is scar tissue from building it. `curl -s` against a dead server +prints nothing and exits 0, so the shell script this replaces read a missing app +as a screen named `""` and walked on. The fixture refuses to adopt a node it did +not start, because a leftover on the fixed port answers exactly like a fresh +one. An explicit `--node-bin` that does not exist is fatal rather than falling +back to PATH, because there are two different programs called `ciris-server` in +this ecosystem and resolving to the wrong one runs the whole suite and reports +on it as though it were right. + +Each of those was found by this harness's own mutation tests, which are the +reason to trust a green run: a node binary that cannot start turns the local +corner red, and a brain that answers when the corner says it should not turns +the latch detector red. A gate that cannot fail is not a gate. diff --git a/testing/__init__.py b/testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testing/cases.py b/testing/cases.py new file mode 100644 index 0000000..648ad9f --- /dev/null +++ b/testing/cases.py @@ -0,0 +1,240 @@ +""" +What we assert, and against which corner. + +A case is a plain function taking a [Context] and raising on failure. Each is +declared with the corners it applies to, so `run_e2e.py --corner X` runs exactly +the cases that mean something for X and skips the rest EXPLICITLY -- a skip is +printed and lands in the report, never silently dropped. + +The assertions are deliberately about things that have actually broken here: + +* `startup_completes` -- the app leaving Startup at all. Every node-facing + regression in this repo has shown up first as a client that sits on Startup + forever, and nothing in CI could see it because nothing in CI ever started + the app. + +* `elements_are_registered` -- `/tree` non-empty. CIRISClient#7 was an Android + `testableWithHandler` that wired a click handler but never called + `registerElement`, so the element was invisible to automation while looking + fine to a human. An empty tree on a populated screen is that bug. + +* `did_not_launch_a_node` / `did_launch_a_node` -- the LOCATION axis, asserted + on behaviour rather than configuration. A client told to use a remote node + that helpfully starts a local one anyway is a data-residency bug, not a + convenience. + +* `claim_pin_is_readable` -- the first-run path the desktop wheel actually + broke: the launcher starts the node and then spawns the UI, so the stdout + banner capture never runs and the PIN has to come from the node's home file. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Callable + +from .driver import DriverError, TestAutomationServer + +# The corners. See node_fixture for why local-agent is not among them. +LOCAL_NODE = "local-node" +REMOTE_NODE = "remote-node" +REMOTE_AGENT = "remote-agent" +REMOTE_UNDETERMINED = "remote-undetermined" +ALL_CORNERS = (LOCAL_NODE, REMOTE_NODE, REMOTE_AGENT, REMOTE_UNDETERMINED) + +# Where startup is allowed to land. Setup on a fresh home, Login once claimed. +TERMINAL_SCREENS = {"Login", "Setup", "Startup"} + + +@dataclass +class Context: + """Everything a case is allowed to look at.""" + + corner: str + app: TestAutomationServer + node_url: str + #: True when the harness -- not the app -- started the real node. + node_was_prestarted: bool + #: The node home, when there is one we own. + node_home: object = None + #: Set by the runner: did the APP itself spawn a node? Answered by session + #: id, not by "is the port answering" -- the remote corners run a real node + #: as the facade's substrate, so port-liveness was true for a blameless + #: client and this case failed against correct behaviour. + app_spawned_node: bool = False + notes: list[str] = field(default_factory=list) + + +CASES: list["Case"] = [] + + +@dataclass +class Case: + name: str + corners: tuple[str, ...] + fn: Callable[[Context], None] + why: str = "" + + +def case(name: str, corners: tuple[str, ...], why: str = ""): + def deco(fn: Callable[[Context], None]) -> Callable[[Context], None]: + CASES.append(Case(name=name, corners=corners, fn=fn, why=why)) + return fn + + return deco + + +# -------------------------------------------------------------------------- +# Cases +# -------------------------------------------------------------------------- + + +@case("startup_completes", ALL_CORNERS, "a client that never leaves Startup is the recurring failure") +def startup_completes(ctx: Context) -> None: + ctx.app.wait_for_server(timeout=120) + screen = ctx.app.screen() + # Startup is transient; give it room, then demand it moved. + import time + + deadline = time.monotonic() + 120 + while time.monotonic() < deadline and screen in ("", "unknown", "Startup"): + time.sleep(1.0) + screen = ctx.app.screen() + if screen in ("", "unknown", "Startup"): + raise AssertionError( + f"app never left Startup against a {ctx.corner} node " + f"(screen={screen!r}, {len(ctx.app.tree())} elements registered)" + ) + if screen not in TERMINAL_SCREENS: + ctx.notes.append(f"landed on {screen!r}, which is past the expected first stop") + ctx.notes.append(f"settled on {screen!r}") + + +@case("elements_are_registered", ALL_CORNERS, "an empty tree on a populated screen is CIRISClient#7") +def elements_are_registered(ctx: Context) -> None: + tags = ctx.app.tags() + if not tags: + raise AssertionError( + f"no elements registered with the automation server on screen " + f"{ctx.app.screen()!r} -- either the screen is genuinely empty or " + f"registerElement is not being called (CIRISClient#7)" + ) + ctx.notes.append(f"{len(tags)} elements registered") + + +@case("did_not_launch_a_node", (REMOTE_NODE, REMOTE_AGENT, REMOTE_UNDETERMINED), + "a client pointed at a remote node must not quietly start a local one") +def did_not_launch_a_node(ctx: Context) -> None: + if ctx.app_spawned_node: + raise AssertionError( + "the client spawned a ciris-server of its own while configured for " + f"the remote node at {ctx.node_url}. A client told to use someone " + "else's node must not quietly stand up a local one." + ) + ctx.notes.append("no local node was launched, as required") + + +@case("did_launch_a_node", (LOCAL_NODE,), "the self-launch path, which the wheel actually ships") +def did_launch_a_node(ctx: Context) -> None: + if ctx.node_was_prestarted: + raise AssertionError("harness bug: the local corner must not pre-start the node") + if not ctx.app_spawned_node: + raise AssertionError( + "the client never launched a node. `ciris-server` was on PATH and " + "nothing was answering, so startServer() should have spawned one." + ) + ctx.notes.append("client launched its own node") + + +@case("claim_pin_is_readable", (LOCAL_NODE,), "the first-run path the desktop wheel broke") +def claim_pin_is_readable(ctx: Context) -> None: + home = ctx.node_home + if home is None: + raise AssertionError("harness bug: no node home recorded for the local corner") + from pathlib import Path + + pin_file = Path(str(home)) / "claim_pin" + if not pin_file.exists() or not pin_file.read_text().strip(): + raise AssertionError( + f"the node wrote no claim PIN at {pin_file}. First run cannot be " + f"completed by a client that only reads the file." + ) + ctx.notes.append("claim PIN present in the node home") + + +@case("automation_surface_answers", ALL_CORNERS, "the driver's own contract with the app") +def automation_surface_answers(ctx: Context) -> None: + h = ctx.app.health() + if not h or h.get("status") != "ok": + raise AssertionError(f"/health did not report ok: {h!r}") + if h.get("testMode") is not True: + raise AssertionError( + "the app is not in test mode -- CIRIS_TEST_MODE was not honoured, " + "and every element assertion below would be vacuous" + ) + # /tree and /screen must agree about the screen, or one of them is stale. + tree_screen = None + try: + raw = ctx.app._call("GET", "/tree") + tree_screen = raw.get("screen") if isinstance(raw, dict) else None + except DriverError: + pass + if tree_screen is not None and tree_screen != ctx.app.screen(): + ctx.notes.append(f"/tree says {tree_screen!r}, /screen says {ctx.app.screen()!r}") + + +@case("mode_gate_matches_corner", ALL_CORNERS, + "the brain axis, asserted on the gate rather than on the layout") +def mode_gate_matches_corner(ctx: Context) -> None: + """ + NODE for a bare node, AGENT for one carrying a brain, and `unset` for a + brain that is folded but not answering. + + That third expectation is the point of the corner. `undetermined` is a + RETRY signal, and a client that resolves it by picking NODE looks correct + on every screen while being wrong about the one fact it exists to know. + The client retries for the startup budget (60s by default) and then leaves + the gate unset, so this waits past that budget before believing an answer. + """ + expected = { + LOCAL_NODE: "NODE", + REMOTE_NODE: "NODE", + REMOTE_AGENT: "AGENT", + REMOTE_UNDETERMINED: "unset", + }[ctx.corner] + + if ctx.corner == REMOTE_UNDETERMINED: + # Watch the WHOLE budget: latching shows up as a decided value at any + # point in it, and checking only at the end would miss a client that + # latched early and a later probe happened to unset. + import time + + deadline = time.monotonic() + 80 + while time.monotonic() < deadline: + mode = ctx.app.client_mode() + if mode != "unset": + raise AssertionError( + f"client latched clientMode={mode!r} against a brain that is " + f"folded but not answering. That is a retry signal, not a " + f"verdict -- the gate must stay unset and be re-probed." + ) + time.sleep(2.0) + ctx.notes.append("gate stayed unset for the whole retry budget, as required") + return + + try: + ctx.app.wait_for_client_mode(expected, timeout=90) + except DriverError as e: + raise AssertionError( + f"{ctx.corner} presents a node the client should read as " + f"{expected}, but {e}" + ) from None + ctx.notes.append(f"clientMode={expected} against {ctx.app.state().get('nodeUrl') or 'the default node'}") + + +def for_corner(corner: str) -> list[Case]: + return [c for c in CASES if corner in c.corners] + + +def skipped_for_corner(corner: str) -> list[Case]: + return [c for c in CASES if corner not in c.corners] diff --git a/testing/driver.py b/testing/driver.py new file mode 100644 index 0000000..6393912 --- /dev/null +++ b/testing/driver.py @@ -0,0 +1,221 @@ +""" +A driver for the client's own TestAutomationServer. + +The SERVER side of this already lives in the client and is ours: +`client/desktopApp/.../testing/TestAutomationServer.kt` (desktop) and the +`TestAutomationServer.{android,ios}.kt` actuals. What did NOT live here was +anything that DRIVES it — that was `tools/test_desktop_wipe_setup.sh` in +CIRISAgent, 191 lines of `curl | grep` against five of the sixteen routes. +This is that driver, taken over and made a library. + +Three things it does that the shell script could not: + +1. **It fails loudly.** The shell script's `get_screen()` was + `curl -s "$TEST_URL/screen"`, and `curl -s` on a dead server prints nothing + and exits 0 — so a run against an app that never started read as a screen + named "" and walked on. Every call here raises `DriverError` with the route, + the status and the body. + +2. **It waits on the right thing.** `wait_for_element` polls `/tree` for the + element, not `sleep 2` and hope. + +3. **It is stdlib-only.** No `requests`, so CI installs nothing to use it. + +Route surface is TestAutomationServer.kt's sixteen; see `TestAutomationServer` +below for the ones we bind. +""" + +from __future__ import annotations + +import json +import time +import urllib.error +import urllib.request +from dataclasses import dataclass, field +from typing import Any + + +class DriverError(RuntimeError): + """A call to the automation server failed, or the app never answered.""" + + +@dataclass +class Element: + """One registered UI element, as `/tree` reports it.""" + + test_tag: str + x: int + y: int + width: int + height: int + text: str | None = None + + @classmethod + def from_json(cls, d: dict[str, Any]) -> "Element": + return cls( + test_tag=d.get("testTag") or d.get("test_tag") or "", + x=int(d.get("x", 0)), + y=int(d.get("y", 0)), + width=int(d.get("width", 0)), + height=int(d.get("height", 0)), + text=d.get("text"), + ) + + +@dataclass +class TestAutomationServer: + """A live connection to one running client's automation server.""" + + base_url: str = "http://127.0.0.1:9091" + timeout: float = 10.0 + trace: list[str] = field(default_factory=list) + + # ---- transport ---------------------------------------------------- + + def _call(self, method: str, route: str, body: dict | None = None) -> Any: + url = f"{self.base_url}{route}" + data = json.dumps(body).encode() if body is not None else None + req = urllib.request.Request(url, data=data, method=method) + if data is not None: + req.add_header("Content-Type", "application/json") + try: + with urllib.request.urlopen(req, timeout=self.timeout) as r: + raw = r.read().decode("utf-8", "replace") + except urllib.error.HTTPError as e: + detail = e.read().decode("utf-8", "replace")[:400] + raise DriverError(f"{method} {route} -> HTTP {e.code}: {detail}") from None + except (urllib.error.URLError, TimeoutError, OSError) as e: + # This is the case `curl -s` swallowed: nothing is listening, which + # means the app is not running. It is never a passing test. + raise DriverError(f"{method} {route} -> no answer from {self.base_url}: {e}") from None + self.trace.append(f"{method} {route}") + if not raw.strip(): + return None + try: + return json.loads(raw) + except json.JSONDecodeError: + return raw + + # ---- reads -------------------------------------------------------- + + def health(self) -> dict: + return self._call("GET", "/health") or {} + + def screen(self) -> str: + """The screen the app believes it is showing.""" + r = self._call("GET", "/screen") + if isinstance(r, dict): + return str(r.get("screen") or r.get("currentScreen") or "") + return str(r or "") + + def state(self) -> dict: + """ + The app's own account of its gates: screen, test mode, clientMode, node URL. + + Served by `/state`, added for this harness. The alternative was to infer + node-vs-agent from which widgets are on screen, which asserts the layout + rather than the gate and passes a client that draws agent affordances + against a bare node. + """ + r = self._call("GET", "/state") + return r if isinstance(r, dict) else {} + + def client_mode(self) -> str: + """`NODE`, `AGENT`, or `unset` while the probe is undetermined.""" + return str(self.state().get("clientMode") or "unset") + + def wait_for_client_mode(self, expected: str, timeout: float = 90.0) -> None: + deadline = time.monotonic() + timeout + seen: list[str] = [] + while time.monotonic() < deadline: + cur = self.client_mode() + if cur == expected: + return + if not seen or seen[-1] != cur: + seen.append(cur) + time.sleep(1.0) + raise DriverError( + f"clientMode never became {expected!r} within {timeout:.0f}s " + f"(saw: {' -> '.join(seen) or ''})" + ) + + def tree(self) -> list[Element]: + r = self._call("GET", "/tree") + raw = r.get("elements", []) if isinstance(r, dict) else (r or []) + return [Element.from_json(e) for e in raw if isinstance(e, dict)] + + def element(self, test_tag: str) -> Element | None: + try: + r = self._call("GET", f"/element/{test_tag}") + except DriverError: + return None + return Element.from_json(r) if isinstance(r, dict) and r.get("testTag") else None + + def tags(self) -> set[str]: + return {e.test_tag for e in self.tree()} + + # ---- writes ------------------------------------------------------- + + def click(self, test_tag: str) -> None: + self._call("POST", "/click", {"testTag": test_tag}) + + def input(self, test_tag: str, text: str, clear_first: bool = True) -> None: + self._call("POST", "/input", {"testTag": test_tag, "text": text, "clearFirst": clear_first}) + + def navigate(self, screen: str) -> None: + self._call("POST", "/navigate", {"screen": screen}) + + def act(self, action: str, **kw: Any) -> Any: + return self._call("POST", "/act", {"action": action, **kw}) + + def screenshot(self, path: str) -> bool: + """Ask the app to raise itself and capture. Returns False if it declined.""" + try: + r = self._call("POST", "/screenshot", {"path": path}) + except DriverError: + return False + return bool(r.get("success", True)) if isinstance(r, dict) else True + + # ---- waits -------------------------------------------------------- + + def wait_for_server(self, timeout: float = 90.0) -> None: + """Block until the app's automation server answers at all.""" + deadline = time.monotonic() + timeout + last = "" + while time.monotonic() < deadline: + try: + self.health() + return + except DriverError as e: + last = str(e) + time.sleep(1.0) + raise DriverError(f"automation server never came up within {timeout:.0f}s: {last}") + + def wait_for_element(self, test_tag: str, timeout: float = 60.0) -> Element: + deadline = time.monotonic() + timeout + seen: set[str] = set() + while time.monotonic() < deadline: + for e in self.tree(): + seen.add(e.test_tag) + if e.test_tag == test_tag: + return e + time.sleep(0.5) + near = ", ".join(sorted(seen)[:25]) or "" + raise DriverError( + f"element {test_tag!r} never appeared within {timeout:.0f}s " + f"(screen={self.screen()!r}; registered: {near})" + ) + + def wait_for_screen(self, screen: str, timeout: float = 90.0) -> None: + deadline = time.monotonic() + timeout + seen: list[str] = [] + while time.monotonic() < deadline: + cur = self.screen() + if cur == screen: + return + if not seen or seen[-1] != cur: + seen.append(cur) + time.sleep(0.5) + raise DriverError( + f"screen {screen!r} never reached within {timeout:.0f}s (saw: {' -> '.join(seen) or ''})" + ) diff --git a/testing/node_fixture.py b/testing/node_fixture.py new file mode 100644 index 0000000..6ad4945 --- /dev/null +++ b/testing/node_fixture.py @@ -0,0 +1,353 @@ +""" +Nodes for the client to be a client OF. + +The client is meant to be complete against a node that is LOCAL or REMOTE, and +that is CARRYING A BRAIN or not. Those are two independent axes and the client +resolves them by two different mechanisms, so a harness that only ever points +the app at one auto-started node tests one corner of four and calls it done. + +**Location** — is the mechanism in `PythonRuntime.desktop.startServer()`. The +app probes its configured URL first and only launches `ciris-server` if nothing +answers. So the axis is not "which URL" but "who started the node": + + local nothing pre-booted, `ciris-server` on PATH -> the app launches it, + and the claim-PIN capture path runs for real + remote a node already answering, `CIRIS_API_URL` set -> the app must connect + without launching, and must never try + +**Brain** — is `ClientMode`, derived from `/v1/system/health` (CIRISServer#390): +AGENT iff the node reports a `cognitive_state`, a non-empty service map, or an +answering folded brain; NODE otherwise; and folded-but-not-answering is +UNDETERMINED — a retry signal the client must not latch. + +A real bare node gives us NODE for free: the released binary boots in ~2s and +reports `role: fabric-node`, `agent: {folded:false, reachable:false}`, no +`cognitive_state`. A real AGENT would need a brain and an LLM bill, and it +still could not produce UNDETERMINED on demand — that state is a race. So the +brain axis is served by [BrainFacade]: a proxy in front of the real node that +rewrites ONLY `/v1/system/health`, exactly as the contract documents it, and +passes every other route through to the real node untouched. The client's mode +gate reads the contract; this drives the contract; nothing is stubbed that the +client actually calls. + +ONE CORNER IS NOT REACHABLE HERE, and is not faked: local x agent. The released +node binds 4242/4243 with no port override (`ciris-server [--home ] +[--key-id ]` is the whole usage), so the facade cannot sit where a +self-launched node must be. Folding a real brain onto a local node is the +downstream mobile/manual test; see testing/README.md. +""" + +from __future__ import annotations + +import json +import os +import shutil +import socket +import subprocess +import threading +import time +import urllib.error +import urllib.request +from dataclasses import dataclass +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + +# The released node's fixed ports; see the module docstring. +NODE_LISTEN_PORT = 4242 +NODE_API_PORT = 4243 +LOCAL_NODE_URL = f"http://127.0.0.1:{NODE_API_PORT}" + +# What a folded, answering brain adds to the node's health. Every key here is +# read by `clientModeFrom`; nothing is decorative. +BRAIN_MERGE = { + "cognitive_state": "WORK", + "role": "agent", + "services": { + f"service_{i:02d}": {"healthy": True, "status": "ok"} for i in range(22) + }, + "agent": {"folded": True, "reachable": True}, +} + +# Folded but not answering: the UNDETERMINED verdict. No cognitive_state, no +# services -- the two positive signals are absent, so a client that latches +# NODE here has the bug this corner exists to catch. +UNREACHABLE_BRAIN_MERGE = { + "agent": {"folded": True, "reachable": False}, +} + +# A configured brain's setup status. +# +# NOT decoration, and not a shortcut: `clientModeFrom` demotes an answering +# brain to NODE when it reports itself unconfigured (CIRISAgent#1075), and the +# real node behind this facade is a fresh one that says `setup_required: true`. +# A facade that folds a brain onto /v1/system/health and leaves setup saying +# "not set up yet" is presenting an INCOHERENT node, and the correct client +# verdict for it is NODE -- so the agent corner would have failed a blameless +# client. An agent presents both, or it is not an agent. +CONFIGURED_SETUP = { + "setup_required": False, + "has_env_file": True, + "is_first_run": False, + "config_exists": True, +} + +# Per-corner rewrites, keyed by the route they apply to. +NODE_REWRITES: dict[str, dict] = {} +AGENT_REWRITES = { + "/v1/system/health": BRAIN_MERGE, + "/v1/setup/status": CONFIGURED_SETUP, +} +# Folded, CONFIGURED, and not answering yet. +# +# The setup rewrite is load-bearing, and leaving it out is a mistake this +# harness made and its own run caught. `undetermined` is +# +# agentFolded && !agentReachable && !brainUnconfigured && !declaredAgent +# +# so a facade that folds an unreachable brain onto a node still reporting +# `setup_required: true` is not presenting the undetermined state at all -- it +# is presenting an unconfigured brain, for which NODE is the CORRECT verdict +# (CIRISAgent#1075). The corner failed a blameless client until the node it +# presented was coherent. What this presents now is the real race: the fold +# boots the brain on a daemon thread after the node composes, so a probe can +# legitimately see folded=true/reachable=false on a fully configured home. +UNDETERMINED_REWRITES = { + "/v1/system/health": UNREACHABLE_BRAIN_MERGE, + "/v1/setup/status": CONFIGURED_SETUP, +} + + +def free_port() -> int: + with socket.socket() as s: + s.bind(("127.0.0.1", 0)) + return int(s.getsockname()[1]) + + +def _health_ok(url: str, timeout: float = 2.0) -> bool: + try: + with urllib.request.urlopen(f"{url}/v1/system/health", timeout=timeout) as r: + return r.status == 200 + except Exception: + return False + + +def wait_until_up(url: str, timeout: float = 90.0) -> None: + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if _health_ok(url): + return + time.sleep(0.5) + raise RuntimeError(f"node at {url} never became healthy within {timeout:.0f}s") + + +def wait_until_down(url: str, timeout: float = 30.0) -> None: + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if not _health_ok(url, timeout=1.0): + return + time.sleep(0.5) + raise RuntimeError(f"something is still answering at {url} after {timeout:.0f}s") + + +@dataclass +class RealNode: + """A real released `ciris-server`, run against a throwaway home.""" + + binary: str + home: Path + key_id: str = "ciris-client" + proc: subprocess.Popen | None = None + log: Path | None = None + + @property + def url(self) -> str: + return LOCAL_NODE_URL + + def start(self) -> "RealNode": + # REFUSE to adopt a node we did not start. The port is fixed at 4243 and + # a leftover node from an earlier run answers exactly like a fresh one -- + # so without this the suite silently drives a foreign node, with foreign + # state, and goes green. Found by this fixture's own smoke test. + if _health_ok(self.url, timeout=1.5): + raise RuntimeError( + f"something is ALREADY answering at {self.url}. Refusing to start, " + f"because a run against a node this fixture does not own proves " + f"nothing. Stop it first: pkill -f 'ciris-server [-]-home'" + ) + self.home.mkdir(parents=True, exist_ok=True) + self.log = self.home.parent / "node.log" + with open(self.log, "wb") as fh: + self.proc = subprocess.Popen( + [self.binary, "--home", str(self.home), "--key-id", self.key_id], + stdout=fh, + stderr=subprocess.STDOUT, + ) + try: + wait_until_up(self.url) + except RuntimeError: + raise RuntimeError( + f"node did not come up; last log lines:\n{self.tail()}" + ) from None + return self + + def tail(self, n: int = 30) -> str: + if not self.log or not self.log.exists(): + return "" + return "\n".join(self.log.read_text("utf-8", "replace").splitlines()[-n:]) + + def claim_pin(self, timeout: float = 20.0) -> str | None: + """ + The one-time claim PIN, from the node's own home. + + Bounded retry, not a single read: the node writes this file DURING boot, + and health goes green before it lands -- a single read a moment after + startup returns None for a PIN that is about to exist. This is the same + race `PythonRuntime.readLocalClaimPin` retries 20 times to close, and a + fixture that reads once reproduces the bug rather than testing around it. + """ + f = self.home / "claim_pin" + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if f.exists(): + pin = f.read_text().strip() + if pin: + return pin + time.sleep(0.5) + return None + + def stop(self) -> None: + if not self.proc: + return + self.proc.terminate() + try: + self.proc.wait(timeout=15) + except subprocess.TimeoutExpired: + self.proc.kill() + self.proc.wait(timeout=10) + self.proc = None + # The next corner binds this port; "terminate() returned" is not the + # same claim as "the listener is gone". + wait_until_down(self.url, timeout=30) + + +class _FacadeHandler(BaseHTTPRequestHandler): + """Proxy everything; rewrite the routes the mode gate reads.""" + + rewrites: dict = {} + upstream: str = LOCAL_NODE_URL + + def log_message(self, fmt: str, *args) -> None: # quiet + pass + + def _proxy(self, method: str) -> None: + length = int(self.headers.get("Content-Length") or 0) + body = self.rfile.read(length) if length else None + req = urllib.request.Request(f"{self.upstream}{self.path}", data=body, method=method) + for h in ("Authorization", "Content-Type", "Accept"): + if self.headers.get(h): + req.add_header(h, self.headers[h]) + try: + with urllib.request.urlopen(req, timeout=30) as r: + status, payload = r.status, r.read() + ctype = r.headers.get("Content-Type", "application/json") + except urllib.error.HTTPError as e: + status, payload = e.code, e.read() + ctype = e.headers.get("Content-Type", "application/json") + except Exception as e: + status, payload, ctype = 502, json.dumps({"error": str(e)}).encode(), "application/json" + + # Merged into `data`, leaving every other field the real node reported + # exactly as it reported it. Only the routes the mode gate reads. + merge = next( + (m for route, m in self.rewrites.items() if self.path.startswith(route)), None + ) + if status == 200 and merge: + try: + doc = json.loads(payload) + data = doc.get("data") + if isinstance(data, dict): + data.update(merge) + payload = json.dumps(doc).encode() + except (json.JSONDecodeError, AttributeError): + pass # not JSON we understand; pass the node's own answer through + + self.send_response(status) + self.send_header("Content-Type", ctype) + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + def do_GET(self) -> None: + self._proxy("GET") + + def do_POST(self) -> None: + self._proxy("POST") + + def do_PUT(self) -> None: + self._proxy("PUT") + + def do_DELETE(self) -> None: + self._proxy("DELETE") + + +@dataclass +class BrainFacade: + """A remote node URL that presents a chosen brain verdict, coherently.""" + + rewrites: dict + upstream: str = LOCAL_NODE_URL + port: int = 0 + _srv: ThreadingHTTPServer | None = None + _thread: threading.Thread | None = None + + @property + def url(self) -> str: + return f"http://127.0.0.1:{self.port}" + + def start(self) -> "BrainFacade": + self.port = self.port or free_port() + handler = type( + "Handler", (_FacadeHandler,), {"rewrites": self.rewrites, "upstream": self.upstream} + ) + self._srv = ThreadingHTTPServer(("127.0.0.1", self.port), handler) + self._thread = threading.Thread(target=self._srv.serve_forever, daemon=True) + self._thread.start() + wait_until_up(self.url, timeout=20) + return self + + def stop(self) -> None: + if self._srv: + self._srv.shutdown() + self._srv.server_close() + self._srv = None + + +def find_node_binary(explicit: str | None = None) -> str: + """ + The released `ciris-server`, wherever CI or a developer put it. + + An explicit choice that does not exist is an ERROR, never a fallback. There + is more than one thing called `ciris-server` in this ecosystem -- the Rust + node this client drives, and a Python console script with a different CLI -- + so silently resolving a bad `--node-bin` to whatever is on PATH runs the + whole suite against the wrong program and reports on it as if it were right. + Caught by this harness's own mutation test. + """ + for label, cand in (("--node-bin", explicit), ("CIRIS_SERVER_BIN", os.environ.get("CIRIS_SERVER_BIN"))): + if cand: + if Path(cand).is_file(): + return str(Path(cand).resolve()) + raise RuntimeError( + f"{label}={cand!r} does not exist. Refusing to fall back to " + f"PATH: the wrong `ciris-server` would run the whole suite and " + f"report on it as though it were the right one." + ) + found = shutil.which("ciris-server") + if found: + return found + raise RuntimeError( + "no `ciris-server` found. Download one from the CIRISServer releases:\n" + " gh release download -R CIRISAI/CIRISServer \\\n" + " -p 'ciris-server--x86_64-unknown-linux-gnu.tar.gz'\n" + "then set CIRIS_SERVER_BIN to the extracted binary, or put it on PATH." + ) diff --git a/testing/run_e2e.py b/testing/run_e2e.py new file mode 100644 index 0000000..add9bd8 --- /dev/null +++ b/testing/run_e2e.py @@ -0,0 +1,516 @@ +""" +The desktop end-to-end runner. + + python3 -m testing.run_e2e --corner all --jar --report out.json + +For each corner it stands up the node the corner describes, launches the real +desktop app against it under a real display, drives it through the automation +server, and writes a machine-readable report. + +WHY THIS EXISTS AT ALL: until now CIRISClient's CI compiled the client, unit +tested `:shared`, built a jar and a wheel -- and never once started the app. +Every defect that reached the server team (a Reset that exits instead of +returning to setup, an Android element that registers a handler but not itself, +a debug export written where no file manager can see it) is a defect that only +exists once the app is RUNNING. This is the missing half. + +HOW IT FAILS: loudly and with evidence. On any case failure it captures a +screenshot through `/screenshot`, dumps the element tree and the node log into +the report, and exits non-zero. A corner that could not be stood up is an +ERROR, never a skip -- the one thing a test harness must never do is decline to +run and report success. +""" + +from __future__ import annotations + +import argparse +import json +import os +import shutil +import signal +import socket +import subprocess +import sys +import tempfile +import time +import traceback +from dataclasses import asdict, dataclass, field +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from testing import cases as case_mod # noqa: E402 +from testing.cases import ( # noqa: E402 + ALL_CORNERS, + LOCAL_NODE, + REMOTE_AGENT, + REMOTE_NODE, + REMOTE_UNDETERMINED, + Context, +) +from testing.driver import DriverError, TestAutomationServer # noqa: E402 +from testing.node_fixture import ( # noqa: E402 + AGENT_REWRITES, + LOCAL_NODE_URL, + NODE_API_PORT, + NODE_REWRITES, + UNDETERMINED_REWRITES, + BrainFacade, + RealNode, + _health_ok, + find_node_binary, + free_port, + wait_until_down, +) + +TEST_PORT_DEFAULT = 9091 + + +@dataclass +class CaseResult: + name: str + status: str # passed | failed | skipped + detail: str = "" + notes: list[str] = field(default_factory=list) + seconds: float = 0.0 + + +@dataclass +class CornerResult: + corner: str + status: str # passed | failed | error + cases: list[CaseResult] = field(default_factory=list) + screen: str = "" + elements: list[str] = field(default_factory=list) + node_log_tail: str = "" + app_log_tail: str = "" + screenshot: str = "" + error: str = "" + seconds: float = 0.0 + + +def _pids_on_port(port: int) -> list[int]: + """PIDs listening on a local TCP port, via /proc -- no lsof dependency.""" + inodes = set() + for tcp in ("/proc/net/tcp", "/proc/net/tcp6"): + try: + lines = Path(tcp).read_text().splitlines()[1:] + except OSError: + continue + for line in lines: + f = line.split() + if len(f) < 10 or f[3] != "0A": # 0A = LISTEN + continue + try: + if int(f[1].split(":")[1], 16) == port: + inodes.add(f[9]) + except (ValueError, IndexError): + continue + if not inodes: + return [] + pids = [] + for entry in Path("/proc").iterdir(): + if not entry.name.isdigit(): + continue + try: + for fd in (entry / "fd").iterdir(): + target = os.readlink(fd) + if target.startswith("socket:[") and target[8:-1] in inodes: + pids.append(int(entry.name)) + break + except (OSError, PermissionError): + continue + return pids + + +def _port_busy(port: int) -> bool: + with socket.socket() as s: + s.settimeout(0.5) + return s.connect_ex(("127.0.0.1", port)) == 0 + + +def _wrap_display(cmd: list[str]) -> list[str]: + """Give the app a display. CI has no X server; xvfb-run supplies one.""" + if os.environ.get("DISPLAY"): + return cmd + xvfb = shutil.which("xvfb-run") + if not xvfb: + raise RuntimeError( + "no DISPLAY and no xvfb-run. The desktop app needs a display; " + "install xvfb (`sudo apt-get install -y xvfb`) or run under one." + ) + # -a picks a free server number; the screen must be big enough that the + # window is not clipped, or elements register at coordinates off-screen. + return [xvfb, "-a", "-s", "-screen 0 1920x1200x24"] + cmd + + + +def _ciris_server_pids() -> list[int]: + """Every live `ciris-server`, by PID. /proc only -- no psutil dependency.""" + out = [] + for entry in Path("/proc").iterdir(): + if not entry.name.isdigit(): + continue + try: + cmd = (entry / "cmdline").read_bytes().replace(b"\0", b" ").decode("utf-8", "replace") + except (OSError, PermissionError): + continue + if "ciris-server" in cmd and "--home" in cmd: + out.append(int(entry.name)) + return out + + +# The app's own record of the spawn. Polling for a live process is racy in +# exactly the case that matters: a node binary that dies on startup exists for +# less than one poll interval, so a client that DID try to launch one reads as a +# client that never tried. The log line is durable evidence of the same event. +NODE_LAUNCH_MARKER = "Started ciris-server (PID:" + + +def app_logged_a_node_launch(app_log: Path) -> bool: + if not app_log.exists(): + return False + return NODE_LAUNCH_MARKER in app_log.read_text("utf-8", "replace") + + +def app_spawned_a_node(app_proc: subprocess.Popen | None, app_log: Path | None = None) -> bool: + """ + Did the APP start a node, as opposed to one merely existing? + + "A node is answering on the local port" cannot answer this: the remote + corners deliberately run a real node as the substrate behind the facade, so + that check was true for a client that did nothing wrong. This asks the + precise question instead. The app is launched with `start_new_session=True`, + so anything it spawns -- through xvfb-run, through the JVM, at any depth -- + inherits its session id, and nothing else on the machine shares it. + """ + if app_log is not None and app_logged_a_node_launch(app_log): + return True + if not app_proc: + return False + try: + app_sid = os.getsid(app_proc.pid) + except (ProcessLookupError, PermissionError): + return False + for pid in _ciris_server_pids(): + try: + if os.getsid(pid) == app_sid: + return True + except (ProcessLookupError, PermissionError): + continue + return False + + +class Corner: + """One row of the matrix, stood up and torn down.""" + + def __init__(self, name: str, jar: Path, node_bin: str, test_port: int, workdir: Path): + self.name = name + self.jar = jar + self.node_bin = node_bin + self.test_port = test_port + self.workdir = workdir + self.node: RealNode | None = None + self.facade: BrainFacade | None = None + self.app: subprocess.Popen | None = None + self.app_log = workdir / "app.log" + self.node_home = workdir / "nodehome" + self.api_url: str | None = None + + # -- setup --------------------------------------------------------- + + def start_nodes(self) -> None: + if self.name == LOCAL_NODE: + # Nothing pre-started ON PURPOSE: the app must launch it, and that + # launch is the thing under test. + if _health_ok(LOCAL_NODE_URL, timeout=1.5): + raise RuntimeError( + f"something is already answering at {LOCAL_NODE_URL}; the " + f"local corner cannot test self-launch against it. " + f"Stop it first: pkill -f 'ciris-server [-]-home'" + ) + return + + # Remote corners need a real node behind the facade. + self.node = RealNode(self.node_bin, self.node_home).start() + rewrites = { + REMOTE_NODE: NODE_REWRITES, + REMOTE_AGENT: AGENT_REWRITES, + REMOTE_UNDETERMINED: UNDETERMINED_REWRITES, + }[self.name] + self.facade = BrainFacade(rewrites, upstream=self.node.url).start() + self.api_url = self.facade.url + + def app_env(self) -> dict[str, str]: + env = dict(os.environ) + env["CIRIS_TEST_MODE"] = "true" + env["CIRIS_TEST_PORT"] = str(self.test_port) + env["CIRIS_HOME"] = str(self.workdir / "cirishome") + env["HOME"] = str(self.workdir / "fakehome") + Path(env["CIRIS_HOME"]).mkdir(parents=True, exist_ok=True) + Path(env["HOME"]).mkdir(parents=True, exist_ok=True) + if self.api_url: + # Both spellings: CIRIS_NODE_URL is upstream's, CIRIS_API_URL is ours. + env["CIRIS_API_URL"] = self.api_url + env["CIRIS_NODE_URL"] = self.api_url + if self.name == LOCAL_NODE: + # The app finds the node by PATH lookup; give it exactly the binary + # this run downloaded, not whatever the developer has installed. + env["PATH"] = f"{Path(self.node_bin).parent}{os.pathsep}{env.get('PATH','')}" + env.pop("CIRIS_API_URL", None) + env.pop("CIRIS_NODE_URL", None) + return env + + def start_app(self) -> None: + cmd = _wrap_display(["java", "-jar", str(self.jar)]) + with open(self.app_log, "wb") as fh: + # Its own session, so we can signal the WHOLE tree. Under xvfb-run + # the JVM is a grandchild; terminating the wrapper alone leaves it + # holding the test port, and the next run then (correctly) refuses + # to start against an app it does not own. + self.app = subprocess.Popen( + cmd, env=self.app_env(), stdout=fh, stderr=subprocess.STDOUT, + start_new_session=True, + ) + + # -- teardown ------------------------------------------------------ + + def stop(self) -> None: + if self.app: + self._signal_app_group(signal.SIGTERM) + try: + self.app.wait(timeout=20) + except subprocess.TimeoutExpired: + self._signal_app_group(signal.SIGKILL) + self.app.wait(timeout=10) + self.app = None + # Prove the port is free. "I sent SIGTERM" is not that claim, and + # the next corner binds this port. + deadline = time.monotonic() + 30 + while time.monotonic() < deadline and _port_busy(self.test_port): + time.sleep(0.5) + if self.facade: + self.facade.stop() + self.facade = None + if self.node: + self.node.stop() + self.node = None + # The local corner's node was started by the APP; it is our job to see + # it gone before the next corner claims the port. + if _health_ok(LOCAL_NODE_URL, timeout=1.5): + # The bracket keeps the pattern from matching the shell that runs + # it -- `pkill -f 'ciris-server --home'` matches its own command + # line and kills the caller. Cost me one silent exit-144 run. + subprocess.run(["pkill", "-f", "ciris-server [-]-home"], check=False) + try: + wait_until_down(LOCAL_NODE_URL, timeout=30) + except RuntimeError: + pass + + def _discover_node_home(self) -> Path | None: + """Where the APP told its node to live -- read off the node's cmdline.""" + for pid in _ciris_server_pids(): + try: + if os.getsid(pid) != os.getsid(self.app.pid): # type: ignore[union-attr] + continue + parts = (Path("/proc") / str(pid) / "cmdline").read_bytes().split(b"\0") + args = [a.decode("utf-8", "replace") for a in parts if a] + if "--home" in args: + return Path(args[args.index("--home") + 1]) + except (OSError, PermissionError, ProcessLookupError, IndexError, AttributeError): + continue + return None + + def _signal_app_group(self, sig: int) -> None: + if not self.app: + return + try: + os.killpg(os.getpgid(self.app.pid), sig) + except (ProcessLookupError, PermissionError): + try: + self.app.send_signal(sig) + except ProcessLookupError: + pass + + def tail(self, p: Path, n: int = 40) -> str: + if not p.exists(): + return "" + return "\n".join(p.read_text("utf-8", "replace").splitlines()[-n:]) + + # -- run ----------------------------------------------------------- + + def run(self) -> CornerResult: + started = time.monotonic() + res = CornerResult(corner=self.name, status="passed") + self.workdir.mkdir(parents=True, exist_ok=True) + try: + self.start_nodes() + self.start_app() + app = TestAutomationServer(f"http://127.0.0.1:{self.test_port}") + try: + app.wait_for_server(timeout=180) + except DriverError as e: + raise RuntimeError( + f"the app never exposed its automation server. " + f"{e}\n--- app log ---\n{self.tail(self.app_log)}" + ) from None + + ctx = Context( + corner=self.name, + app=app, + node_url=self.api_url or LOCAL_NODE_URL, + node_was_prestarted=self.node is not None, + node_home=self.node_home if self.name == LOCAL_NODE else None, + ) + if self.name == LOCAL_NODE: + # The app launches its node lazily; give it the same window the + # client's own startServer() health loop uses before deciding + # it never happened. + deadline = time.monotonic() + 90 + while time.monotonic() < deadline and not app_spawned_a_node(self.app, self.app_log): + time.sleep(1.0) + # It writes its home under the app's CIRIS_HOME, not ours. + ctx.node_home = self._discover_node_home() or ctx.node_home + + for c in case_mod.for_corner(self.name): + t0 = time.monotonic() + # Re-read each time: the local corner's answer changes as the + # app gets around to launching its node. + ctx.app_spawned_node = app_spawned_a_node(self.app, self.app_log) + before = list(ctx.notes) + try: + c.fn(ctx) + res.cases.append( + CaseResult(c.name, "passed", + notes=[n for n in ctx.notes if n not in before], + seconds=round(time.monotonic() - t0, 2)) + ) + except Exception as e: + res.status = "failed" + res.cases.append( + CaseResult(c.name, "failed", detail=str(e), + notes=[n for n in ctx.notes if n not in before], + seconds=round(time.monotonic() - t0, 2)) + ) + + for c in case_mod.skipped_for_corner(self.name): + res.cases.append(CaseResult(c.name, "skipped", detail=f"not applicable to {self.name}")) + + res.screen = app.screen() + res.elements = sorted(app.tags()) + if res.status == "failed": + shot = self.workdir / "failure.png" + if app.screenshot(str(shot)) and shot.exists(): + res.screenshot = str(shot) + except Exception as e: + res.status = "error" + res.error = f"{e}\n{traceback.format_exc(limit=3)}" + finally: + if self.node: + res.node_log_tail = self.node.tail() + res.app_log_tail = self.tail(self.app_log) + self.stop() + res.seconds = round(time.monotonic() - started, 1) + return res + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--corner", default="all", + help=f"one of {', '.join(ALL_CORNERS)}, or 'all'") + ap.add_argument("--jar", required=True, help="the desktop uber-jar") + ap.add_argument("--node-bin", default=None, help="released ciris-server binary") + ap.add_argument("--test-port", type=int, default=TEST_PORT_DEFAULT) + ap.add_argument("--report", default=None, help="write a JSON report here") + ap.add_argument("--workdir", default=None) + ap.add_argument("--reclaim", action="store_true", + help="kill a leftover TEST-MODE app holding the test port " + "(safe: a test-mode app is a previous run's artefact)") + args = ap.parse_args() + + jar = Path(args.jar).resolve() + if not jar.is_file(): + print(f"no such jar: {jar}", file=sys.stderr) + return 2 + node_bin = find_node_binary(args.node_bin) + corners = list(ALL_CORNERS) if args.corner == "all" else [args.corner] + unknown = [c for c in corners if c not in ALL_CORNERS] + if unknown: + print(f"unknown corner(s): {', '.join(unknown)}", file=sys.stderr) + return 2 + + root = Path(args.workdir) if args.workdir else Path(tempfile.mkdtemp(prefix="ciris-e2e-")) + print(f"jar : {jar}") + print(f"node : {node_bin}") + print(f"workdir : {root}") + print(f"corners : {', '.join(corners)}\n") + + if _port_busy(args.test_port) and args.reclaim: + # Only ever a TEST-MODE CIRIS app: that is a disposable artefact of a + # previous run by construction, and killing it is safe. Anything else + # on the port still stops the run. + probe = TestAutomationServer(f"http://127.0.0.1:{args.test_port}") + try: + if probe.health().get("testMode") is True: + for pid in _pids_on_port(args.test_port): + try: + os.killpg(os.getpgid(pid), signal.SIGKILL) + except (ProcessLookupError, PermissionError): + pass + deadline = time.monotonic() + 20 + while time.monotonic() < deadline and _port_busy(args.test_port): + time.sleep(0.5) + print(f"reclaimed port {args.test_port} from a leftover test-mode app") + except DriverError: + pass + + if _port_busy(args.test_port): + who = "" + try: + who = f" It answers /screen with {TestAutomationServer(f'http://127.0.0.1:{args.test_port}').screen()!r}, so it is a CIRIS app -- most likely one a previous run left behind." + except DriverError: + who = " It is not answering as a CIRIS automation server." + print( + f"port {args.test_port} is busy; the driver would drive the wrong app.{who}\n" + f"Either stop it, or pass --test-port with a free one.", + file=sys.stderr, + ) + return 2 + + results: list[CornerResult] = [] + for name in corners: + print(f"=== {name} ".ljust(70, "=") + "\n") + r = Corner(name, jar, node_bin, args.test_port, root / name).run() + results.append(r) + for c in r.cases: + if c.status == "skipped": + continue + mark = "PASS" if c.status == "passed" else "FAIL" + print(f" [{mark}] {c.name} ({c.seconds}s)") + for n in c.notes: + print(f" - {n}") + if c.detail and c.status == "failed": + print(f" ! {c.detail}") + n_skip = sum(1 for c in r.cases if c.status == "skipped") + if n_skip: + print(f" ({n_skip} case(s) not applicable to this corner)") + if r.status == "error": + print(f" [ERROR] {r.error.splitlines()[0] if r.error else '?'}") + print(f" --- app log ---\n{r.app_log_tail}") + print(f" -> {r.status.upper()} in {r.seconds}s\n") + + if args.report: + Path(args.report).write_text(json.dumps( + {"corners": [asdict(r) for r in results]}, indent=2)) + print(f"report: {args.report}") + + bad = [r for r in results if r.status != "passed"] + print("\n" + "=" * 70) + for r in results: + print(f" {r.corner:24s} {r.status}") + print("=" * 70) + return 1 if bad else 0 + + +if __name__ == "__main__": + sys.exit(main()) From 6cab1c5eebe1020918b589a900f448d169ef2a84 Mon Sep 17 00:00:00 2001 From: Eric Moore Date: Thu, 27 Aug 2026 18:42:00 -0500 Subject: [PATCH 2/5] =?UTF-8?q?feat(claim):=20remote=20first-run=20claim?= =?UTF-8?q?=20=E2=80=94=20the=20FSD,=20and=20the=20half=20that=20is=20buil?= =?UTF-8?q?dable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There are research agents running fully configured and unclaimed, and no clean route to claiming them from this client. This is the specification for closing that, plus the client half of it. It also carries the six Codex findings on the walk tests (PR #10). WHAT THE INVESTIGATION FOUND, AND IT CHANGES THE DESIGN. The setup surface is split by a loopback guard, and only ONE route is reachable off-host: POST /v1/setup/root reachable remotely GET /v1/setup/status loopback only GET /v1/setup/owned-nodes loopback only GET /v1/setup/consent-disclosure loopback only MEASURED, not read off the source: the released 0.5.190 binary, queried over loopback and over the host's own LAN address. The reads answer 200 to 127.0.0.1 and 403 "setup routes are localhost-only" to the LAN. `/v1/setup/root` answers 405 from the LAN rather than 403 — and that is the useful row, because a loopback-layered route rejects before it ever considers the method. A 405 off-host is positive proof the claim route sits outside the guard. So the two cases the operator has are not one problem: A configured, unclaimed -> buildable now; the local node signs and delivers B bare, unconfigured -> BLOCKED. The wizard reads /v1/setup/status and /v1/setup/consent-disclosure, both loopback-only. No client work reaches it; it needs a CIRISServer decision, and the FSD states the ask. Case A is implemented here: FOUR OUTCOMES, NOT TWO. A wrong PIN, an already-owned node, an unreachable target and a malformed NodeCode need different next actions, and all four rendered as one string — "Claim failed:" plus whatever the substrate said. An operator claiming a fleet could not tell "I mistyped eight characters" from "this one already has an owner", which are opposite situations: one is a retry, the other is a success that already happened. ClaimFailure classifies them. ON THE SERVER'S CODES, NOT ITS PROSE. CIRISServer emits auth.claim.pin_invalid, auth.claim.pin_missing and auth.claim.not_armed. The previous match was English substrings ("claim pin", "invalid pin"), which break the moment the server rewords — and rewording an error message is not a breaking change anybody announces. Prose patterns are kept below the codes, for older nodes, which is the only thing they are fit for. The verbatim message is kept ALONGSIDE the classification and never replaced by it: UNKNOWN exists so an unrecognised refusal reaches the operator intact instead of being flattened into a guess. MY OWN TEST CAUGHT THE ORDERING. The not_armed sentence contains the words "one-time PIN", so a PIN-first order reads an already-owned node as a PIN problem and sends the operator back to a console for a PIN that was never minted. "not armed" is now tested first, and that case is a test. THE PRECONDITION IS ASKED BEFORE THE SECRETS. The claim is signed by the operator's own node — the app does no crypto — so it needs that node up. That is knowable on entry, and the screen now says so instead of collecting a NodeCode and a one-time PIN and failing at the POST, after a wasted trip to the target's console. Desktop, Android and iOS all ship a local node, so a signer normally exists; web has no local runtime and cannot claim at all. Codex on PR #10, all six: - the folded-agent facade no longer overwrites role. A node's merged health keeps role="fabric-node" while a brain answers, and clientModeFrom treats the agent role as CONCLUSIVE — so that one field held the agent corner green while the client could have stopped reading cognitive_state, the service map and agent.reachable entirely. - the bare-node corner now reports configured setup. brainUnconfigured is the FIRST arm of clientModeFrom, an unconditional NODE that never reaches the role, cognitive_state or service checks — so the corner was classifying correctly for a reason that bypassed classification. - the node binary is on PATH in EVERY corner. Keeping it out of the remote ones made did_not_launch_a_node vacuous on a runner, where there is no ciris-server to find: the regression is a client that launches a local node whenever it CAN, and it had no way to exhibit it. - a node that starts but never goes healthy is killed before the raise. It was left holding the fixed port, and since start() never returned the caller never assigned it, so nothing could clean it up. - which node the walk ran against is in the report. The client can cut a version before the server tags the matching one, and evidence that cannot name its pairing cannot settle a compatibility question later. - the claim-PIN case is REMOVED rather than fixed. It read the node's own claim_pin file, which proves the server emitted a PIN and says nothing about the client. The instinct was to instrument the client's reader; the PIN is not ours to instrument — the node handles it as part of the complete call, and the only client interest is whether this is a local first run, which the node's own first-run signals already answer. AND A LIMITATION OF MY OWN HARNESS, STATED: the "remote" corners put the facade on 127.0.0.1, so the node sees a LOOPBACK peer and the loopback-only routes answer normally. They exercise the client's remote configuration path but not remote reachability — a genuinely off-host client meets 403s the harness never produces. Nothing in testing/ is evidence about off-host behaviour until a corner makes the node observe a non-loopback source. mobile.claim_node_no_signer is English-only here; the translate lane fills the other 28 in CI, where the key lives. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01EGE52kPzjGFiPzGcs63ZC1 --- .github/workflows/build.yml | 1 + client/VENDORING.md | 2 +- .../src/main/assets/localization/en.json | 3 +- .../src/main/resources/localization/en.json | 3 +- client/iosApp/iosApp/localization/en.json | 3 +- .../mobile/shared/models/ClaimFailure.kt | 95 +++++++ .../shared/ui/screens/ClaimNodeScreen.kt | 30 +- .../viewmodels/NodeSwitcherViewModel.kt | 79 +++++- .../mobile/shared/models/ClaimFailureTest.kt | 92 +++++++ .../resources/localization/en.json | 3 +- docs/FSD-remote-first-run-claim.md | 259 ++++++++++++++++++ testing/README.md | 14 + testing/cases.py | 16 -- testing/node_fixture.py | 44 ++- testing/run_e2e.py | 44 ++- 15 files changed, 647 insertions(+), 41 deletions(-) create mode 100644 client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/models/ClaimFailure.kt create mode 100644 client/shared/src/commonTest/kotlin/ai/ciris/mobile/shared/models/ClaimFailureTest.kt create mode 100644 docs/FSD-remote-first-run-claim.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3e580ce..ec897f9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -265,6 +265,7 @@ jobs: --corner all \ --jar "$jar" \ --node-bin node/ciris-server \ + --node-version "${{ steps.node.outputs.version }}" \ --report e2e-report.json \ --workdir e2e-work diff --git a/client/VENDORING.md b/client/VENDORING.md index c314f71..c483288 100644 --- a/client/VENDORING.md +++ b/client/VENDORING.md @@ -40,7 +40,7 @@ source is the pair a bisect wants: The tree's current recorded state — sha256-of-sha256s over every git-tracked file under `client/` except this one: -**state digest:** `84344374e09b7a6014bc29f7dcc396e2beea14635bae26e3edcaf04ba3316a9e` +**state digest:** `30c55d9c2263a2fc12b0897d98e3d9f4c785119648ac7a90b2b9b1e297562fbb` `packaging/check_vendoring.py` asserts it on every push, and refuses any tracked file matching a §2 never-vendor class. **Any commit that touches diff --git a/client/androidApp/src/main/assets/localization/en.json b/client/androidApp/src/main/assets/localization/en.json index 8561878..931aee0 100644 --- a/client/androidApp/src/main/assets/localization/en.json +++ b/client/androidApp/src/main/assets/localization/en.json @@ -2993,7 +2993,8 @@ "wallet_transfer_note": "Transfer to another Base address for fiat cashout", "wallet_transfer_success": "Transfer successful! TX: {tx}", "wallet_trust_degraded": "Hardware Trust Degraded", - "wallet_warning": "Warning" + "wallet_warning": "Warning", + "claim_node_no_signer": "Claiming binds your identity, and that signature comes from your own node — which is not running. Start your node, then claim." }, "moderation": { "ladder": { diff --git a/client/desktopApp/src/main/resources/localization/en.json b/client/desktopApp/src/main/resources/localization/en.json index 8561878..931aee0 100644 --- a/client/desktopApp/src/main/resources/localization/en.json +++ b/client/desktopApp/src/main/resources/localization/en.json @@ -2993,7 +2993,8 @@ "wallet_transfer_note": "Transfer to another Base address for fiat cashout", "wallet_transfer_success": "Transfer successful! TX: {tx}", "wallet_trust_degraded": "Hardware Trust Degraded", - "wallet_warning": "Warning" + "wallet_warning": "Warning", + "claim_node_no_signer": "Claiming binds your identity, and that signature comes from your own node — which is not running. Start your node, then claim." }, "moderation": { "ladder": { diff --git a/client/iosApp/iosApp/localization/en.json b/client/iosApp/iosApp/localization/en.json index 8561878..931aee0 100644 --- a/client/iosApp/iosApp/localization/en.json +++ b/client/iosApp/iosApp/localization/en.json @@ -2993,7 +2993,8 @@ "wallet_transfer_note": "Transfer to another Base address for fiat cashout", "wallet_transfer_success": "Transfer successful! TX: {tx}", "wallet_trust_degraded": "Hardware Trust Degraded", - "wallet_warning": "Warning" + "wallet_warning": "Warning", + "claim_node_no_signer": "Claiming binds your identity, and that signature comes from your own node — which is not running. Start your node, then claim." }, "moderation": { "ladder": { diff --git a/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/models/ClaimFailure.kt b/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/models/ClaimFailure.kt new file mode 100644 index 0000000..6fd15a7 --- /dev/null +++ b/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/models/ClaimFailure.kt @@ -0,0 +1,95 @@ +package ai.ciris.mobile.shared.models + +/** + * WHY A CLAIM FAILED — as a fact, not a sentence. + * + * Claiming a remote node is the act that binds a responsible party to it, and + * it fails in ways that need DIFFERENT NEXT ACTIONS from the operator: + * + * - the PIN was wrong -> re-read it from the node's console + * - the node is already claimed -> nothing to do; it has an owner + * - the node could not be reached -> a network/address problem, not a secret + * - the NodeCode itself is malformed -> re-scan or re-paste the code + * + * Before this, all four rendered as one string ("Claim failed: …" with the + * node's raw message appended, or a PIN message matched on English prose), so + * an operator claiming a fleet of research agents could not tell "I mistyped + * eight characters" from "this one already has an owner" without reading a + * substrate error. Those are opposite situations: one is a retry, the other is + * a success that already happened. + * + * MATCHED ON THE SERVER'S STABLE CODES, NOT ITS PROSE. CIRISServer emits + * `auth.claim.pin_invalid`, `auth.claim.pin_missing` and `auth.claim.not_armed` + * (`src/auth/bootstrap.rs`), and those are wire-stable in a way the English + * sentences beside them are not — the previous prose match (`"claim pin"`, + * `"invalid pin"`) breaks the moment the server rewords, and rewording an error + * message is not a breaking change anybody would announce. The prose patterns + * are kept BELOW the codes as a fallback for older nodes, which is the only + * thing they are fit for. + */ +enum class ClaimFailure { + /** The one-time PIN was wrong, or was not supplied. */ + PIN_REJECTED, + + /** + * The node is not armed for a first-run claim — it has no one-time PIN + * because ownership is already established. Not a retryable error. + */ + ALREADY_CLAIMED, + + /** The target could not be reached: no transport hint, or the connection failed. */ + UNREACHABLE, + + /** The NodeCode could not be decoded. */ + BAD_NODE_CODE, + + /** Anything else — surfaced verbatim rather than guessed at. */ + UNKNOWN; + + /** Re-entering the PIN or retrying can plausibly succeed. */ + val isRetryable: Boolean + get() = this == PIN_REJECTED || this == UNREACHABLE || this == BAD_NODE_CODE +} + +/** + * Classify a claim failure from whatever the node said. + * + * Order matters: the codes are checked first and win outright. A message can + * satisfy more than one prose pattern (a target rejection body carries both the + * word "claim" and an HTTP status), so the fallbacks are ordered most- to + * least-specific and the first match is taken. + */ +fun classifyClaimFailure(message: String?): ClaimFailure { + val m = message?.lowercase().orEmpty() + if (m.isBlank()) return ClaimFailure.UNKNOWN + + // 1) The server's own codes. Wire-stable; these are the answer when present. + if (m.contains("auth.claim.pin_invalid") || m.contains("auth.claim.pin_missing")) { + return ClaimFailure.PIN_REJECTED + } + if (m.contains("auth.claim.not_armed")) return ClaimFailure.ALREADY_CLAIMED + + // 2) Prose, for nodes older than those codes. Never the primary signal. + // "not armed for a first-run claim" is the distinguishing phrase, and it + // must be tested BEFORE the PIN patterns: the sentence carrying it also + // contains the words "one-time PIN", so a PIN-first order sends the operator + // to the console to re-read a PIN that was never minted, on a node that + // already has an owner. Caught by this class's own test. + if (m.contains("not armed") || m.contains("already claimed") || + m.contains("ownership may already be claimed") + ) { + return ClaimFailure.ALREADY_CLAIMED + } + if (m.contains("claim_pin") || m.contains("claim pin") || m.contains("invalid pin")) { + return ClaimFailure.PIN_REJECTED + } + if (m.contains("no transport_hint") || m.contains("cannot reach the node") || + m.contains("reach target node") + ) { + return ClaimFailure.UNREACHABLE + } + if (m.contains("decode target nodecode") || m.contains("bad node code")) { + return ClaimFailure.BAD_NODE_CODE + } + return ClaimFailure.UNKNOWN +} diff --git a/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/ui/screens/ClaimNodeScreen.kt b/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/ui/screens/ClaimNodeScreen.kt index cdbb995..347d6d7 100644 --- a/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/ui/screens/ClaimNodeScreen.kt +++ b/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/ui/screens/ClaimNodeScreen.kt @@ -27,6 +27,8 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold @@ -83,6 +85,14 @@ fun ClaimNodeScreen( // Default to "self" but make the founder pick before claiming. var cohortScope by remember { mutableStateOf("self") } + // ASK THE PRECONDITION FIRST. The claim is signed by this device's OWN node + // (the app does no crypto), so without it running there is nothing to type a + // NodeCode and a one-time PIN into. Checking on entry means the operator + // learns that before making a trip to the target node's console, rather than + // after -- see docs/FSD-remote-first-run-claim.md A2. + androidx.compose.runtime.LaunchedEffect(Unit) { viewModel.checkLocalSigner() } + val signerMissing = bootstrap.localSignerReady == false + // While the connect-or-claim pipeline is running we disable the button. val inFlight = bootstrap.inProgress || bootstrap.claimInProgress val pinned = bootstrap.pinnedProfile @@ -128,6 +138,23 @@ fun ClaimNodeScreen( color = MaterialTheme.colorScheme.onSurfaceVariant, ) + if (signerMissing) { + Spacer(Modifier.height(16.dp)) + Card( + modifier = Modifier.fillMaxWidth().testable("card_claim_no_signer"), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + ), + ) { + Text( + text = localizedString("mobile.claim_node_no_signer"), + modifier = Modifier.padding(14.dp), + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onErrorContainer, + ) + } + } + Spacer(Modifier.height(20.dp)) // ── NodeCode field (paste / QR forms; dashed CIRIS-V1-… accepted) ── @@ -195,7 +222,8 @@ fun ClaimNodeScreen( // resulting PINNED profile below and chain the claim then. viewModel.connectByNodeCode(codeInput.trim()) }, - enabled = !inFlight && codeInput.isNotBlank() && pinInput.isNotBlank() && !claimed, + enabled = !inFlight && !signerMissing && + codeInput.isNotBlank() && pinInput.isNotBlank() && !claimed, modifier = Modifier .fillMaxWidth() .height(48.dp) diff --git a/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/viewmodels/NodeSwitcherViewModel.kt b/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/viewmodels/NodeSwitcherViewModel.kt index 5dc8ec1..111d06c 100644 --- a/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/viewmodels/NodeSwitcherViewModel.kt +++ b/client/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/viewmodels/NodeSwitcherViewModel.kt @@ -763,7 +763,13 @@ class NodeSwitcherViewModel( ) return } - _bootstrap.value = _bootstrap.value.copy(claimInProgress = true, claimError = null, claimedRole = null) + _bootstrap.value = _bootstrap.value.copy( + claimInProgress = true, + claimError = null, + claimFailure = null, + claimErrorDetail = null, + claimedRole = null, + ) viewModelScope.launch { try { // Drive the LOCAL node to claim the target. The local node does ALL @@ -788,22 +794,54 @@ class NodeSwitcherViewModel( // PIN. The local node (or, via it, the target) returns 4xx with a // body that mentions the pin (e.g. "invalid_claim_pin" / "claim // pin"); claimRemote re-throws it. - val msg = e.message.orEmpty() - val isPinRejection = msg.contains("claim_pin", ignoreCase = true) || - msg.contains("claim pin", ignoreCase = true) || - msg.contains("invalid pin", ignoreCase = true) + // FOUR OUTCOMES, NOT TWO. See [ClaimFailure]: a wrong PIN, an + // already-owned node, an unreachable target and a malformed + // NodeCode need different next actions, and the previous + // two-way split (PIN prose, else the raw message) made the + // three non-PIN cases indistinguishable to an operator + // claiming a fleet. Classified on the server's stable codes. + val failure = ai.ciris.mobile.shared.models.classifyClaimFailure(e.message) _bootstrap.value = _bootstrap.value.copy( claimInProgress = false, - claimError = if (isPinRejection) { - "The node rejected the PIN — check the one-time PIN on the node's console and try again." - } else { - "Claim failed: ${e.message}" + claimFailure = failure, + // The verbatim message is KEPT alongside the classification, + // never replaced by it: UNKNOWN exists precisely so an + // unrecognised refusal reaches the operator intact rather + // than being flattened into a guess. + claimErrorDetail = e.message, + claimError = when (failure) { + ai.ciris.mobile.shared.models.ClaimFailure.PIN_REJECTED -> + "The node rejected the PIN — check the one-time PIN on the node's console and try again." + ai.ciris.mobile.shared.models.ClaimFailure.ALREADY_CLAIMED -> + "This node already has an owner — there is nothing to claim. It prints a one-time PIN only while unclaimed." + ai.ciris.mobile.shared.models.ClaimFailure.UNREACHABLE -> + "Could not reach that node to claim it — check the address in its NodeCode and that the node is running." + ai.ciris.mobile.shared.models.ClaimFailure.BAD_NODE_CODE -> + "That NodeCode could not be read — re-scan or re-paste the full CIRIS-V1- code." + ai.ciris.mobile.shared.models.ClaimFailure.UNKNOWN -> + "Claim failed: ${e.message}" }, ) } } } + /** + * Is the local node -- the thing that SIGNS a claim -- reachable? + * + * Desktop, Android and iOS all ship a local node, so this is normally true; + * it is false when that node is not running yet, and on the web build, + * which has no local runtime at all and therefore cannot claim anything. + */ + fun checkLocalSigner() { + viewModelScope.launch { + val ready = runCatching { + apiClient.isLocalNodeUp(ai.ciris.mobile.shared.api.CIRISApiClient.LOCAL_NODE_URL) + }.getOrDefault(false) + PlatformLogger.i(TAG, "[claimAdmin] local signer ready=$ready") + _bootstrap.value = _bootstrap.value.copy(localSignerReady = ready) + } + } } /** Phase of the NodeCode bootstrap, for driving the connect/pin/claim UI. */ @@ -826,6 +864,29 @@ data class NodeBootstrapState( /** Non-null on a successful claim (e.g. "SYSTEM_ADMIN"). */ val claimedRole: String? = null, val claimError: String? = null, + /** + * WHY the claim failed, as a fact the UI can branch on -- see [ClaimFailure]. + * `null` when no claim has failed. + */ + val claimFailure: ai.ciris.mobile.shared.models.ClaimFailure? = null, + /** + * The node's own message, kept verbatim beside the classification. The + * classification is for deciding what to OFFER; this is what actually + * happened, and an unrecognised refusal must still reach the operator. + */ + val claimErrorDetail: String? = null, + /** + * Can this device sign a claim at all? + * + * The owner-binding is built and hybrid-signed by the operator's OWN node -- + * the app performs no crypto by design -- so claiming requires the local + * node to be up. `null` while unchecked, `false` when it is not reachable. + * + * Checked BEFORE the operator is asked for a NodeCode and a one-time PIN. + * Collecting both secrets and only then failing at the POST wastes a + * console trip, and the one-time PIN may be consumed or re-read for nothing. + */ + val localSignerReady: Boolean? = null, ) { val isPinned: Boolean get() = pinnedProfile != null val isAdminClaimed: Boolean get() = claimedRole != null diff --git a/client/shared/src/commonTest/kotlin/ai/ciris/mobile/shared/models/ClaimFailureTest.kt b/client/shared/src/commonTest/kotlin/ai/ciris/mobile/shared/models/ClaimFailureTest.kt new file mode 100644 index 0000000..e6cfb0c --- /dev/null +++ b/client/shared/src/commonTest/kotlin/ai/ciris/mobile/shared/models/ClaimFailureTest.kt @@ -0,0 +1,92 @@ +package ai.ciris.mobile.shared.models + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * The four claim outcomes an operator has to tell apart, and the one rule that + * keeps them apart: the server's CODES beat its prose. + */ +class ClaimFailureTest { + + @Test + fun codes_win_outright() { + assertEquals( + ClaimFailure.PIN_REJECTED, + classifyClaimFailure("target rejected the claim (HTTP 401): auth.claim.pin_invalid"), + ) + assertEquals( + ClaimFailure.PIN_REJECTED, + classifyClaimFailure("target rejected the claim (HTTP 400): auth.claim.pin_missing"), + ) + assertEquals( + ClaimFailure.ALREADY_CLAIMED, + classifyClaimFailure("target rejected the claim (HTTP 409): auth.claim.not_armed"), + ) + } + + @Test + fun a_reworded_server_message_still_classifies_by_code() { + // The whole reason to match the code: this sentence is not the sentence + // the server ships today, and the verdict must not depend on that. + assertEquals( + ClaimFailure.ALREADY_CLAIMED, + classifyClaimFailure("HTTP 409: auth.claim.not_armed — totally different wording here"), + ) + } + + @Test + fun prose_fallback_covers_older_nodes() { + assertEquals( + ClaimFailure.ALREADY_CLAIMED, + classifyClaimFailure( + "this node is not armed for a first-run claim (no one-time PIN) — " + + "ownership may already be claimed", + ), + ) + assertEquals(ClaimFailure.PIN_REJECTED, classifyClaimFailure("invalid claim pin")) + assertEquals( + ClaimFailure.UNREACHABLE, + classifyClaimFailure( + "target NodeCode carries no transport_hint — cannot reach the node to claim it", + ), + ) + assertEquals( + ClaimFailure.UNREACHABLE, + classifyClaimFailure("reach target node: connection refused"), + ) + assertEquals( + ClaimFailure.BAD_NODE_CODE, + classifyClaimFailure("decode target NodeCode: bad checksum"), + ) + } + + @Test + fun already_claimed_is_not_offered_as_a_retry() { + // Retrying a node that already has an owner cannot succeed, and telling + // the operator to try again sends them back to the console for a PIN + // that was never minted. + assertFalse(ClaimFailure.ALREADY_CLAIMED.isRetryable) + assertTrue(ClaimFailure.PIN_REJECTED.isRetryable) + assertTrue(ClaimFailure.UNREACHABLE.isRetryable) + assertTrue(ClaimFailure.BAD_NODE_CODE.isRetryable) + } + + @Test + fun an_already_claimed_body_is_not_read_as_a_pin_problem() { + // The not_armed sentence contains the word "PIN". Ordering the prose + // fallbacks wrongly would send the operator to hunt for a PIN that the + // node never printed, because it has an owner. + val body = "this node is not armed for a first-run claim (no one-time PIN)" + assertEquals(ClaimFailure.ALREADY_CLAIMED, classifyClaimFailure(body)) + } + + @Test + fun nothing_useful_stays_unknown() { + assertEquals(ClaimFailure.UNKNOWN, classifyClaimFailure(null)) + assertEquals(ClaimFailure.UNKNOWN, classifyClaimFailure("")) + assertEquals(ClaimFailure.UNKNOWN, classifyClaimFailure("HTTP 500 internal error")) + } +} diff --git a/client/shared/src/desktopMain/resources/localization/en.json b/client/shared/src/desktopMain/resources/localization/en.json index 8561878..931aee0 100644 --- a/client/shared/src/desktopMain/resources/localization/en.json +++ b/client/shared/src/desktopMain/resources/localization/en.json @@ -2993,7 +2993,8 @@ "wallet_transfer_note": "Transfer to another Base address for fiat cashout", "wallet_transfer_success": "Transfer successful! TX: {tx}", "wallet_trust_degraded": "Hardware Trust Degraded", - "wallet_warning": "Warning" + "wallet_warning": "Warning", + "claim_node_no_signer": "Claiming binds your identity, and that signature comes from your own node — which is not running. Start your node, then claim." }, "moderation": { "ladder": { diff --git a/docs/FSD-remote-first-run-claim.md b/docs/FSD-remote-first-run-claim.md new file mode 100644 index 0000000..9f0f947 --- /dev/null +++ b/docs/FSD-remote-first-run-claim.md @@ -0,0 +1,259 @@ +# FSD — Remote first-run claim + +**Status:** draft for review +**Author:** CIRISClient +**Covers:** claiming a remote CIRIS node from this client — both the +*configured-but-unclaimed* case and the *configure-and-claim* case. + +--- + +## 1. The gap + +There are CIRIS nodes running as research agents that are **fully configured and +unclaimed**. A node in that state works, serves its API, and has no responsible +party bound to it. Today an operator's route to claiming one from this client is +narrow and undiscovered, and for some client deployments it does not exist. + +Two cases, which are not the same problem: + +| | node state | what the operator needs | +|---|---|---| +| **A** | configured, unclaimed | claim it: bind an owner | +| **B** | bare, unconfigured | run first-run config against it, *then* claim | + +Case A is the one in front of us. Case B is the more ambitious one, and §3 +establishes that it is **blocked server-side today** — it is not a client +omission, and no amount of client work reaches it. + +--- + +## 2. What exists today + +**`ClaimNodeScreen`** (`ui/screens/ClaimNodeScreen.kt`) already takes a NodeCode, +a claim PIN and a display name, and drives `claimRemote`. It is reached from +`ManageNodesScreen` → "claim ownership" (`CIRISApp.kt:3583`). + +**`CIRISApiClient.claimRemote`** (`api/CIRISApiClient.kt:2264`) POSTs +`{node_code, claim_pin, cohort_scope}` to **the local node's** +`/v1/setup/claim-remote`. The app performs no crypto: the local node decodes the +NodeCode, builds and hybrid-signs the owner-binding +`delegates_to(user → target, infra:*)` in its own substrate, and POSTs the signed +artifact to **the target's** `/v1/setup/root`. + +**The first-run wizard** self-claims by the same path with the target set to +itself (`SetupViewModel.claimLocalNodeOwnership`, `viewmodels/SetupViewModel.kt:1052`), +taking the PIN from `claimPinProvider` — which reads the local node's +`/claim_pin` file. That provider returns null for any node this device did +not start, and the wizard then surfaces "claim PIN not captured … you can claim +ownership later from the Network surface". + +So the machinery for a remote claim exists. What is missing is above it. + +--- + +## 3. The contract that constrains the design + +Verified against CIRISServer 0.5.190/0.5.191 source and the released +`x86_64-unknown-linux-gnu` binary. + +### 3.1 Only ONE setup route is reachable off-host + +`src/auth/bootstrap.rs` builds two routers and merges them — +`claim.merge(loopback_reads)`, where `loopback_reads` carries +`require_loopback`: + +| route | reachable remotely? | +|---|---| +| `POST /v1/setup/root` | **yes** | +| `GET /v1/setup/status` | no — loopback only | +| `GET /v1/setup/owned-nodes` | no — loopback only | +| `GET /v1/setup/consent-disclosure` | no — loopback only | + +A non-loopback caller gets `403 "setup routes are localhost-only (run the wizard +on the node's own host)"` (`src/auth/loopback.rs:44`). + +**Measured, not inferred.** Released `ciris-server v0.5.190`, binding `0.0.0.0`, +queried over loopback and over the host's own LAN address (192.168.50.8): + +| route | via `127.0.0.1` | via LAN address | +|---|---|---| +| `GET /v1/setup/status` | 200 | **403** | +| `GET /v1/setup/owned-nodes` | 200 | **403** | +| `GET /v1/system/health` | 200 | 200 | +| `GET /v1/setup/root` | 405 | 405 | + +The last row is the useful one: `/v1/setup/root` answers **405 Method Not Allowed +from the LAN**, not 403. A loopback-layered route rejects before it ever +considers the method, so a 405 off-host is positive proof that the claim route +sits outside the guard — the claim is reachable remotely, and only the reads are +not. + +This is a deliberate trust boundary, not an oversight: `/v1/setup/root` can be +open because it authenticates on its own terms — the one-time claim PIN plus a +signed owner-binding — while the reads would otherwise leak a node's setup and +ownership posture to anyone who can reach it. + +### 3.2 Consequences, stated plainly + +1. **This client cannot ask a remote node whether it is set up, or whether it has + an owner.** `getSetupStatus()` and the `owned-nodes` probe behind + `nodeHasOwner()` both 403 off-host. Anything the UI wants to say about a + remote node's claim state must come from the operator or from a route outside + `/v1/setup/*`. + +2. **Case B is blocked.** The first-run wizard reads `/v1/setup/status` to know + there is a first run at all, and `/v1/setup/consent-disclosure` to render what + joining grants in the substrate's own words. Both are loopback-only, so the + wizard cannot run against a remote node. Closing Case B requires a CIRISServer + change — see §7. + +3. **Claiming runs through the local node, and every shipped platform has one.** + `/v1/setup/claim-remote` is where the signing happens, and it is loopback-only + *and* first-run-gated (`src/claim_remote.rs:391` — owner-gated once owned, + open during first-run, "loopback-only via the setup-route guard"). The app is + deliberately not allowed to do crypto itself, so the claim needs a substrate + holding the operator's identity. + + That substrate is always present on the platforms we ship to the stores: + desktop, **Android and iOS all run a local node** (`PythonRuntime.{desktop, + android,ios}.kt`). The precondition is therefore not "do you have a node" but + "is your node up and are you signed in to it" — a state the client can check + before asking for anything. + + The one exception is the **wasm/web** build, which has no local runtime + (`PythonRuntime.wasmJs.kt` — "Web mode - connecting to remote server"). Web is + remote-only and cannot claim; it must say so rather than offer the flow. + +### 3.3 The claim body + +`POST /v1/setup/root` takes `{node_code, cohort_scope, claim_pin, owner_binding}` +where `owner_binding` is the user-signed `delegates_to` +(`CIRISServer tests/ownership.rs:376`). `201` on success, echoing +`identity_key_id`, `cohort_scope`, `role: SYSTEM_ADMIN` and +`owner_binding_attestation_id`. + +--- + +## 4. Design — Case A: claim a configured, unclaimed remote node + +**Shape:** operator supplies the target's NodeCode and its one-time claim PIN +(read from that node's console — the PIN never travels over HTTP by design); the +**local** node signs and delivers the claim. + +``` + operator ──NodeCode + PIN──▶ client + │ POST /v1/setup/claim-remote (loopback) + ▼ + local node ──signs owner-binding──┐ + │ POST /v1/setup/root + ▼ + target node +``` + +### 4.1 What to build + +**A1 — An entry point that matches the task.** Claiming several research agents +in a sitting is the actual workload, and today the flow is one node at a time +buried under ManageNodes. `ClaimNodeScreen` already offers "claim another" +(`ClaimNodeScreen.kt:292`); the work is to make the entry point reachable and +named for the job, and to keep the entered display name with the claimed node. + +**A2 — Check the precondition before asking for secrets.** The claim is signed +by the operator's own node, so it needs that node up and a live session on it. +Both are knowable *before* the operator types a NodeCode and a PIN, and the +screen should say which one is missing up front rather than collecting both and +failing at the POST. On web, where there is no local runtime at all, the flow is +not offered. + +**A3 — Distinguish the PIN failures.** A wrong PIN, an already-claimed node, and +an unreachable target are three different situations with three different next +actions. `NodeSwitcherViewModel.claimRemoteNode` (`viewmodels/NodeSwitcherViewModel.kt:722`) +already inspects the body for `invalid_claim_pin`; the screen should render that +distinction rather than one generic failure. + +**A4 — Do not claim to know remote setup state.** Since §3.2(1) makes it +unknowable, the UI must not imply it. No "this node is unclaimed" badge for a +remote node; the operator is the source of that fact. + +### 4.2 Explicitly NOT in Case A + +Auto-discovery of unclaimed nodes on a network. The client cannot probe claim +state off-host, and a scan that inferred it from other signals would be guessing +about ownership — the one thing this flow exists to establish precisely. + +--- + +## 5. Design — Case B: configure and claim a remote node + +Blocked today (§3.2(2)). Two honest options: + +**B1 — Configure on the node's own host, claim from here.** No server change. +The operator runs first-run on the remote host (console/SSH — which they already +need for the PIN), and the node then falls into Case A. This is what the current +trust boundary is telling us to do, and it is the recommendation for now. + +**B2 — Make remote first-run possible.** Requires CIRISServer to expose, off-host +and safely, what the wizard needs: setup status and the consent disclosure. That +is a security decision about a deliberate boundary, and it belongs to the server +team, not to this client. §7 states the ask. + +--- + +## 6. UX — where the PIN is entered + +One rule, in both cases: **the claim PIN is typed by a human who read it from the +target node's console.** It is written `0600` to `/claim_pin`, served by no +route, and `/v1/setup/status` carries only its *path*, never its value +(`CIRISServer tests/claim_pin_file_is_declared.rs`). + +The local first-run wizard auto-fills it because the client is on the same host +and can read the file. That convenience does not generalise, and the code should +stop implying it does: `claimPinProvider` returning null is the normal case for +any node this device did not start, not an error condition. + +--- + +## 7. Server dependency (Case B only) + +For CIRISServer, if remote first-run is wanted: + +> The first-run wizard needs `GET /v1/setup/status` and +> `GET /v1/setup/consent-disclosure` off-host. Both are behind +> `require_loopback` today. What would make them safe to expose — the claim PIN +> as a bearer, a short-lived setup token minted on the console, an explicit +> `--allow-remote-setup` flag, or nothing at all? + +Until that is answered, Case B is B1. + +--- + +## 8. Testing + +The walk-test matrix (`testing/README.md`) grows a corner: + +| corner | node | asserts | +|---|---|---| +| `remote-unclaimed` | configured, no owner | the claim flow reaches `/v1/setup/root` and binds an owner | + +**A LIMITATION IN THE CURRENT HARNESS MUST BE FIXED FIRST, AND IT IS MINE.** The +existing "remote" corners put a facade on `127.0.0.1`, so the node sees a +**loopback** peer and the loopback-only routes answer normally. Those corners +therefore exercise the client's remote *configuration* path but not remote +*reachability* — a genuinely off-host client gets 403s that the harness never +sees. Testing any of §3 honestly requires the node to observe a non-loopback +source address (bind a second interface, or reach the host by its LAN address). +Until that is done, no walk-test result should be read as evidence about remote +behaviour. + +--- + +## 9. Summary of decisions + +1. Case A is buildable now and is the priority; the signing path already exists. +2. Case B is server-blocked; recommend B1 and raise §7 with the server team. +3. The client must never present remote setup/claim state it cannot observe. + Desktop, Android and iOS all carry a local node, so a signer always exists; + web is remote-only and cannot claim. +4. The claim PIN stays human-entered off-host. That is the design, not a gap. +5. The walk-test harness must see a non-loopback peer before it can test any of + this. diff --git a/testing/README.md b/testing/README.md index 922a58e..58ae12c 100644 --- a/testing/README.md +++ b/testing/README.md @@ -51,6 +51,20 @@ reports itself unconfigured (CIRISAgent#1075), and `undetermined` requires `/v1/setup/status` still said `setup_required: true`. An agent presents both, or it is not an agent. +### What the "remote" corners do NOT test + +The facade runs on `127.0.0.1`, so the node sees a **loopback** peer and its +loopback-only routes answer normally. `GET /v1/setup/status` and +`/v1/setup/owned-nodes` are localhost-only and return 403 to a genuinely off-host +client (measured against the released binary; see +`docs/FSD-remote-first-run-claim.md` §3.1). + +So these corners exercise the client's remote *configuration* path -- it is +pointed at another URL, it must not launch a node, it derives its mode from that +URL -- but not remote *reachability*. A real off-host client meets 403s this +harness never produces. Until a corner makes the node observe a non-loopback +source address, no result here is evidence about off-host behaviour. + ### The corner this cannot cover **local × agent.** The released node binds 4242/4243 with no port override — diff --git a/testing/cases.py b/testing/cases.py index 648ad9f..1852212 100644 --- a/testing/cases.py +++ b/testing/cases.py @@ -146,22 +146,6 @@ def did_launch_a_node(ctx: Context) -> None: ctx.notes.append("client launched its own node") -@case("claim_pin_is_readable", (LOCAL_NODE,), "the first-run path the desktop wheel broke") -def claim_pin_is_readable(ctx: Context) -> None: - home = ctx.node_home - if home is None: - raise AssertionError("harness bug: no node home recorded for the local corner") - from pathlib import Path - - pin_file = Path(str(home)) / "claim_pin" - if not pin_file.exists() or not pin_file.read_text().strip(): - raise AssertionError( - f"the node wrote no claim PIN at {pin_file}. First run cannot be " - f"completed by a client that only reads the file." - ) - ctx.notes.append("claim PIN present in the node home") - - @case("automation_surface_answers", ALL_CORNERS, "the driver's own contract with the app") def automation_surface_answers(ctx: Context) -> None: h = ctx.app.health() diff --git a/testing/node_fixture.py b/testing/node_fixture.py index 6ad4945..2c1b263 100644 --- a/testing/node_fixture.py +++ b/testing/node_fixture.py @@ -61,12 +61,19 @@ # read by `clientModeFrom`; nothing is decorative. BRAIN_MERGE = { "cognitive_state": "WORK", - "role": "agent", "services": { f"service_{i:02d}": {"healthy": True, "status": "ok"} for i in range(22) }, "agent": {"folded": True, "reachable": True}, } +# NOTE THE ABSENCE OF `role` (Codex, PR #10). A node's merged health keeps +# `role: "fabric-node"` while a folded brain answers over it -- ClientMode.kt +# says so explicitly, and `clientModeFrom` treats `role == "agent"` as +# CONCLUSIVE, checked before it looks at anything else. Setting it here made +# the agent corner green on that one field alone: the client could stop reading +# cognitive_state, the service map and agent.reachable entirely -- the whole +# folded-node derivation this corner exists for -- and nothing would go red. +# Leaving the real node's role intact forces the verdict through `answeringFold`. # Folded but not answering: the UNDETERMINED verdict. No cognitive_state, no # services -- the two positive signals are absent, so a client that latches @@ -92,7 +99,18 @@ } # Per-corner rewrites, keyed by the route they apply to. -NODE_REWRITES: dict[str, dict] = {} + +# A bare node that is SET UP (Codex, PR #10). +# +# The setup rewrite is what makes this corner test anything. A fresh node +# reports `setup_required: true`, and `brainUnconfigured` is the FIRST arm of +# `clientModeFrom` -- an unconditional NODE that never reaches the role check, +# the cognitive_state check or the service-count check. So a bare node on a +# fresh home is classified correctly for a reason that bypasses classification, +# and a client that promoted every configured node to AGENT stayed green here. +# The health envelope is left exactly as the real node reports it: this is a +# genuinely bare node that has simply completed its wizard. +NODE_REWRITES = {"/v1/setup/status": CONFIGURED_SETUP} AGENT_REWRITES = { "/v1/system/health": BRAIN_MERGE, "/v1/setup/status": CONFIGURED_SETUP, @@ -185,11 +203,31 @@ def start(self) -> "RealNode": try: wait_until_up(self.url) except RuntimeError: + # KILL IT BEFORE RAISING (Codex, PR #10). A node that starts but + # never goes healthy is still holding the fixed port. Raising from + # here means `start()` never returns, so the caller never assigns + # self.node and its `finally` has nothing to clean up -- the orphan + # then fails every following corner, and outlives the run on a + # developer's machine. + tail = self.tail() + self.stop_process_only() raise RuntimeError( - f"node did not come up; last log lines:\n{self.tail()}" + f"node did not come up; last log lines:\n{tail}" ) from None return self + def stop_process_only(self) -> None: + """Terminate without waiting on the port -- for a node that never rose.""" + if not self.proc: + return + self.proc.terminate() + try: + self.proc.wait(timeout=15) + except subprocess.TimeoutExpired: + self.proc.kill() + self.proc.wait(timeout=10) + self.proc = None + def tail(self, n: int = 30) -> str: if not self.log or not self.log.exists(): return "" diff --git a/testing/run_e2e.py b/testing/run_e2e.py index add9bd8..9cdbcf3 100644 --- a/testing/run_e2e.py +++ b/testing/run_e2e.py @@ -81,6 +81,10 @@ class CornerResult: status: str # passed | failed | error cases: list[CaseResult] = field(default_factory=list) screen: str = "" + #: The app's own /state at the end of the corner -- clientMode and node URL. + #: Evidence, and the thing a later compatibility question + #: will actually be asked about. + app_state: dict = field(default_factory=dict) elements: list[str] = field(default_factory=list) node_log_tail: str = "" app_log_tail: str = "" @@ -123,6 +127,11 @@ def _pids_on_port(port: int) -> list[int]: return pids +def _client_version() -> str: + v = Path(__file__).resolve().parent.parent / "VERSION" + return v.read_text().strip() if v.is_file() else "" + + def _port_busy(port: int) -> bool: with socket.socket() as s: s.settimeout(0.5) @@ -249,14 +258,21 @@ def app_env(self) -> dict[str, str]: env["HOME"] = str(self.workdir / "fakehome") Path(env["CIRIS_HOME"]).mkdir(parents=True, exist_ok=True) Path(env["HOME"]).mkdir(parents=True, exist_ok=True) + # The node binary is on PATH in EVERY corner (Codex, PR #10). + # + # It reads like it belongs only to the local corner, and putting it + # there made `did_not_launch_a_node` vacuous: on a CI runner there is no + # `ciris-server` on PATH at all, so a client that DOES launch a local + # node whenever it can find one -- the actual regression, and the common + # developer setup where a node is installed -- had no way to exhibit it. + # The corners now differ by CIRIS_API_URL alone, which is the real + # difference, and the negative is a negative. + env["PATH"] = f"{Path(self.node_bin).parent}{os.pathsep}{env.get('PATH','')}" if self.api_url: # Both spellings: CIRIS_NODE_URL is upstream's, CIRIS_API_URL is ours. env["CIRIS_API_URL"] = self.api_url env["CIRIS_NODE_URL"] = self.api_url - if self.name == LOCAL_NODE: - # The app finds the node by PATH lookup; give it exactly the binary - # this run downloaded, not whatever the developer has installed. - env["PATH"] = f"{Path(self.node_bin).parent}{os.pathsep}{env.get('PATH','')}" + else: env.pop("CIRIS_API_URL", None) env.pop("CIRIS_NODE_URL", None) return env @@ -397,6 +413,7 @@ def run(self) -> CornerResult: res.cases.append(CaseResult(c.name, "skipped", detail=f"not applicable to {self.name}")) res.screen = app.screen() + res.app_state = app.state() res.elements = sorted(app.tags()) if res.status == "failed": shot = self.workdir / "failure.png" @@ -422,6 +439,9 @@ def main() -> int: ap.add_argument("--node-bin", default=None, help="released ciris-server binary") ap.add_argument("--test-port", type=int, default=TEST_PORT_DEFAULT) ap.add_argument("--report", default=None, help="write a JSON report here") + ap.add_argument("--node-version", default=None, + help="the CIRISServer tag this node came from; recorded in " + "the report so the evidence identifies its pairing") ap.add_argument("--workdir", default=None) ap.add_argument("--reclaim", action="store_true", help="kill a leftover TEST-MODE app holding the test port " @@ -443,7 +463,9 @@ def main() -> int: print(f"jar : {jar}") print(f"node : {node_bin}") print(f"workdir : {root}") - print(f"corners : {', '.join(corners)}\n") + print(f"corners : {', '.join(corners)}") + print(f"pairing : client {_client_version()} against node " + f"{args.node_version or ''}\n") if _port_busy(args.test_port) and args.reclaim: # Only ever a TEST-MODE CIRIS app: that is a disposable artefact of a @@ -500,8 +522,16 @@ def main() -> int: print(f" -> {r.status.upper()} in {r.seconds}s\n") if args.report: - Path(args.report).write_text(json.dumps( - {"corners": [asdict(r) for r in results]}, indent=2)) + # WHICH PAIRING PRODUCED THIS (Codex, PR #10). The client can cut a + # version before the server tags the matching one, so CI falls back to + # the latest release -- and evidence that cannot say which node it ran + # against cannot be used to settle a compatibility question later. + Path(args.report).write_text(json.dumps({ + "client_version": _client_version(), + "node_version": args.node_version or "", + "node_binary": node_bin, + "corners": [asdict(r) for r in results], + }, indent=2)) print(f"report: {args.report}") bad = [r for r in results if r.status != "passed"] From 361e9e15ffd4077c0d9a071d9d42f7a4d16d7fdd Mon Sep 17 00:00:00 2001 From: Eric Moore Date: Thu, 27 Aug 2026 18:55:09 -0500 Subject: [PATCH 3/5] fix(i18n): the English was the defect, again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The translate lane failed closed on Yoruba: rejected at sonnet, at opus, and at gpt-5-pro. Escalation is the design and it ran; what it could not do is translate a sentence that does not parse. Claiming binds your identity, and that signature comes from your own node — which is not running. Start your node, then claim. "that signature" refers to NOTHING. No signature has been mentioned; the reader is asked to resolve a definite reference to a noun that never appeared. That is rule 2 of localization/TRANSLATION_GUIDE.md §3 — the dangling referent — in a string I wrote after writing the rule. The reviewer named both faults precisely, in Yoruba, and both are faults in the English: major/accuracy reorders the logic and makes it sound as if what is bound is the node itself, not the act of claiming major/fluency "kò ń ṣiṣẹ́" is ungrammatical — negative with progressive The second looks like a translation defect and is downstream of the first: an em-dash clause hanging off an unresolvable referent gives the model nothing to attach the negation to. Your own node signs the claim, and it is not running. Start your node, then claim. Subject signs object. "it" is the node, the nearest and only candidate. Nothing is introduced that was not named. This is the eighth time in this repo that a translation blocker was an English blocker, and the pipeline's value is mostly that it says so out loud instead of shipping 28 fluent renderings of a sentence nobody could follow. Nothing was committed by the failed run: the write step ran, the commit step did not. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01EGE52kPzjGFiPzGcs63ZC1 --- client/VENDORING.md | 2 +- client/androidApp/src/main/assets/localization/en.json | 2 +- client/desktopApp/src/main/resources/localization/en.json | 2 +- client/iosApp/iosApp/localization/en.json | 2 +- client/shared/src/desktopMain/resources/localization/en.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/VENDORING.md b/client/VENDORING.md index c483288..46fdfb9 100644 --- a/client/VENDORING.md +++ b/client/VENDORING.md @@ -40,7 +40,7 @@ source is the pair a bisect wants: The tree's current recorded state — sha256-of-sha256s over every git-tracked file under `client/` except this one: -**state digest:** `30c55d9c2263a2fc12b0897d98e3d9f4c785119648ac7a90b2b9b1e297562fbb` +**state digest:** `36ba650dbecbd3942196a6ae30ad5da22392de51e71a2f03ca0afb0bed3c7a40` `packaging/check_vendoring.py` asserts it on every push, and refuses any tracked file matching a §2 never-vendor class. **Any commit that touches diff --git a/client/androidApp/src/main/assets/localization/en.json b/client/androidApp/src/main/assets/localization/en.json index 931aee0..bcab02d 100644 --- a/client/androidApp/src/main/assets/localization/en.json +++ b/client/androidApp/src/main/assets/localization/en.json @@ -2994,7 +2994,7 @@ "wallet_transfer_success": "Transfer successful! TX: {tx}", "wallet_trust_degraded": "Hardware Trust Degraded", "wallet_warning": "Warning", - "claim_node_no_signer": "Claiming binds your identity, and that signature comes from your own node — which is not running. Start your node, then claim." + "claim_node_no_signer": "Your own node signs the claim, and it is not running. Start your node, then claim." }, "moderation": { "ladder": { diff --git a/client/desktopApp/src/main/resources/localization/en.json b/client/desktopApp/src/main/resources/localization/en.json index 931aee0..bcab02d 100644 --- a/client/desktopApp/src/main/resources/localization/en.json +++ b/client/desktopApp/src/main/resources/localization/en.json @@ -2994,7 +2994,7 @@ "wallet_transfer_success": "Transfer successful! TX: {tx}", "wallet_trust_degraded": "Hardware Trust Degraded", "wallet_warning": "Warning", - "claim_node_no_signer": "Claiming binds your identity, and that signature comes from your own node — which is not running. Start your node, then claim." + "claim_node_no_signer": "Your own node signs the claim, and it is not running. Start your node, then claim." }, "moderation": { "ladder": { diff --git a/client/iosApp/iosApp/localization/en.json b/client/iosApp/iosApp/localization/en.json index 931aee0..bcab02d 100644 --- a/client/iosApp/iosApp/localization/en.json +++ b/client/iosApp/iosApp/localization/en.json @@ -2994,7 +2994,7 @@ "wallet_transfer_success": "Transfer successful! TX: {tx}", "wallet_trust_degraded": "Hardware Trust Degraded", "wallet_warning": "Warning", - "claim_node_no_signer": "Claiming binds your identity, and that signature comes from your own node — which is not running. Start your node, then claim." + "claim_node_no_signer": "Your own node signs the claim, and it is not running. Start your node, then claim." }, "moderation": { "ladder": { diff --git a/client/shared/src/desktopMain/resources/localization/en.json b/client/shared/src/desktopMain/resources/localization/en.json index 931aee0..bcab02d 100644 --- a/client/shared/src/desktopMain/resources/localization/en.json +++ b/client/shared/src/desktopMain/resources/localization/en.json @@ -2994,7 +2994,7 @@ "wallet_transfer_success": "Transfer successful! TX: {tx}", "wallet_trust_degraded": "Hardware Trust Degraded", "wallet_warning": "Warning", - "claim_node_no_signer": "Claiming binds your identity, and that signature comes from your own node — which is not running. Start your node, then claim." + "claim_node_no_signer": "Your own node signs the claim, and it is not running. Start your node, then claim." }, "moderation": { "ladder": { From e9351864b6359dc294a018e72fd64755bceddd74 Mon Sep 17 00:00:00 2001 From: Eric Moore Date: Thu, 27 Aug 2026 19:16:48 -0500 Subject: [PATCH 4/5] fix(i18n): "claim" needed an object, and "running" needed to mean stopped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second rejection, five languages this time — fr, ha, sw, uk, yo — and the same verdict as the first: the English is what cannot be translated. Your own node signs the claim, and it is not running. Start your node, then claim. Two faults, each of which English lets a writer leave open and no target language can: "claim" FLOATS BETWEEN NOUN AND VERB, AND THE VERB HAS NO OBJECT. fr needs the equivalent of "then claim IT"; the object is implicit in English only ha bare "claim" reads as CONTINUING a claim rather than starting one uk "the claim" as a noun collides with the established UI term for claiming a node ("заявити право на вузол") — the anchors already fixed a term and this string reached past it "IS NOT RUNNING" DOES NOT SAY WHETHER THE NODE IS STOPPED OR BROKEN. sw aspect lands on "has not been operated" yo reads as "it does not work / is faulty" — a broken node, not one that is simply not started Your node must be started before it can claim another node. Start your node, then try again. "claim" appears once, as a transitive verb with an explicit object, so there is no noun form to collide with the established term. "must be started" is a state rather than a health verdict. "try again" carries the second action without a second objectless "claim". The reviewer is doing the job it was built for: uk did not object to grammar, it objected that the string ignored terminology the corpus had already settled. That is the "corpus outranks your instinct" rule in TRANSLATION_GUIDE.md §3, enforced against me. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01EGE52kPzjGFiPzGcs63ZC1 --- client/VENDORING.md | 2 +- client/androidApp/src/main/assets/localization/en.json | 2 +- client/desktopApp/src/main/resources/localization/en.json | 2 +- client/iosApp/iosApp/localization/en.json | 2 +- client/shared/src/desktopMain/resources/localization/en.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/VENDORING.md b/client/VENDORING.md index 46fdfb9..d500443 100644 --- a/client/VENDORING.md +++ b/client/VENDORING.md @@ -40,7 +40,7 @@ source is the pair a bisect wants: The tree's current recorded state — sha256-of-sha256s over every git-tracked file under `client/` except this one: -**state digest:** `36ba650dbecbd3942196a6ae30ad5da22392de51e71a2f03ca0afb0bed3c7a40` +**state digest:** `882b9b3c4e9b4c68b16627368f8449df5932e8d0565d5ca9fcb903494a09c111` `packaging/check_vendoring.py` asserts it on every push, and refuses any tracked file matching a §2 never-vendor class. **Any commit that touches diff --git a/client/androidApp/src/main/assets/localization/en.json b/client/androidApp/src/main/assets/localization/en.json index bcab02d..336913c 100644 --- a/client/androidApp/src/main/assets/localization/en.json +++ b/client/androidApp/src/main/assets/localization/en.json @@ -2994,7 +2994,7 @@ "wallet_transfer_success": "Transfer successful! TX: {tx}", "wallet_trust_degraded": "Hardware Trust Degraded", "wallet_warning": "Warning", - "claim_node_no_signer": "Your own node signs the claim, and it is not running. Start your node, then claim." + "claim_node_no_signer": "Your node must be started before it can claim another node. Start your node, then try again." }, "moderation": { "ladder": { diff --git a/client/desktopApp/src/main/resources/localization/en.json b/client/desktopApp/src/main/resources/localization/en.json index bcab02d..336913c 100644 --- a/client/desktopApp/src/main/resources/localization/en.json +++ b/client/desktopApp/src/main/resources/localization/en.json @@ -2994,7 +2994,7 @@ "wallet_transfer_success": "Transfer successful! TX: {tx}", "wallet_trust_degraded": "Hardware Trust Degraded", "wallet_warning": "Warning", - "claim_node_no_signer": "Your own node signs the claim, and it is not running. Start your node, then claim." + "claim_node_no_signer": "Your node must be started before it can claim another node. Start your node, then try again." }, "moderation": { "ladder": { diff --git a/client/iosApp/iosApp/localization/en.json b/client/iosApp/iosApp/localization/en.json index bcab02d..336913c 100644 --- a/client/iosApp/iosApp/localization/en.json +++ b/client/iosApp/iosApp/localization/en.json @@ -2994,7 +2994,7 @@ "wallet_transfer_success": "Transfer successful! TX: {tx}", "wallet_trust_degraded": "Hardware Trust Degraded", "wallet_warning": "Warning", - "claim_node_no_signer": "Your own node signs the claim, and it is not running. Start your node, then claim." + "claim_node_no_signer": "Your node must be started before it can claim another node. Start your node, then try again." }, "moderation": { "ladder": { diff --git a/client/shared/src/desktopMain/resources/localization/en.json b/client/shared/src/desktopMain/resources/localization/en.json index bcab02d..336913c 100644 --- a/client/shared/src/desktopMain/resources/localization/en.json +++ b/client/shared/src/desktopMain/resources/localization/en.json @@ -2994,7 +2994,7 @@ "wallet_transfer_success": "Transfer successful! TX: {tx}", "wallet_trust_degraded": "Hardware Trust Degraded", "wallet_warning": "Warning", - "claim_node_no_signer": "Your own node signs the claim, and it is not running. Start your node, then claim." + "claim_node_no_signer": "Your node must be started before it can claim another node. Start your node, then try again." }, "moderation": { "ladder": { From 8040d570bca6358c84303ef4497c9ac1a7d652bc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 00:20:07 +0000 Subject: [PATCH 5/5] i18n: translate lane (translate -> evaluate -> repair) Machine translation, independently reviewed against MQM, and repaired where the review found a critical, major or terminology error. Every value here is status=draft / review_status=needs_native_review: this pipeline guarantees terminology, structure and meaning, and does not guarantee native fluency. Validated by check_localization_sync.py --strict in this same run. The MQM findings are attached to the run as i18n-report.json. Review like any other diff. --- client/VENDORING.md | 2 +- client/androidApp/src/main/assets/localization/am.json | 1 + client/androidApp/src/main/assets/localization/ar.json | 1 + client/androidApp/src/main/assets/localization/bn.json | 1 + client/androidApp/src/main/assets/localization/de.json | 1 + client/androidApp/src/main/assets/localization/es.json | 1 + client/androidApp/src/main/assets/localization/fa.json | 1 + client/androidApp/src/main/assets/localization/fr.json | 1 + client/androidApp/src/main/assets/localization/ha.json | 1 + client/androidApp/src/main/assets/localization/hi.json | 1 + client/androidApp/src/main/assets/localization/id.json | 1 + client/androidApp/src/main/assets/localization/it.json | 1 + client/androidApp/src/main/assets/localization/ja.json | 1 + client/androidApp/src/main/assets/localization/ko.json | 1 + client/androidApp/src/main/assets/localization/mr.json | 1 + client/androidApp/src/main/assets/localization/my.json | 1 + client/androidApp/src/main/assets/localization/pa.json | 1 + client/androidApp/src/main/assets/localization/pt.json | 1 + client/androidApp/src/main/assets/localization/ru.json | 1 + client/androidApp/src/main/assets/localization/sw.json | 1 + client/androidApp/src/main/assets/localization/ta.json | 1 + client/androidApp/src/main/assets/localization/te.json | 1 + client/androidApp/src/main/assets/localization/th.json | 1 + client/androidApp/src/main/assets/localization/tr.json | 1 + client/androidApp/src/main/assets/localization/uk.json | 1 + client/androidApp/src/main/assets/localization/ur.json | 1 + client/androidApp/src/main/assets/localization/vi.json | 1 + client/androidApp/src/main/assets/localization/yo.json | 1 + client/androidApp/src/main/assets/localization/zh.json | 1 + client/desktopApp/src/main/resources/localization/am.json | 1 + client/desktopApp/src/main/resources/localization/ar.json | 1 + client/desktopApp/src/main/resources/localization/bn.json | 1 + client/desktopApp/src/main/resources/localization/de.json | 1 + client/desktopApp/src/main/resources/localization/es.json | 1 + client/desktopApp/src/main/resources/localization/fa.json | 1 + client/desktopApp/src/main/resources/localization/fr.json | 1 + client/desktopApp/src/main/resources/localization/ha.json | 1 + client/desktopApp/src/main/resources/localization/hi.json | 1 + client/desktopApp/src/main/resources/localization/id.json | 1 + client/desktopApp/src/main/resources/localization/it.json | 1 + client/desktopApp/src/main/resources/localization/ja.json | 1 + client/desktopApp/src/main/resources/localization/ko.json | 1 + client/desktopApp/src/main/resources/localization/mr.json | 1 + client/desktopApp/src/main/resources/localization/my.json | 1 + client/desktopApp/src/main/resources/localization/pa.json | 1 + client/desktopApp/src/main/resources/localization/pt.json | 1 + client/desktopApp/src/main/resources/localization/ru.json | 1 + client/desktopApp/src/main/resources/localization/sw.json | 1 + client/desktopApp/src/main/resources/localization/ta.json | 1 + client/desktopApp/src/main/resources/localization/te.json | 1 + client/desktopApp/src/main/resources/localization/th.json | 1 + client/desktopApp/src/main/resources/localization/tr.json | 1 + client/desktopApp/src/main/resources/localization/uk.json | 1 + client/desktopApp/src/main/resources/localization/ur.json | 1 + client/desktopApp/src/main/resources/localization/vi.json | 1 + client/desktopApp/src/main/resources/localization/yo.json | 1 + client/desktopApp/src/main/resources/localization/zh.json | 1 + client/iosApp/iosApp/localization/am.json | 1 + client/iosApp/iosApp/localization/ar.json | 1 + client/iosApp/iosApp/localization/bn.json | 1 + client/iosApp/iosApp/localization/de.json | 1 + client/iosApp/iosApp/localization/es.json | 1 + client/iosApp/iosApp/localization/fa.json | 1 + client/iosApp/iosApp/localization/fr.json | 1 + client/iosApp/iosApp/localization/ha.json | 1 + client/iosApp/iosApp/localization/hi.json | 1 + client/iosApp/iosApp/localization/id.json | 1 + client/iosApp/iosApp/localization/it.json | 1 + client/iosApp/iosApp/localization/ja.json | 1 + client/iosApp/iosApp/localization/ko.json | 1 + client/iosApp/iosApp/localization/mr.json | 1 + client/iosApp/iosApp/localization/my.json | 1 + client/iosApp/iosApp/localization/pa.json | 1 + client/iosApp/iosApp/localization/pt.json | 1 + client/iosApp/iosApp/localization/ru.json | 1 + client/iosApp/iosApp/localization/sw.json | 1 + client/iosApp/iosApp/localization/ta.json | 1 + client/iosApp/iosApp/localization/te.json | 1 + client/iosApp/iosApp/localization/th.json | 1 + client/iosApp/iosApp/localization/tr.json | 1 + client/iosApp/iosApp/localization/uk.json | 1 + client/iosApp/iosApp/localization/ur.json | 1 + client/iosApp/iosApp/localization/vi.json | 1 + client/iosApp/iosApp/localization/yo.json | 1 + client/iosApp/iosApp/localization/zh.json | 1 + client/shared/src/desktopMain/resources/localization/am.json | 1 + client/shared/src/desktopMain/resources/localization/ar.json | 1 + client/shared/src/desktopMain/resources/localization/bn.json | 1 + client/shared/src/desktopMain/resources/localization/de.json | 1 + client/shared/src/desktopMain/resources/localization/es.json | 1 + client/shared/src/desktopMain/resources/localization/fa.json | 1 + client/shared/src/desktopMain/resources/localization/fr.json | 1 + client/shared/src/desktopMain/resources/localization/ha.json | 1 + client/shared/src/desktopMain/resources/localization/hi.json | 1 + client/shared/src/desktopMain/resources/localization/id.json | 1 + client/shared/src/desktopMain/resources/localization/it.json | 1 + client/shared/src/desktopMain/resources/localization/ja.json | 1 + client/shared/src/desktopMain/resources/localization/ko.json | 1 + client/shared/src/desktopMain/resources/localization/mr.json | 1 + client/shared/src/desktopMain/resources/localization/my.json | 1 + client/shared/src/desktopMain/resources/localization/pa.json | 1 + client/shared/src/desktopMain/resources/localization/pt.json | 1 + client/shared/src/desktopMain/resources/localization/ru.json | 1 + client/shared/src/desktopMain/resources/localization/sw.json | 1 + client/shared/src/desktopMain/resources/localization/ta.json | 1 + client/shared/src/desktopMain/resources/localization/te.json | 1 + client/shared/src/desktopMain/resources/localization/th.json | 1 + client/shared/src/desktopMain/resources/localization/tr.json | 1 + client/shared/src/desktopMain/resources/localization/uk.json | 1 + client/shared/src/desktopMain/resources/localization/ur.json | 1 + client/shared/src/desktopMain/resources/localization/vi.json | 1 + client/shared/src/desktopMain/resources/localization/yo.json | 1 + client/shared/src/desktopMain/resources/localization/zh.json | 1 + 113 files changed, 113 insertions(+), 1 deletion(-) diff --git a/client/VENDORING.md b/client/VENDORING.md index d500443..03e3227 100644 --- a/client/VENDORING.md +++ b/client/VENDORING.md @@ -40,7 +40,7 @@ source is the pair a bisect wants: The tree's current recorded state — sha256-of-sha256s over every git-tracked file under `client/` except this one: -**state digest:** `882b9b3c4e9b4c68b16627368f8449df5932e8d0565d5ca9fcb903494a09c111` +**state digest:** `b07c80c55503d4082466878ab3fed24027983f88e45ad2371212b785cdf2ea63` `packaging/check_vendoring.py` asserts it on every push, and refuses any tracked file matching a §2 never-vendor class. **Any commit that touches diff --git a/client/androidApp/src/main/assets/localization/am.json b/client/androidApp/src/main/assets/localization/am.json index b478e20..55d8b26 100644 --- a/client/androidApp/src/main/assets/localization/am.json +++ b/client/androidApp/src/main/assets/localization/am.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "ማስተላለፍ ተሳክቷል! TX: {tx}", "wallet_trust_degraded": "ስልክ ሥቅ ተዳክሞ ነበር", "wallet_warning": "ማስጠንቀቂያ", + "claim_node_no_signer": "የእርስዎ ኖድ ሌላ ኖድ ከመያዙ በፊት መጀመር አለበት። ኖድዎን ይጀምሩ፣ ከዚያ እንደገና ይሞክሩ።", "attestation_actions_desc": "ለዚህ መዝገብ ያሉ እርምጃዎች", "chat_badge_message": "መልዕክት", "chat_details": "ዓይነት {type} · ወሰን {scope} · ሁኔታ {status}", diff --git a/client/androidApp/src/main/assets/localization/ar.json b/client/androidApp/src/main/assets/localization/ar.json index 3b025d4..31d7e78 100644 --- a/client/androidApp/src/main/assets/localization/ar.json +++ b/client/androidApp/src/main/assets/localization/ar.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "نجح التحويل! TX: {tx}", "wallet_trust_degraded": "تدهورت ثقة الأجهزة", "wallet_warning": "تحذير", + "claim_node_no_signer": "يجب تشغيل عقدتك قبل أن تتمكن من المطالبة بعقدة أخرى. شغّل عقدتك، ثم حاول مرة أخرى.", "attestation_actions_desc": "إجراءات هذا السجل", "chat_badge_message": "رسالة", "chat_details": "النوع {type} · النطاق {scope} · الحالة {status}", diff --git a/client/androidApp/src/main/assets/localization/bn.json b/client/androidApp/src/main/assets/localization/bn.json index 30f1052..744a4b8 100644 --- a/client/androidApp/src/main/assets/localization/bn.json +++ b/client/androidApp/src/main/assets/localization/bn.json @@ -2925,6 +2925,7 @@ "wallet_transfer_success": "ট্রান্সফার সফল! TX: {tx}", "wallet_trust_degraded": "হার্ডওয়্যার বিশ্বাস হ্রাস পেয়েছে", "wallet_warning": "সতর্কতা", + "claim_node_no_signer": "আরেকটি নোড দাবি করার আগে আপনার নোড চালু থাকতে হবে। আপনার নোড চালু করুন, তারপর আবার চেষ্টা করুন।", "attestation_actions_desc": "এই রেকর্ডের জন্য পদক্ষেপ", "chat_badge_message": "বার্তা", "chat_details": "ধরন {type} · পরিসর {scope} · অবস্থা {status}", diff --git a/client/androidApp/src/main/assets/localization/de.json b/client/androidApp/src/main/assets/localization/de.json index 74e684c..1bb19cb 100644 --- a/client/androidApp/src/main/assets/localization/de.json +++ b/client/androidApp/src/main/assets/localization/de.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Überweisung erfolgreich! TX: {tx}", "wallet_trust_degraded": "Hardware-Vertrauen herabgestuft", "wallet_warning": "Warnung", + "claim_node_no_signer": "Ihr Node muss gestartet sein, bevor er einen anderen Knoten übernehmen kann. Starten Sie Ihren Node und versuchen Sie es erneut.", "attestation_actions_desc": "Aktionen für diesen Datensatz", "chat_badge_message": "NACHRICHT", "chat_details": "Typ {type} · Bereich {scope} · Status {status}", diff --git a/client/androidApp/src/main/assets/localization/es.json b/client/androidApp/src/main/assets/localization/es.json index 66a6762..6b600c2 100644 --- a/client/androidApp/src/main/assets/localization/es.json +++ b/client/androidApp/src/main/assets/localization/es.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "¡Transferencia exitosa! TX: {tx}", "wallet_trust_degraded": "Confianza de Hardware Degradada", "wallet_warning": "Advertencia", + "claim_node_no_signer": "Tu nodo debe estar iniciado antes de poder reclamar otro nodo. Inicia tu nodo e inténtalo de nuevo.", "attestation_actions_desc": "Acciones para este registro", "chat_badge_message": "MENSAJE", "chat_details": "tipo {type} · ámbito {scope} · estado {status}", diff --git a/client/androidApp/src/main/assets/localization/fa.json b/client/androidApp/src/main/assets/localization/fa.json index d9caea0..0fda01b 100644 --- a/client/androidApp/src/main/assets/localization/fa.json +++ b/client/androidApp/src/main/assets/localization/fa.json @@ -2930,6 +2930,7 @@ "wallet_transfer_success": "انتقال با موفقیت انجام شد! TX: {tx}", "wallet_trust_degraded": "افت اعتماد سخت‌افزاری", "wallet_warning": "هشدار", + "claim_node_no_signer": "گرهٔ شما باید پیش از ادعای گره‌ای دیگر راه‌اندازی شده باشد. گرهٔ خود را راه‌اندازی کنید و دوباره تلاش کنید.", "attestation_actions_desc": "اقدامات برای این رکورد", "chat_badge_message": "پیام", "chat_details": "نوع {type} · محدوده {scope} · وضعیت {status}", diff --git a/client/androidApp/src/main/assets/localization/fr.json b/client/androidApp/src/main/assets/localization/fr.json index 10abf12..5716835 100644 --- a/client/androidApp/src/main/assets/localization/fr.json +++ b/client/androidApp/src/main/assets/localization/fr.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfert réussi ! TX: {tx}", "wallet_trust_degraded": "Confiance Matérielle Dégradée", "wallet_warning": "Avertissement", + "claim_node_no_signer": "Votre nœud doit être démarré avant de pouvoir revendiquer un autre nœud. Démarrez votre nœud, puis réessayez.", "attestation_actions_desc": "Actions pour cet enregistrement", "chat_badge_message": "MESSAGE", "chat_details": "type {type} · portée {scope} · statut {status}", diff --git a/client/androidApp/src/main/assets/localization/ha.json b/client/androidApp/src/main/assets/localization/ha.json index 7ac4491..79baae8 100644 --- a/client/androidApp/src/main/assets/localization/ha.json +++ b/client/androidApp/src/main/assets/localization/ha.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Aika ya yi nasara! TX: {tx}", "wallet_trust_degraded": "Tabbas Kayan Aiki Ya Rage", "wallet_warning": "Gargaɗi", + "claim_node_no_signer": "Dole ne a fara kumburinka kafin ya iya karɓar wani kumburi. Fara kumburinka, sannan ka sake gwadawa.", "attestation_actions_desc": "Ayyuka don wannan rikodin", "chat_badge_message": "SAƘO", "chat_details": "nau'i {type} · iyaka {scope} · matsayi {status}", diff --git a/client/androidApp/src/main/assets/localization/hi.json b/client/androidApp/src/main/assets/localization/hi.json index 7654f45..d483015 100644 --- a/client/androidApp/src/main/assets/localization/hi.json +++ b/client/androidApp/src/main/assets/localization/hi.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "स्थानांतरण सफल! TX: {tx}", "wallet_trust_degraded": "हार्डवेयर ट्रस्ट कम हुआ", "wallet_warning": "चेतावनी", + "claim_node_no_signer": "किसी अन्य नोड का दावा करने से पहले आपका नोड शुरू होना चाहिए। अपना नोड शुरू करें, फिर दोबारा प्रयास करें।", "attestation_actions_desc": "इस रिकॉर्ड के लिए कार्रवाइयाँ", "chat_badge_message": "संदेश", "chat_details": "प्रकार {type} · दायरा {scope} · स्थिति {status}", diff --git a/client/androidApp/src/main/assets/localization/id.json b/client/androidApp/src/main/assets/localization/id.json index 809bf6a..aa8a9af 100644 --- a/client/androidApp/src/main/assets/localization/id.json +++ b/client/androidApp/src/main/assets/localization/id.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer berhasil! TX: {tx}", "wallet_trust_degraded": "Kepercayaan Perangkat Keras Menurun", "wallet_warning": "Peringatan", + "claim_node_no_signer": "Node Anda harus dijalankan terlebih dahulu sebelum dapat mengklaim node lain. Jalankan node Anda, lalu coba lagi.", "attestation_actions_desc": "Tindakan untuk catatan ini", "chat_badge_message": "PESAN", "chat_details": "jenis {type} · lingkup {scope} · status {status}", diff --git a/client/androidApp/src/main/assets/localization/it.json b/client/androidApp/src/main/assets/localization/it.json index 1748af9..38dd3e2 100644 --- a/client/androidApp/src/main/assets/localization/it.json +++ b/client/androidApp/src/main/assets/localization/it.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Trasferimento riuscito! TX: {tx}", "wallet_trust_degraded": "Fiducia Hardware Degradata", "wallet_warning": "Avviso", + "claim_node_no_signer": "Il tuo nodo deve essere avviato prima di poter rivendicare un altro nodo. Avvia il tuo nodo, quindi riprova.", "attestation_actions_desc": "Azioni per questo record", "chat_badge_message": "MESSAGGIO", "chat_details": "tipo {type} · ambito {scope} · stato {status}", diff --git a/client/androidApp/src/main/assets/localization/ja.json b/client/androidApp/src/main/assets/localization/ja.json index 07f3851..1dfa258 100644 --- a/client/androidApp/src/main/assets/localization/ja.json +++ b/client/androidApp/src/main/assets/localization/ja.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "送金成功!TX: {tx}", "wallet_trust_degraded": "ハードウェア信頼が低下しました", "wallet_warning": "警告", + "claim_node_no_signer": "他のノードの所有権を取得するには、まずお客様のノードを起動する必要があります。ノードを起動してから、もう一度お試しください。", "attestation_actions_desc": "このレコードに対する操作", "chat_badge_message": "メッセージ", "chat_details": "種別 {type} · スコープ {scope} · 状態 {status}", diff --git a/client/androidApp/src/main/assets/localization/ko.json b/client/androidApp/src/main/assets/localization/ko.json index a766a42..ac48476 100644 --- a/client/androidApp/src/main/assets/localization/ko.json +++ b/client/androidApp/src/main/assets/localization/ko.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "전송 성공! TX: {tx}", "wallet_trust_degraded": "하드웨어 신뢰 저하됨", "wallet_warning": "경고", + "claim_node_no_signer": "노드를 시작해야 다른 노드의 소유권을 주장할 수 있습니다. 노드를 시작한 후 다시 시도하세요.", "attestation_actions_desc": "이 레코드에 대한 작업", "chat_badge_message": "메시지", "chat_details": "유형 {type} · 범위 {scope} · 상태 {status}", diff --git a/client/androidApp/src/main/assets/localization/mr.json b/client/androidApp/src/main/assets/localization/mr.json index 0c7cf1f..2fa2e8f 100644 --- a/client/androidApp/src/main/assets/localization/mr.json +++ b/client/androidApp/src/main/assets/localization/mr.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer यशful! TX: {tx}", "wallet_trust_degraded": "हार्डवेअर विश्वास खालावला", "wallet_warning": "इशारा", + "claim_node_no_signer": "आणखी एका नोडचा दावा करण्यापूर्वी तुमचा नोड सुरू असणे आवश्यक आहे. तुमचा नोड सुरू करा आणि नंतर पुन्हा प्रयत्न करा.", "attestation_actions_desc": "या नोंदीसाठी क्रिया", "chat_badge_message": "संदेश", "chat_details": "प्रकार {type} · व्याप्ती {scope} · स्थिती {status}", diff --git a/client/androidApp/src/main/assets/localization/my.json b/client/androidApp/src/main/assets/localization/my.json index aa01418..72cc1a3 100644 --- a/client/androidApp/src/main/assets/localization/my.json +++ b/client/androidApp/src/main/assets/localization/my.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer အောင်မြင်ful! TX: {tx}", "wallet_trust_degraded": "Hardware ယုံကြည်မှု အားနည်းသွားပြီ", "wallet_warning": "သတိပေး", + "claim_node_no_signer": "အခြား Node ကို ပိုင်ဆိုင်မှုပြောရန် သင်၏ node ကို အရင်စတင်ထားရမည်။ သင်၏ node ကို စတင်ပြီး ထပ်မံကြိုးစားပါ။", "attestation_actions_desc": "ဤမှတ်တမ်းအတွက် လုပ်ဆောင်ချက်များ", "chat_badge_message": "မက်ဆေ့ချ်", "chat_details": "အမျိုးအစား {type} · scope {scope} · အခြေအနေ {status}", diff --git a/client/androidApp/src/main/assets/localization/pa.json b/client/androidApp/src/main/assets/localization/pa.json index ebf8145..da6082d 100644 --- a/client/androidApp/src/main/assets/localization/pa.json +++ b/client/androidApp/src/main/assets/localization/pa.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "ਟ੍ਰਾਂਸਫਰ ਸਫਲ! TX: {tx}", "wallet_trust_degraded": "ਹਾਰਡਵੇਅਰ ਭਰੋਸਾ ਘਟਿਆ", "wallet_warning": "ਚੇਤਾਵਨੀ", + "claim_node_no_signer": "ਕਿਸੇ ਹੋਰ ਨੋਡ ਦਾ ਦਾਅਵਾ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਤੁਹਾਡਾ ਨੋਡ ਸ਼ੁਰੂ ਹੋਣਾ ਲਾਜ਼ਮੀ ਹੈ। ਆਪਣਾ ਨੋਡ ਸ਼ੁਰੂ ਕਰੋ, ਫਿਰ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।", "attestation_actions_desc": "ਇਸ ਰਿਕਾਰਡ ਲਈ ਕਾਰਵਾਈਆਂ", "chat_badge_message": "ਸੁਨੇਹਾ", "chat_details": "ਕਿਸਮ {type} · ਦਾਇਰਾ {scope} · ਸਥਿਤੀ {status}", diff --git a/client/androidApp/src/main/assets/localization/pt.json b/client/androidApp/src/main/assets/localization/pt.json index fa5fa8c..d00c840 100644 --- a/client/androidApp/src/main/assets/localization/pt.json +++ b/client/androidApp/src/main/assets/localization/pt.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transferência bem-sucedida! TX: {tx}", "wallet_trust_degraded": "Confiança de Hardware Degradada", "wallet_warning": "Aviso", + "claim_node_no_signer": "Seu nó precisa ser iniciado antes que possa reivindicar outro nó. Inicie seu nó e tente novamente.", "attestation_actions_desc": "Ações para este registo", "chat_badge_message": "MENSAGEM", "chat_details": "tipo {type} · âmbito {scope} · estado {status}", diff --git a/client/androidApp/src/main/assets/localization/ru.json b/client/androidApp/src/main/assets/localization/ru.json index 484e28e..b4bbaeb 100644 --- a/client/androidApp/src/main/assets/localization/ru.json +++ b/client/androidApp/src/main/assets/localization/ru.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Перевод успешен! TX: {tx}", "wallet_trust_degraded": "Доверие к оборудованию ухудшено", "wallet_warning": "Предупреждение", + "claim_node_no_signer": "Ваш узел должен быть запущен, прежде чем он сможет заявить права на другой узел. Запустите узел и повторите попытку.", "attestation_actions_desc": "Действия для этой записи", "chat_badge_message": "СООБЩЕНИЕ", "chat_details": "тип {type} · область {scope} · статус {status}", diff --git a/client/androidApp/src/main/assets/localization/sw.json b/client/androidApp/src/main/assets/localization/sw.json index 522bfde..f05a8e1 100644 --- a/client/androidApp/src/main/assets/localization/sw.json +++ b/client/androidApp/src/main/assets/localization/sw.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Uhamisho umefanikiwa! TX: {tx}", "wallet_trust_degraded": "Kuaminika kwa Maunzi Kumedhoofika", "wallet_warning": "Onyo", + "claim_node_no_signer": "Nodi yako inahitaji kuanzishwa kabla ya kudai nodi nyingine. Anzisha nodi yako, kisha ujaribu tena.", "attestation_actions_desc": "Vitendo vya rekodi hii", "chat_badge_message": "UJUMBE", "chat_details": "aina {type} · wigo {scope} · hali {status}", diff --git a/client/androidApp/src/main/assets/localization/ta.json b/client/androidApp/src/main/assets/localization/ta.json index 34f5c0e..12dc310 100644 --- a/client/androidApp/src/main/assets/localization/ta.json +++ b/client/androidApp/src/main/assets/localization/ta.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "பரிமாற்றம் வெற்றிகரம்! TX: {tx}", "wallet_trust_degraded": "Hardware நம்பகத்தன்மை Degraded", "wallet_warning": "எச்சரிக்கை", + "claim_node_no_signer": "உங்கள் நோடு மற்றொரு நோடைக் கோரும் முன் தொடங்கப்பட்டிருக்க வேண்டும். உங்கள் நோடைத் தொடங்கி, மீண்டும் முயற்சிக்கவும்.", "attestation_actions_desc": "இந்தப் பதிவுக்கான செயல்கள்", "chat_badge_message": "செய்தி", "chat_details": "வகை {type} · எல்லை {scope} · நிலை {status}", diff --git a/client/androidApp/src/main/assets/localization/te.json b/client/androidApp/src/main/assets/localization/te.json index 78b6759..6a6cd1b 100644 --- a/client/androidApp/src/main/assets/localization/te.json +++ b/client/androidApp/src/main/assets/localization/te.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "బదిలీ విజయవంతమైంది! TX: {tx}", "wallet_trust_degraded": "హార్డ్‌వేర్ నమ్మకం తగ్గింది", "wallet_warning": "హెచ్చరిక", + "claim_node_no_signer": "మీ నోడ్ మరో నోడ్‌ను క్లెయిమ్ చేయడానికి ముందు ప్రారంభమై ఉండాలి. మీ నోడ్‌ను ప్రారంభించి, మళ్ళీ ప్రయత్నించండి.", "attestation_actions_desc": "ఈ రికార్డుకు చర్యలు", "chat_badge_message": "సందేశం", "chat_details": "రకం {type} · పరిధి {scope} · స్థితి {status}", diff --git a/client/androidApp/src/main/assets/localization/th.json b/client/androidApp/src/main/assets/localization/th.json index 78de09e..4cbc211 100644 --- a/client/androidApp/src/main/assets/localization/th.json +++ b/client/androidApp/src/main/assets/localization/th.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer สำเร็จful! TX: {tx}", "wallet_trust_degraded": "ความน่าเชื่อถือของฮาร์ดแวร์ลดลง", "wallet_warning": "คำเตือน", + "claim_node_no_signer": "โหนดของคุณต้องเริ่มทำงานก่อนจึงจะสามารถอ้างสิทธิ์โหนดอื่นได้ กรุณาเริ่มโหนดของคุณ แล้วลองอีกครั้ง", "attestation_actions_desc": "การดำเนินการสำหรับระเบียนนี้", "chat_badge_message": "ข้อความ", "chat_details": "ประเภท {type} · ขอบเขต {scope} · สถานะ {status}", diff --git a/client/androidApp/src/main/assets/localization/tr.json b/client/androidApp/src/main/assets/localization/tr.json index 9708b87..443adfc 100644 --- a/client/androidApp/src/main/assets/localization/tr.json +++ b/client/androidApp/src/main/assets/localization/tr.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer başarılı! TX: {tx}", "wallet_trust_degraded": "Donanım Güveni Düşmüş", "wallet_warning": "Uyarı", + "claim_node_no_signer": "Başka bir düğümü sahiplenmeden önce kendi düğümünüz başlatılmış olmalıdır. Düğümünüzü başlatın ve tekrar deneyin.", "attestation_actions_desc": "Bu kayıt için eylemler", "chat_badge_message": "MESAJ", "chat_details": "tür {type} · kapsam {scope} · durum {status}", diff --git a/client/androidApp/src/main/assets/localization/uk.json b/client/androidApp/src/main/assets/localization/uk.json index fafcb63..a3ebdd0 100644 --- a/client/androidApp/src/main/assets/localization/uk.json +++ b/client/androidApp/src/main/assets/localization/uk.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Переказ успішний! TX: {tx}", "wallet_trust_degraded": "Апаратну довіру знижено", "wallet_warning": "Попередження", + "claim_node_no_signer": "Ваш вузол потрібно запустити, перш ніж він зможе заявити право на інший вузол. Запустіть свій вузол і спробуйте ще раз.", "attestation_actions_desc": "Дії для цього запису", "chat_badge_message": "ПОВІДОМЛЕННЯ", "chat_details": "тип {type} · область {scope} · статус {status}", diff --git a/client/androidApp/src/main/assets/localization/ur.json b/client/androidApp/src/main/assets/localization/ur.json index 7c17d31..39b48e6 100644 --- a/client/androidApp/src/main/assets/localization/ur.json +++ b/client/androidApp/src/main/assets/localization/ur.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "ٹرانسفر کامیاب! TX: {tx}", "wallet_trust_degraded": "ہارڈ ویئر اعتماد کمزور پڑ گیا", "wallet_warning": "انتباہ", + "claim_node_no_signer": "کسی اور نوڈ کا دعویٰ کرنے سے پہلے آپ کا نوڈ شروع ہونا ضروری ہے۔ اپنا نوڈ شروع کریں، پھر دوبارہ کوشش کریں۔", "attestation_actions_desc": "اس ریکارڈ کے لیے اقدامات", "chat_badge_message": "پیغام", "chat_details": "قسم {type} · دائرہ {scope} · حیثیت {status}", diff --git a/client/androidApp/src/main/assets/localization/vi.json b/client/androidApp/src/main/assets/localization/vi.json index 245869a..018b388 100644 --- a/client/androidApp/src/main/assets/localization/vi.json +++ b/client/androidApp/src/main/assets/localization/vi.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Chuyển khoản thành công! TX: {tx}", "wallet_trust_degraded": "Tin tưởng Suy giảm", "wallet_warning": "Cảnh báo", + "claim_node_no_signer": "Nút của bạn phải được khởi động trước khi có thể nhận quyền một nút khác. Hãy khởi động nút của bạn, rồi thử lại.", "attestation_actions_desc": "Các thao tác cho bản ghi này", "chat_badge_message": "TIN NHẮN", "chat_details": "loại {type} · phạm vi {scope} · trạng thái {status}", diff --git a/client/androidApp/src/main/assets/localization/yo.json b/client/androidApp/src/main/assets/localization/yo.json index d7f8400..0ebc93f 100644 --- a/client/androidApp/src/main/assets/localization/yo.json +++ b/client/androidApp/src/main/assets/localization/yo.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Gbígbé ṣe àṣeyọrí! TX: {tx}", "wallet_trust_degraded": "Ìgbẹ́kẹ̀lé Ohun Ẹ̀rọ Ti Dín Kù", "wallet_warning": "Ìkìlọ̀", + "claim_node_no_signer": "Gbọ́dọ̀ bẹ̀rẹ̀ node rẹ kí ó tó lè gba node mìíràn. Bẹ̀rẹ̀ node rẹ, lẹ́yìn náà gbìyànjú lẹ́ẹ̀kan sí i.", "attestation_actions_desc": "Àwọn ìgbésẹ̀ fún àkọsílẹ̀ yìí", "chat_badge_message": "ÌFIRÁNṢẸ́", "chat_details": "irú {type} · ààlà {scope} · ipò {status}", diff --git a/client/androidApp/src/main/assets/localization/zh.json b/client/androidApp/src/main/assets/localization/zh.json index 2bcede3..060d438 100644 --- a/client/androidApp/src/main/assets/localization/zh.json +++ b/client/androidApp/src/main/assets/localization/zh.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "转账成功!TX: {tx}", "wallet_trust_degraded": "硬件信任降级", "wallet_warning": "警告", + "claim_node_no_signer": "您的节点必须先启动才能认领另一个节点。请启动您的节点,然后重试。", "attestation_actions_desc": "此记录的操作", "chat_badge_message": "消息", "chat_details": "类型 {type} · 范围 {scope} · 状态 {status}", diff --git a/client/desktopApp/src/main/resources/localization/am.json b/client/desktopApp/src/main/resources/localization/am.json index b478e20..55d8b26 100644 --- a/client/desktopApp/src/main/resources/localization/am.json +++ b/client/desktopApp/src/main/resources/localization/am.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "ማስተላለፍ ተሳክቷል! TX: {tx}", "wallet_trust_degraded": "ስልክ ሥቅ ተዳክሞ ነበር", "wallet_warning": "ማስጠንቀቂያ", + "claim_node_no_signer": "የእርስዎ ኖድ ሌላ ኖድ ከመያዙ በፊት መጀመር አለበት። ኖድዎን ይጀምሩ፣ ከዚያ እንደገና ይሞክሩ።", "attestation_actions_desc": "ለዚህ መዝገብ ያሉ እርምጃዎች", "chat_badge_message": "መልዕክት", "chat_details": "ዓይነት {type} · ወሰን {scope} · ሁኔታ {status}", diff --git a/client/desktopApp/src/main/resources/localization/ar.json b/client/desktopApp/src/main/resources/localization/ar.json index 3b025d4..31d7e78 100644 --- a/client/desktopApp/src/main/resources/localization/ar.json +++ b/client/desktopApp/src/main/resources/localization/ar.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "نجح التحويل! TX: {tx}", "wallet_trust_degraded": "تدهورت ثقة الأجهزة", "wallet_warning": "تحذير", + "claim_node_no_signer": "يجب تشغيل عقدتك قبل أن تتمكن من المطالبة بعقدة أخرى. شغّل عقدتك، ثم حاول مرة أخرى.", "attestation_actions_desc": "إجراءات هذا السجل", "chat_badge_message": "رسالة", "chat_details": "النوع {type} · النطاق {scope} · الحالة {status}", diff --git a/client/desktopApp/src/main/resources/localization/bn.json b/client/desktopApp/src/main/resources/localization/bn.json index 30f1052..744a4b8 100644 --- a/client/desktopApp/src/main/resources/localization/bn.json +++ b/client/desktopApp/src/main/resources/localization/bn.json @@ -2925,6 +2925,7 @@ "wallet_transfer_success": "ট্রান্সফার সফল! TX: {tx}", "wallet_trust_degraded": "হার্ডওয়্যার বিশ্বাস হ্রাস পেয়েছে", "wallet_warning": "সতর্কতা", + "claim_node_no_signer": "আরেকটি নোড দাবি করার আগে আপনার নোড চালু থাকতে হবে। আপনার নোড চালু করুন, তারপর আবার চেষ্টা করুন।", "attestation_actions_desc": "এই রেকর্ডের জন্য পদক্ষেপ", "chat_badge_message": "বার্তা", "chat_details": "ধরন {type} · পরিসর {scope} · অবস্থা {status}", diff --git a/client/desktopApp/src/main/resources/localization/de.json b/client/desktopApp/src/main/resources/localization/de.json index 74e684c..1bb19cb 100644 --- a/client/desktopApp/src/main/resources/localization/de.json +++ b/client/desktopApp/src/main/resources/localization/de.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Überweisung erfolgreich! TX: {tx}", "wallet_trust_degraded": "Hardware-Vertrauen herabgestuft", "wallet_warning": "Warnung", + "claim_node_no_signer": "Ihr Node muss gestartet sein, bevor er einen anderen Knoten übernehmen kann. Starten Sie Ihren Node und versuchen Sie es erneut.", "attestation_actions_desc": "Aktionen für diesen Datensatz", "chat_badge_message": "NACHRICHT", "chat_details": "Typ {type} · Bereich {scope} · Status {status}", diff --git a/client/desktopApp/src/main/resources/localization/es.json b/client/desktopApp/src/main/resources/localization/es.json index 66a6762..6b600c2 100644 --- a/client/desktopApp/src/main/resources/localization/es.json +++ b/client/desktopApp/src/main/resources/localization/es.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "¡Transferencia exitosa! TX: {tx}", "wallet_trust_degraded": "Confianza de Hardware Degradada", "wallet_warning": "Advertencia", + "claim_node_no_signer": "Tu nodo debe estar iniciado antes de poder reclamar otro nodo. Inicia tu nodo e inténtalo de nuevo.", "attestation_actions_desc": "Acciones para este registro", "chat_badge_message": "MENSAJE", "chat_details": "tipo {type} · ámbito {scope} · estado {status}", diff --git a/client/desktopApp/src/main/resources/localization/fa.json b/client/desktopApp/src/main/resources/localization/fa.json index d9caea0..0fda01b 100644 --- a/client/desktopApp/src/main/resources/localization/fa.json +++ b/client/desktopApp/src/main/resources/localization/fa.json @@ -2930,6 +2930,7 @@ "wallet_transfer_success": "انتقال با موفقیت انجام شد! TX: {tx}", "wallet_trust_degraded": "افت اعتماد سخت‌افزاری", "wallet_warning": "هشدار", + "claim_node_no_signer": "گرهٔ شما باید پیش از ادعای گره‌ای دیگر راه‌اندازی شده باشد. گرهٔ خود را راه‌اندازی کنید و دوباره تلاش کنید.", "attestation_actions_desc": "اقدامات برای این رکورد", "chat_badge_message": "پیام", "chat_details": "نوع {type} · محدوده {scope} · وضعیت {status}", diff --git a/client/desktopApp/src/main/resources/localization/fr.json b/client/desktopApp/src/main/resources/localization/fr.json index 10abf12..5716835 100644 --- a/client/desktopApp/src/main/resources/localization/fr.json +++ b/client/desktopApp/src/main/resources/localization/fr.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfert réussi ! TX: {tx}", "wallet_trust_degraded": "Confiance Matérielle Dégradée", "wallet_warning": "Avertissement", + "claim_node_no_signer": "Votre nœud doit être démarré avant de pouvoir revendiquer un autre nœud. Démarrez votre nœud, puis réessayez.", "attestation_actions_desc": "Actions pour cet enregistrement", "chat_badge_message": "MESSAGE", "chat_details": "type {type} · portée {scope} · statut {status}", diff --git a/client/desktopApp/src/main/resources/localization/ha.json b/client/desktopApp/src/main/resources/localization/ha.json index 7ac4491..79baae8 100644 --- a/client/desktopApp/src/main/resources/localization/ha.json +++ b/client/desktopApp/src/main/resources/localization/ha.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Aika ya yi nasara! TX: {tx}", "wallet_trust_degraded": "Tabbas Kayan Aiki Ya Rage", "wallet_warning": "Gargaɗi", + "claim_node_no_signer": "Dole ne a fara kumburinka kafin ya iya karɓar wani kumburi. Fara kumburinka, sannan ka sake gwadawa.", "attestation_actions_desc": "Ayyuka don wannan rikodin", "chat_badge_message": "SAƘO", "chat_details": "nau'i {type} · iyaka {scope} · matsayi {status}", diff --git a/client/desktopApp/src/main/resources/localization/hi.json b/client/desktopApp/src/main/resources/localization/hi.json index 7654f45..d483015 100644 --- a/client/desktopApp/src/main/resources/localization/hi.json +++ b/client/desktopApp/src/main/resources/localization/hi.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "स्थानांतरण सफल! TX: {tx}", "wallet_trust_degraded": "हार्डवेयर ट्रस्ट कम हुआ", "wallet_warning": "चेतावनी", + "claim_node_no_signer": "किसी अन्य नोड का दावा करने से पहले आपका नोड शुरू होना चाहिए। अपना नोड शुरू करें, फिर दोबारा प्रयास करें।", "attestation_actions_desc": "इस रिकॉर्ड के लिए कार्रवाइयाँ", "chat_badge_message": "संदेश", "chat_details": "प्रकार {type} · दायरा {scope} · स्थिति {status}", diff --git a/client/desktopApp/src/main/resources/localization/id.json b/client/desktopApp/src/main/resources/localization/id.json index 809bf6a..aa8a9af 100644 --- a/client/desktopApp/src/main/resources/localization/id.json +++ b/client/desktopApp/src/main/resources/localization/id.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer berhasil! TX: {tx}", "wallet_trust_degraded": "Kepercayaan Perangkat Keras Menurun", "wallet_warning": "Peringatan", + "claim_node_no_signer": "Node Anda harus dijalankan terlebih dahulu sebelum dapat mengklaim node lain. Jalankan node Anda, lalu coba lagi.", "attestation_actions_desc": "Tindakan untuk catatan ini", "chat_badge_message": "PESAN", "chat_details": "jenis {type} · lingkup {scope} · status {status}", diff --git a/client/desktopApp/src/main/resources/localization/it.json b/client/desktopApp/src/main/resources/localization/it.json index 1748af9..38dd3e2 100644 --- a/client/desktopApp/src/main/resources/localization/it.json +++ b/client/desktopApp/src/main/resources/localization/it.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Trasferimento riuscito! TX: {tx}", "wallet_trust_degraded": "Fiducia Hardware Degradata", "wallet_warning": "Avviso", + "claim_node_no_signer": "Il tuo nodo deve essere avviato prima di poter rivendicare un altro nodo. Avvia il tuo nodo, quindi riprova.", "attestation_actions_desc": "Azioni per questo record", "chat_badge_message": "MESSAGGIO", "chat_details": "tipo {type} · ambito {scope} · stato {status}", diff --git a/client/desktopApp/src/main/resources/localization/ja.json b/client/desktopApp/src/main/resources/localization/ja.json index 07f3851..1dfa258 100644 --- a/client/desktopApp/src/main/resources/localization/ja.json +++ b/client/desktopApp/src/main/resources/localization/ja.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "送金成功!TX: {tx}", "wallet_trust_degraded": "ハードウェア信頼が低下しました", "wallet_warning": "警告", + "claim_node_no_signer": "他のノードの所有権を取得するには、まずお客様のノードを起動する必要があります。ノードを起動してから、もう一度お試しください。", "attestation_actions_desc": "このレコードに対する操作", "chat_badge_message": "メッセージ", "chat_details": "種別 {type} · スコープ {scope} · 状態 {status}", diff --git a/client/desktopApp/src/main/resources/localization/ko.json b/client/desktopApp/src/main/resources/localization/ko.json index a766a42..ac48476 100644 --- a/client/desktopApp/src/main/resources/localization/ko.json +++ b/client/desktopApp/src/main/resources/localization/ko.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "전송 성공! TX: {tx}", "wallet_trust_degraded": "하드웨어 신뢰 저하됨", "wallet_warning": "경고", + "claim_node_no_signer": "노드를 시작해야 다른 노드의 소유권을 주장할 수 있습니다. 노드를 시작한 후 다시 시도하세요.", "attestation_actions_desc": "이 레코드에 대한 작업", "chat_badge_message": "메시지", "chat_details": "유형 {type} · 범위 {scope} · 상태 {status}", diff --git a/client/desktopApp/src/main/resources/localization/mr.json b/client/desktopApp/src/main/resources/localization/mr.json index 0c7cf1f..2fa2e8f 100644 --- a/client/desktopApp/src/main/resources/localization/mr.json +++ b/client/desktopApp/src/main/resources/localization/mr.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer यशful! TX: {tx}", "wallet_trust_degraded": "हार्डवेअर विश्वास खालावला", "wallet_warning": "इशारा", + "claim_node_no_signer": "आणखी एका नोडचा दावा करण्यापूर्वी तुमचा नोड सुरू असणे आवश्यक आहे. तुमचा नोड सुरू करा आणि नंतर पुन्हा प्रयत्न करा.", "attestation_actions_desc": "या नोंदीसाठी क्रिया", "chat_badge_message": "संदेश", "chat_details": "प्रकार {type} · व्याप्ती {scope} · स्थिती {status}", diff --git a/client/desktopApp/src/main/resources/localization/my.json b/client/desktopApp/src/main/resources/localization/my.json index aa01418..72cc1a3 100644 --- a/client/desktopApp/src/main/resources/localization/my.json +++ b/client/desktopApp/src/main/resources/localization/my.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer အောင်မြင်ful! TX: {tx}", "wallet_trust_degraded": "Hardware ယုံကြည်မှု အားနည်းသွားပြီ", "wallet_warning": "သတိပေး", + "claim_node_no_signer": "အခြား Node ကို ပိုင်ဆိုင်မှုပြောရန် သင်၏ node ကို အရင်စတင်ထားရမည်။ သင်၏ node ကို စတင်ပြီး ထပ်မံကြိုးစားပါ။", "attestation_actions_desc": "ဤမှတ်တမ်းအတွက် လုပ်ဆောင်ချက်များ", "chat_badge_message": "မက်ဆေ့ချ်", "chat_details": "အမျိုးအစား {type} · scope {scope} · အခြေအနေ {status}", diff --git a/client/desktopApp/src/main/resources/localization/pa.json b/client/desktopApp/src/main/resources/localization/pa.json index ebf8145..da6082d 100644 --- a/client/desktopApp/src/main/resources/localization/pa.json +++ b/client/desktopApp/src/main/resources/localization/pa.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "ਟ੍ਰਾਂਸਫਰ ਸਫਲ! TX: {tx}", "wallet_trust_degraded": "ਹਾਰਡਵੇਅਰ ਭਰੋਸਾ ਘਟਿਆ", "wallet_warning": "ਚੇਤਾਵਨੀ", + "claim_node_no_signer": "ਕਿਸੇ ਹੋਰ ਨੋਡ ਦਾ ਦਾਅਵਾ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਤੁਹਾਡਾ ਨੋਡ ਸ਼ੁਰੂ ਹੋਣਾ ਲਾਜ਼ਮੀ ਹੈ। ਆਪਣਾ ਨੋਡ ਸ਼ੁਰੂ ਕਰੋ, ਫਿਰ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।", "attestation_actions_desc": "ਇਸ ਰਿਕਾਰਡ ਲਈ ਕਾਰਵਾਈਆਂ", "chat_badge_message": "ਸੁਨੇਹਾ", "chat_details": "ਕਿਸਮ {type} · ਦਾਇਰਾ {scope} · ਸਥਿਤੀ {status}", diff --git a/client/desktopApp/src/main/resources/localization/pt.json b/client/desktopApp/src/main/resources/localization/pt.json index fa5fa8c..d00c840 100644 --- a/client/desktopApp/src/main/resources/localization/pt.json +++ b/client/desktopApp/src/main/resources/localization/pt.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transferência bem-sucedida! TX: {tx}", "wallet_trust_degraded": "Confiança de Hardware Degradada", "wallet_warning": "Aviso", + "claim_node_no_signer": "Seu nó precisa ser iniciado antes que possa reivindicar outro nó. Inicie seu nó e tente novamente.", "attestation_actions_desc": "Ações para este registo", "chat_badge_message": "MENSAGEM", "chat_details": "tipo {type} · âmbito {scope} · estado {status}", diff --git a/client/desktopApp/src/main/resources/localization/ru.json b/client/desktopApp/src/main/resources/localization/ru.json index 484e28e..b4bbaeb 100644 --- a/client/desktopApp/src/main/resources/localization/ru.json +++ b/client/desktopApp/src/main/resources/localization/ru.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Перевод успешен! TX: {tx}", "wallet_trust_degraded": "Доверие к оборудованию ухудшено", "wallet_warning": "Предупреждение", + "claim_node_no_signer": "Ваш узел должен быть запущен, прежде чем он сможет заявить права на другой узел. Запустите узел и повторите попытку.", "attestation_actions_desc": "Действия для этой записи", "chat_badge_message": "СООБЩЕНИЕ", "chat_details": "тип {type} · область {scope} · статус {status}", diff --git a/client/desktopApp/src/main/resources/localization/sw.json b/client/desktopApp/src/main/resources/localization/sw.json index 522bfde..f05a8e1 100644 --- a/client/desktopApp/src/main/resources/localization/sw.json +++ b/client/desktopApp/src/main/resources/localization/sw.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Uhamisho umefanikiwa! TX: {tx}", "wallet_trust_degraded": "Kuaminika kwa Maunzi Kumedhoofika", "wallet_warning": "Onyo", + "claim_node_no_signer": "Nodi yako inahitaji kuanzishwa kabla ya kudai nodi nyingine. Anzisha nodi yako, kisha ujaribu tena.", "attestation_actions_desc": "Vitendo vya rekodi hii", "chat_badge_message": "UJUMBE", "chat_details": "aina {type} · wigo {scope} · hali {status}", diff --git a/client/desktopApp/src/main/resources/localization/ta.json b/client/desktopApp/src/main/resources/localization/ta.json index 34f5c0e..12dc310 100644 --- a/client/desktopApp/src/main/resources/localization/ta.json +++ b/client/desktopApp/src/main/resources/localization/ta.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "பரிமாற்றம் வெற்றிகரம்! TX: {tx}", "wallet_trust_degraded": "Hardware நம்பகத்தன்மை Degraded", "wallet_warning": "எச்சரிக்கை", + "claim_node_no_signer": "உங்கள் நோடு மற்றொரு நோடைக் கோரும் முன் தொடங்கப்பட்டிருக்க வேண்டும். உங்கள் நோடைத் தொடங்கி, மீண்டும் முயற்சிக்கவும்.", "attestation_actions_desc": "இந்தப் பதிவுக்கான செயல்கள்", "chat_badge_message": "செய்தி", "chat_details": "வகை {type} · எல்லை {scope} · நிலை {status}", diff --git a/client/desktopApp/src/main/resources/localization/te.json b/client/desktopApp/src/main/resources/localization/te.json index 78b6759..6a6cd1b 100644 --- a/client/desktopApp/src/main/resources/localization/te.json +++ b/client/desktopApp/src/main/resources/localization/te.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "బదిలీ విజయవంతమైంది! TX: {tx}", "wallet_trust_degraded": "హార్డ్‌వేర్ నమ్మకం తగ్గింది", "wallet_warning": "హెచ్చరిక", + "claim_node_no_signer": "మీ నోడ్ మరో నోడ్‌ను క్లెయిమ్ చేయడానికి ముందు ప్రారంభమై ఉండాలి. మీ నోడ్‌ను ప్రారంభించి, మళ్ళీ ప్రయత్నించండి.", "attestation_actions_desc": "ఈ రికార్డుకు చర్యలు", "chat_badge_message": "సందేశం", "chat_details": "రకం {type} · పరిధి {scope} · స్థితి {status}", diff --git a/client/desktopApp/src/main/resources/localization/th.json b/client/desktopApp/src/main/resources/localization/th.json index 78de09e..4cbc211 100644 --- a/client/desktopApp/src/main/resources/localization/th.json +++ b/client/desktopApp/src/main/resources/localization/th.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer สำเร็จful! TX: {tx}", "wallet_trust_degraded": "ความน่าเชื่อถือของฮาร์ดแวร์ลดลง", "wallet_warning": "คำเตือน", + "claim_node_no_signer": "โหนดของคุณต้องเริ่มทำงานก่อนจึงจะสามารถอ้างสิทธิ์โหนดอื่นได้ กรุณาเริ่มโหนดของคุณ แล้วลองอีกครั้ง", "attestation_actions_desc": "การดำเนินการสำหรับระเบียนนี้", "chat_badge_message": "ข้อความ", "chat_details": "ประเภท {type} · ขอบเขต {scope} · สถานะ {status}", diff --git a/client/desktopApp/src/main/resources/localization/tr.json b/client/desktopApp/src/main/resources/localization/tr.json index 9708b87..443adfc 100644 --- a/client/desktopApp/src/main/resources/localization/tr.json +++ b/client/desktopApp/src/main/resources/localization/tr.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer başarılı! TX: {tx}", "wallet_trust_degraded": "Donanım Güveni Düşmüş", "wallet_warning": "Uyarı", + "claim_node_no_signer": "Başka bir düğümü sahiplenmeden önce kendi düğümünüz başlatılmış olmalıdır. Düğümünüzü başlatın ve tekrar deneyin.", "attestation_actions_desc": "Bu kayıt için eylemler", "chat_badge_message": "MESAJ", "chat_details": "tür {type} · kapsam {scope} · durum {status}", diff --git a/client/desktopApp/src/main/resources/localization/uk.json b/client/desktopApp/src/main/resources/localization/uk.json index fafcb63..a3ebdd0 100644 --- a/client/desktopApp/src/main/resources/localization/uk.json +++ b/client/desktopApp/src/main/resources/localization/uk.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Переказ успішний! TX: {tx}", "wallet_trust_degraded": "Апаратну довіру знижено", "wallet_warning": "Попередження", + "claim_node_no_signer": "Ваш вузол потрібно запустити, перш ніж він зможе заявити право на інший вузол. Запустіть свій вузол і спробуйте ще раз.", "attestation_actions_desc": "Дії для цього запису", "chat_badge_message": "ПОВІДОМЛЕННЯ", "chat_details": "тип {type} · область {scope} · статус {status}", diff --git a/client/desktopApp/src/main/resources/localization/ur.json b/client/desktopApp/src/main/resources/localization/ur.json index 7c17d31..39b48e6 100644 --- a/client/desktopApp/src/main/resources/localization/ur.json +++ b/client/desktopApp/src/main/resources/localization/ur.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "ٹرانسفر کامیاب! TX: {tx}", "wallet_trust_degraded": "ہارڈ ویئر اعتماد کمزور پڑ گیا", "wallet_warning": "انتباہ", + "claim_node_no_signer": "کسی اور نوڈ کا دعویٰ کرنے سے پہلے آپ کا نوڈ شروع ہونا ضروری ہے۔ اپنا نوڈ شروع کریں، پھر دوبارہ کوشش کریں۔", "attestation_actions_desc": "اس ریکارڈ کے لیے اقدامات", "chat_badge_message": "پیغام", "chat_details": "قسم {type} · دائرہ {scope} · حیثیت {status}", diff --git a/client/desktopApp/src/main/resources/localization/vi.json b/client/desktopApp/src/main/resources/localization/vi.json index 245869a..018b388 100644 --- a/client/desktopApp/src/main/resources/localization/vi.json +++ b/client/desktopApp/src/main/resources/localization/vi.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Chuyển khoản thành công! TX: {tx}", "wallet_trust_degraded": "Tin tưởng Suy giảm", "wallet_warning": "Cảnh báo", + "claim_node_no_signer": "Nút của bạn phải được khởi động trước khi có thể nhận quyền một nút khác. Hãy khởi động nút của bạn, rồi thử lại.", "attestation_actions_desc": "Các thao tác cho bản ghi này", "chat_badge_message": "TIN NHẮN", "chat_details": "loại {type} · phạm vi {scope} · trạng thái {status}", diff --git a/client/desktopApp/src/main/resources/localization/yo.json b/client/desktopApp/src/main/resources/localization/yo.json index d7f8400..0ebc93f 100644 --- a/client/desktopApp/src/main/resources/localization/yo.json +++ b/client/desktopApp/src/main/resources/localization/yo.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Gbígbé ṣe àṣeyọrí! TX: {tx}", "wallet_trust_degraded": "Ìgbẹ́kẹ̀lé Ohun Ẹ̀rọ Ti Dín Kù", "wallet_warning": "Ìkìlọ̀", + "claim_node_no_signer": "Gbọ́dọ̀ bẹ̀rẹ̀ node rẹ kí ó tó lè gba node mìíràn. Bẹ̀rẹ̀ node rẹ, lẹ́yìn náà gbìyànjú lẹ́ẹ̀kan sí i.", "attestation_actions_desc": "Àwọn ìgbésẹ̀ fún àkọsílẹ̀ yìí", "chat_badge_message": "ÌFIRÁNṢẸ́", "chat_details": "irú {type} · ààlà {scope} · ipò {status}", diff --git a/client/desktopApp/src/main/resources/localization/zh.json b/client/desktopApp/src/main/resources/localization/zh.json index 2bcede3..060d438 100644 --- a/client/desktopApp/src/main/resources/localization/zh.json +++ b/client/desktopApp/src/main/resources/localization/zh.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "转账成功!TX: {tx}", "wallet_trust_degraded": "硬件信任降级", "wallet_warning": "警告", + "claim_node_no_signer": "您的节点必须先启动才能认领另一个节点。请启动您的节点,然后重试。", "attestation_actions_desc": "此记录的操作", "chat_badge_message": "消息", "chat_details": "类型 {type} · 范围 {scope} · 状态 {status}", diff --git a/client/iosApp/iosApp/localization/am.json b/client/iosApp/iosApp/localization/am.json index b478e20..55d8b26 100644 --- a/client/iosApp/iosApp/localization/am.json +++ b/client/iosApp/iosApp/localization/am.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "ማስተላለፍ ተሳክቷል! TX: {tx}", "wallet_trust_degraded": "ስልክ ሥቅ ተዳክሞ ነበር", "wallet_warning": "ማስጠንቀቂያ", + "claim_node_no_signer": "የእርስዎ ኖድ ሌላ ኖድ ከመያዙ በፊት መጀመር አለበት። ኖድዎን ይጀምሩ፣ ከዚያ እንደገና ይሞክሩ።", "attestation_actions_desc": "ለዚህ መዝገብ ያሉ እርምጃዎች", "chat_badge_message": "መልዕክት", "chat_details": "ዓይነት {type} · ወሰን {scope} · ሁኔታ {status}", diff --git a/client/iosApp/iosApp/localization/ar.json b/client/iosApp/iosApp/localization/ar.json index 3b025d4..31d7e78 100644 --- a/client/iosApp/iosApp/localization/ar.json +++ b/client/iosApp/iosApp/localization/ar.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "نجح التحويل! TX: {tx}", "wallet_trust_degraded": "تدهورت ثقة الأجهزة", "wallet_warning": "تحذير", + "claim_node_no_signer": "يجب تشغيل عقدتك قبل أن تتمكن من المطالبة بعقدة أخرى. شغّل عقدتك، ثم حاول مرة أخرى.", "attestation_actions_desc": "إجراءات هذا السجل", "chat_badge_message": "رسالة", "chat_details": "النوع {type} · النطاق {scope} · الحالة {status}", diff --git a/client/iosApp/iosApp/localization/bn.json b/client/iosApp/iosApp/localization/bn.json index 30f1052..744a4b8 100644 --- a/client/iosApp/iosApp/localization/bn.json +++ b/client/iosApp/iosApp/localization/bn.json @@ -2925,6 +2925,7 @@ "wallet_transfer_success": "ট্রান্সফার সফল! TX: {tx}", "wallet_trust_degraded": "হার্ডওয়্যার বিশ্বাস হ্রাস পেয়েছে", "wallet_warning": "সতর্কতা", + "claim_node_no_signer": "আরেকটি নোড দাবি করার আগে আপনার নোড চালু থাকতে হবে। আপনার নোড চালু করুন, তারপর আবার চেষ্টা করুন।", "attestation_actions_desc": "এই রেকর্ডের জন্য পদক্ষেপ", "chat_badge_message": "বার্তা", "chat_details": "ধরন {type} · পরিসর {scope} · অবস্থা {status}", diff --git a/client/iosApp/iosApp/localization/de.json b/client/iosApp/iosApp/localization/de.json index 74e684c..1bb19cb 100644 --- a/client/iosApp/iosApp/localization/de.json +++ b/client/iosApp/iosApp/localization/de.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Überweisung erfolgreich! TX: {tx}", "wallet_trust_degraded": "Hardware-Vertrauen herabgestuft", "wallet_warning": "Warnung", + "claim_node_no_signer": "Ihr Node muss gestartet sein, bevor er einen anderen Knoten übernehmen kann. Starten Sie Ihren Node und versuchen Sie es erneut.", "attestation_actions_desc": "Aktionen für diesen Datensatz", "chat_badge_message": "NACHRICHT", "chat_details": "Typ {type} · Bereich {scope} · Status {status}", diff --git a/client/iosApp/iosApp/localization/es.json b/client/iosApp/iosApp/localization/es.json index 66a6762..6b600c2 100644 --- a/client/iosApp/iosApp/localization/es.json +++ b/client/iosApp/iosApp/localization/es.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "¡Transferencia exitosa! TX: {tx}", "wallet_trust_degraded": "Confianza de Hardware Degradada", "wallet_warning": "Advertencia", + "claim_node_no_signer": "Tu nodo debe estar iniciado antes de poder reclamar otro nodo. Inicia tu nodo e inténtalo de nuevo.", "attestation_actions_desc": "Acciones para este registro", "chat_badge_message": "MENSAJE", "chat_details": "tipo {type} · ámbito {scope} · estado {status}", diff --git a/client/iosApp/iosApp/localization/fa.json b/client/iosApp/iosApp/localization/fa.json index d9caea0..0fda01b 100644 --- a/client/iosApp/iosApp/localization/fa.json +++ b/client/iosApp/iosApp/localization/fa.json @@ -2930,6 +2930,7 @@ "wallet_transfer_success": "انتقال با موفقیت انجام شد! TX: {tx}", "wallet_trust_degraded": "افت اعتماد سخت‌افزاری", "wallet_warning": "هشدار", + "claim_node_no_signer": "گرهٔ شما باید پیش از ادعای گره‌ای دیگر راه‌اندازی شده باشد. گرهٔ خود را راه‌اندازی کنید و دوباره تلاش کنید.", "attestation_actions_desc": "اقدامات برای این رکورد", "chat_badge_message": "پیام", "chat_details": "نوع {type} · محدوده {scope} · وضعیت {status}", diff --git a/client/iosApp/iosApp/localization/fr.json b/client/iosApp/iosApp/localization/fr.json index 10abf12..5716835 100644 --- a/client/iosApp/iosApp/localization/fr.json +++ b/client/iosApp/iosApp/localization/fr.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfert réussi ! TX: {tx}", "wallet_trust_degraded": "Confiance Matérielle Dégradée", "wallet_warning": "Avertissement", + "claim_node_no_signer": "Votre nœud doit être démarré avant de pouvoir revendiquer un autre nœud. Démarrez votre nœud, puis réessayez.", "attestation_actions_desc": "Actions pour cet enregistrement", "chat_badge_message": "MESSAGE", "chat_details": "type {type} · portée {scope} · statut {status}", diff --git a/client/iosApp/iosApp/localization/ha.json b/client/iosApp/iosApp/localization/ha.json index 7ac4491..79baae8 100644 --- a/client/iosApp/iosApp/localization/ha.json +++ b/client/iosApp/iosApp/localization/ha.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Aika ya yi nasara! TX: {tx}", "wallet_trust_degraded": "Tabbas Kayan Aiki Ya Rage", "wallet_warning": "Gargaɗi", + "claim_node_no_signer": "Dole ne a fara kumburinka kafin ya iya karɓar wani kumburi. Fara kumburinka, sannan ka sake gwadawa.", "attestation_actions_desc": "Ayyuka don wannan rikodin", "chat_badge_message": "SAƘO", "chat_details": "nau'i {type} · iyaka {scope} · matsayi {status}", diff --git a/client/iosApp/iosApp/localization/hi.json b/client/iosApp/iosApp/localization/hi.json index 7654f45..d483015 100644 --- a/client/iosApp/iosApp/localization/hi.json +++ b/client/iosApp/iosApp/localization/hi.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "स्थानांतरण सफल! TX: {tx}", "wallet_trust_degraded": "हार्डवेयर ट्रस्ट कम हुआ", "wallet_warning": "चेतावनी", + "claim_node_no_signer": "किसी अन्य नोड का दावा करने से पहले आपका नोड शुरू होना चाहिए। अपना नोड शुरू करें, फिर दोबारा प्रयास करें।", "attestation_actions_desc": "इस रिकॉर्ड के लिए कार्रवाइयाँ", "chat_badge_message": "संदेश", "chat_details": "प्रकार {type} · दायरा {scope} · स्थिति {status}", diff --git a/client/iosApp/iosApp/localization/id.json b/client/iosApp/iosApp/localization/id.json index 809bf6a..aa8a9af 100644 --- a/client/iosApp/iosApp/localization/id.json +++ b/client/iosApp/iosApp/localization/id.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer berhasil! TX: {tx}", "wallet_trust_degraded": "Kepercayaan Perangkat Keras Menurun", "wallet_warning": "Peringatan", + "claim_node_no_signer": "Node Anda harus dijalankan terlebih dahulu sebelum dapat mengklaim node lain. Jalankan node Anda, lalu coba lagi.", "attestation_actions_desc": "Tindakan untuk catatan ini", "chat_badge_message": "PESAN", "chat_details": "jenis {type} · lingkup {scope} · status {status}", diff --git a/client/iosApp/iosApp/localization/it.json b/client/iosApp/iosApp/localization/it.json index 1748af9..38dd3e2 100644 --- a/client/iosApp/iosApp/localization/it.json +++ b/client/iosApp/iosApp/localization/it.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Trasferimento riuscito! TX: {tx}", "wallet_trust_degraded": "Fiducia Hardware Degradata", "wallet_warning": "Avviso", + "claim_node_no_signer": "Il tuo nodo deve essere avviato prima di poter rivendicare un altro nodo. Avvia il tuo nodo, quindi riprova.", "attestation_actions_desc": "Azioni per questo record", "chat_badge_message": "MESSAGGIO", "chat_details": "tipo {type} · ambito {scope} · stato {status}", diff --git a/client/iosApp/iosApp/localization/ja.json b/client/iosApp/iosApp/localization/ja.json index 07f3851..1dfa258 100644 --- a/client/iosApp/iosApp/localization/ja.json +++ b/client/iosApp/iosApp/localization/ja.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "送金成功!TX: {tx}", "wallet_trust_degraded": "ハードウェア信頼が低下しました", "wallet_warning": "警告", + "claim_node_no_signer": "他のノードの所有権を取得するには、まずお客様のノードを起動する必要があります。ノードを起動してから、もう一度お試しください。", "attestation_actions_desc": "このレコードに対する操作", "chat_badge_message": "メッセージ", "chat_details": "種別 {type} · スコープ {scope} · 状態 {status}", diff --git a/client/iosApp/iosApp/localization/ko.json b/client/iosApp/iosApp/localization/ko.json index a766a42..ac48476 100644 --- a/client/iosApp/iosApp/localization/ko.json +++ b/client/iosApp/iosApp/localization/ko.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "전송 성공! TX: {tx}", "wallet_trust_degraded": "하드웨어 신뢰 저하됨", "wallet_warning": "경고", + "claim_node_no_signer": "노드를 시작해야 다른 노드의 소유권을 주장할 수 있습니다. 노드를 시작한 후 다시 시도하세요.", "attestation_actions_desc": "이 레코드에 대한 작업", "chat_badge_message": "메시지", "chat_details": "유형 {type} · 범위 {scope} · 상태 {status}", diff --git a/client/iosApp/iosApp/localization/mr.json b/client/iosApp/iosApp/localization/mr.json index 0c7cf1f..2fa2e8f 100644 --- a/client/iosApp/iosApp/localization/mr.json +++ b/client/iosApp/iosApp/localization/mr.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer यशful! TX: {tx}", "wallet_trust_degraded": "हार्डवेअर विश्वास खालावला", "wallet_warning": "इशारा", + "claim_node_no_signer": "आणखी एका नोडचा दावा करण्यापूर्वी तुमचा नोड सुरू असणे आवश्यक आहे. तुमचा नोड सुरू करा आणि नंतर पुन्हा प्रयत्न करा.", "attestation_actions_desc": "या नोंदीसाठी क्रिया", "chat_badge_message": "संदेश", "chat_details": "प्रकार {type} · व्याप्ती {scope} · स्थिती {status}", diff --git a/client/iosApp/iosApp/localization/my.json b/client/iosApp/iosApp/localization/my.json index aa01418..72cc1a3 100644 --- a/client/iosApp/iosApp/localization/my.json +++ b/client/iosApp/iosApp/localization/my.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer အောင်မြင်ful! TX: {tx}", "wallet_trust_degraded": "Hardware ယုံကြည်မှု အားနည်းသွားပြီ", "wallet_warning": "သတိပေး", + "claim_node_no_signer": "အခြား Node ကို ပိုင်ဆိုင်မှုပြောရန် သင်၏ node ကို အရင်စတင်ထားရမည်။ သင်၏ node ကို စတင်ပြီး ထပ်မံကြိုးစားပါ။", "attestation_actions_desc": "ဤမှတ်တမ်းအတွက် လုပ်ဆောင်ချက်များ", "chat_badge_message": "မက်ဆေ့ချ်", "chat_details": "အမျိုးအစား {type} · scope {scope} · အခြေအနေ {status}", diff --git a/client/iosApp/iosApp/localization/pa.json b/client/iosApp/iosApp/localization/pa.json index ebf8145..da6082d 100644 --- a/client/iosApp/iosApp/localization/pa.json +++ b/client/iosApp/iosApp/localization/pa.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "ਟ੍ਰਾਂਸਫਰ ਸਫਲ! TX: {tx}", "wallet_trust_degraded": "ਹਾਰਡਵੇਅਰ ਭਰੋਸਾ ਘਟਿਆ", "wallet_warning": "ਚੇਤਾਵਨੀ", + "claim_node_no_signer": "ਕਿਸੇ ਹੋਰ ਨੋਡ ਦਾ ਦਾਅਵਾ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਤੁਹਾਡਾ ਨੋਡ ਸ਼ੁਰੂ ਹੋਣਾ ਲਾਜ਼ਮੀ ਹੈ। ਆਪਣਾ ਨੋਡ ਸ਼ੁਰੂ ਕਰੋ, ਫਿਰ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।", "attestation_actions_desc": "ਇਸ ਰਿਕਾਰਡ ਲਈ ਕਾਰਵਾਈਆਂ", "chat_badge_message": "ਸੁਨੇਹਾ", "chat_details": "ਕਿਸਮ {type} · ਦਾਇਰਾ {scope} · ਸਥਿਤੀ {status}", diff --git a/client/iosApp/iosApp/localization/pt.json b/client/iosApp/iosApp/localization/pt.json index fa5fa8c..d00c840 100644 --- a/client/iosApp/iosApp/localization/pt.json +++ b/client/iosApp/iosApp/localization/pt.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transferência bem-sucedida! TX: {tx}", "wallet_trust_degraded": "Confiança de Hardware Degradada", "wallet_warning": "Aviso", + "claim_node_no_signer": "Seu nó precisa ser iniciado antes que possa reivindicar outro nó. Inicie seu nó e tente novamente.", "attestation_actions_desc": "Ações para este registo", "chat_badge_message": "MENSAGEM", "chat_details": "tipo {type} · âmbito {scope} · estado {status}", diff --git a/client/iosApp/iosApp/localization/ru.json b/client/iosApp/iosApp/localization/ru.json index 484e28e..b4bbaeb 100644 --- a/client/iosApp/iosApp/localization/ru.json +++ b/client/iosApp/iosApp/localization/ru.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Перевод успешен! TX: {tx}", "wallet_trust_degraded": "Доверие к оборудованию ухудшено", "wallet_warning": "Предупреждение", + "claim_node_no_signer": "Ваш узел должен быть запущен, прежде чем он сможет заявить права на другой узел. Запустите узел и повторите попытку.", "attestation_actions_desc": "Действия для этой записи", "chat_badge_message": "СООБЩЕНИЕ", "chat_details": "тип {type} · область {scope} · статус {status}", diff --git a/client/iosApp/iosApp/localization/sw.json b/client/iosApp/iosApp/localization/sw.json index 522bfde..f05a8e1 100644 --- a/client/iosApp/iosApp/localization/sw.json +++ b/client/iosApp/iosApp/localization/sw.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Uhamisho umefanikiwa! TX: {tx}", "wallet_trust_degraded": "Kuaminika kwa Maunzi Kumedhoofika", "wallet_warning": "Onyo", + "claim_node_no_signer": "Nodi yako inahitaji kuanzishwa kabla ya kudai nodi nyingine. Anzisha nodi yako, kisha ujaribu tena.", "attestation_actions_desc": "Vitendo vya rekodi hii", "chat_badge_message": "UJUMBE", "chat_details": "aina {type} · wigo {scope} · hali {status}", diff --git a/client/iosApp/iosApp/localization/ta.json b/client/iosApp/iosApp/localization/ta.json index 34f5c0e..12dc310 100644 --- a/client/iosApp/iosApp/localization/ta.json +++ b/client/iosApp/iosApp/localization/ta.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "பரிமாற்றம் வெற்றிகரம்! TX: {tx}", "wallet_trust_degraded": "Hardware நம்பகத்தன்மை Degraded", "wallet_warning": "எச்சரிக்கை", + "claim_node_no_signer": "உங்கள் நோடு மற்றொரு நோடைக் கோரும் முன் தொடங்கப்பட்டிருக்க வேண்டும். உங்கள் நோடைத் தொடங்கி, மீண்டும் முயற்சிக்கவும்.", "attestation_actions_desc": "இந்தப் பதிவுக்கான செயல்கள்", "chat_badge_message": "செய்தி", "chat_details": "வகை {type} · எல்லை {scope} · நிலை {status}", diff --git a/client/iosApp/iosApp/localization/te.json b/client/iosApp/iosApp/localization/te.json index 78b6759..6a6cd1b 100644 --- a/client/iosApp/iosApp/localization/te.json +++ b/client/iosApp/iosApp/localization/te.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "బదిలీ విజయవంతమైంది! TX: {tx}", "wallet_trust_degraded": "హార్డ్‌వేర్ నమ్మకం తగ్గింది", "wallet_warning": "హెచ్చరిక", + "claim_node_no_signer": "మీ నోడ్ మరో నోడ్‌ను క్లెయిమ్ చేయడానికి ముందు ప్రారంభమై ఉండాలి. మీ నోడ్‌ను ప్రారంభించి, మళ్ళీ ప్రయత్నించండి.", "attestation_actions_desc": "ఈ రికార్డుకు చర్యలు", "chat_badge_message": "సందేశం", "chat_details": "రకం {type} · పరిధి {scope} · స్థితి {status}", diff --git a/client/iosApp/iosApp/localization/th.json b/client/iosApp/iosApp/localization/th.json index 78de09e..4cbc211 100644 --- a/client/iosApp/iosApp/localization/th.json +++ b/client/iosApp/iosApp/localization/th.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer สำเร็จful! TX: {tx}", "wallet_trust_degraded": "ความน่าเชื่อถือของฮาร์ดแวร์ลดลง", "wallet_warning": "คำเตือน", + "claim_node_no_signer": "โหนดของคุณต้องเริ่มทำงานก่อนจึงจะสามารถอ้างสิทธิ์โหนดอื่นได้ กรุณาเริ่มโหนดของคุณ แล้วลองอีกครั้ง", "attestation_actions_desc": "การดำเนินการสำหรับระเบียนนี้", "chat_badge_message": "ข้อความ", "chat_details": "ประเภท {type} · ขอบเขต {scope} · สถานะ {status}", diff --git a/client/iosApp/iosApp/localization/tr.json b/client/iosApp/iosApp/localization/tr.json index 9708b87..443adfc 100644 --- a/client/iosApp/iosApp/localization/tr.json +++ b/client/iosApp/iosApp/localization/tr.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer başarılı! TX: {tx}", "wallet_trust_degraded": "Donanım Güveni Düşmüş", "wallet_warning": "Uyarı", + "claim_node_no_signer": "Başka bir düğümü sahiplenmeden önce kendi düğümünüz başlatılmış olmalıdır. Düğümünüzü başlatın ve tekrar deneyin.", "attestation_actions_desc": "Bu kayıt için eylemler", "chat_badge_message": "MESAJ", "chat_details": "tür {type} · kapsam {scope} · durum {status}", diff --git a/client/iosApp/iosApp/localization/uk.json b/client/iosApp/iosApp/localization/uk.json index fafcb63..a3ebdd0 100644 --- a/client/iosApp/iosApp/localization/uk.json +++ b/client/iosApp/iosApp/localization/uk.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Переказ успішний! TX: {tx}", "wallet_trust_degraded": "Апаратну довіру знижено", "wallet_warning": "Попередження", + "claim_node_no_signer": "Ваш вузол потрібно запустити, перш ніж він зможе заявити право на інший вузол. Запустіть свій вузол і спробуйте ще раз.", "attestation_actions_desc": "Дії для цього запису", "chat_badge_message": "ПОВІДОМЛЕННЯ", "chat_details": "тип {type} · область {scope} · статус {status}", diff --git a/client/iosApp/iosApp/localization/ur.json b/client/iosApp/iosApp/localization/ur.json index 7c17d31..39b48e6 100644 --- a/client/iosApp/iosApp/localization/ur.json +++ b/client/iosApp/iosApp/localization/ur.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "ٹرانسفر کامیاب! TX: {tx}", "wallet_trust_degraded": "ہارڈ ویئر اعتماد کمزور پڑ گیا", "wallet_warning": "انتباہ", + "claim_node_no_signer": "کسی اور نوڈ کا دعویٰ کرنے سے پہلے آپ کا نوڈ شروع ہونا ضروری ہے۔ اپنا نوڈ شروع کریں، پھر دوبارہ کوشش کریں۔", "attestation_actions_desc": "اس ریکارڈ کے لیے اقدامات", "chat_badge_message": "پیغام", "chat_details": "قسم {type} · دائرہ {scope} · حیثیت {status}", diff --git a/client/iosApp/iosApp/localization/vi.json b/client/iosApp/iosApp/localization/vi.json index 245869a..018b388 100644 --- a/client/iosApp/iosApp/localization/vi.json +++ b/client/iosApp/iosApp/localization/vi.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Chuyển khoản thành công! TX: {tx}", "wallet_trust_degraded": "Tin tưởng Suy giảm", "wallet_warning": "Cảnh báo", + "claim_node_no_signer": "Nút của bạn phải được khởi động trước khi có thể nhận quyền một nút khác. Hãy khởi động nút của bạn, rồi thử lại.", "attestation_actions_desc": "Các thao tác cho bản ghi này", "chat_badge_message": "TIN NHẮN", "chat_details": "loại {type} · phạm vi {scope} · trạng thái {status}", diff --git a/client/iosApp/iosApp/localization/yo.json b/client/iosApp/iosApp/localization/yo.json index d7f8400..0ebc93f 100644 --- a/client/iosApp/iosApp/localization/yo.json +++ b/client/iosApp/iosApp/localization/yo.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Gbígbé ṣe àṣeyọrí! TX: {tx}", "wallet_trust_degraded": "Ìgbẹ́kẹ̀lé Ohun Ẹ̀rọ Ti Dín Kù", "wallet_warning": "Ìkìlọ̀", + "claim_node_no_signer": "Gbọ́dọ̀ bẹ̀rẹ̀ node rẹ kí ó tó lè gba node mìíràn. Bẹ̀rẹ̀ node rẹ, lẹ́yìn náà gbìyànjú lẹ́ẹ̀kan sí i.", "attestation_actions_desc": "Àwọn ìgbésẹ̀ fún àkọsílẹ̀ yìí", "chat_badge_message": "ÌFIRÁNṢẸ́", "chat_details": "irú {type} · ààlà {scope} · ipò {status}", diff --git a/client/iosApp/iosApp/localization/zh.json b/client/iosApp/iosApp/localization/zh.json index 2bcede3..060d438 100644 --- a/client/iosApp/iosApp/localization/zh.json +++ b/client/iosApp/iosApp/localization/zh.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "转账成功!TX: {tx}", "wallet_trust_degraded": "硬件信任降级", "wallet_warning": "警告", + "claim_node_no_signer": "您的节点必须先启动才能认领另一个节点。请启动您的节点,然后重试。", "attestation_actions_desc": "此记录的操作", "chat_badge_message": "消息", "chat_details": "类型 {type} · 范围 {scope} · 状态 {status}", diff --git a/client/shared/src/desktopMain/resources/localization/am.json b/client/shared/src/desktopMain/resources/localization/am.json index b478e20..55d8b26 100644 --- a/client/shared/src/desktopMain/resources/localization/am.json +++ b/client/shared/src/desktopMain/resources/localization/am.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "ማስተላለፍ ተሳክቷል! TX: {tx}", "wallet_trust_degraded": "ስልክ ሥቅ ተዳክሞ ነበር", "wallet_warning": "ማስጠንቀቂያ", + "claim_node_no_signer": "የእርስዎ ኖድ ሌላ ኖድ ከመያዙ በፊት መጀመር አለበት። ኖድዎን ይጀምሩ፣ ከዚያ እንደገና ይሞክሩ።", "attestation_actions_desc": "ለዚህ መዝገብ ያሉ እርምጃዎች", "chat_badge_message": "መልዕክት", "chat_details": "ዓይነት {type} · ወሰን {scope} · ሁኔታ {status}", diff --git a/client/shared/src/desktopMain/resources/localization/ar.json b/client/shared/src/desktopMain/resources/localization/ar.json index 3b025d4..31d7e78 100644 --- a/client/shared/src/desktopMain/resources/localization/ar.json +++ b/client/shared/src/desktopMain/resources/localization/ar.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "نجح التحويل! TX: {tx}", "wallet_trust_degraded": "تدهورت ثقة الأجهزة", "wallet_warning": "تحذير", + "claim_node_no_signer": "يجب تشغيل عقدتك قبل أن تتمكن من المطالبة بعقدة أخرى. شغّل عقدتك، ثم حاول مرة أخرى.", "attestation_actions_desc": "إجراءات هذا السجل", "chat_badge_message": "رسالة", "chat_details": "النوع {type} · النطاق {scope} · الحالة {status}", diff --git a/client/shared/src/desktopMain/resources/localization/bn.json b/client/shared/src/desktopMain/resources/localization/bn.json index 30f1052..744a4b8 100644 --- a/client/shared/src/desktopMain/resources/localization/bn.json +++ b/client/shared/src/desktopMain/resources/localization/bn.json @@ -2925,6 +2925,7 @@ "wallet_transfer_success": "ট্রান্সফার সফল! TX: {tx}", "wallet_trust_degraded": "হার্ডওয়্যার বিশ্বাস হ্রাস পেয়েছে", "wallet_warning": "সতর্কতা", + "claim_node_no_signer": "আরেকটি নোড দাবি করার আগে আপনার নোড চালু থাকতে হবে। আপনার নোড চালু করুন, তারপর আবার চেষ্টা করুন।", "attestation_actions_desc": "এই রেকর্ডের জন্য পদক্ষেপ", "chat_badge_message": "বার্তা", "chat_details": "ধরন {type} · পরিসর {scope} · অবস্থা {status}", diff --git a/client/shared/src/desktopMain/resources/localization/de.json b/client/shared/src/desktopMain/resources/localization/de.json index 74e684c..1bb19cb 100644 --- a/client/shared/src/desktopMain/resources/localization/de.json +++ b/client/shared/src/desktopMain/resources/localization/de.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Überweisung erfolgreich! TX: {tx}", "wallet_trust_degraded": "Hardware-Vertrauen herabgestuft", "wallet_warning": "Warnung", + "claim_node_no_signer": "Ihr Node muss gestartet sein, bevor er einen anderen Knoten übernehmen kann. Starten Sie Ihren Node und versuchen Sie es erneut.", "attestation_actions_desc": "Aktionen für diesen Datensatz", "chat_badge_message": "NACHRICHT", "chat_details": "Typ {type} · Bereich {scope} · Status {status}", diff --git a/client/shared/src/desktopMain/resources/localization/es.json b/client/shared/src/desktopMain/resources/localization/es.json index 66a6762..6b600c2 100644 --- a/client/shared/src/desktopMain/resources/localization/es.json +++ b/client/shared/src/desktopMain/resources/localization/es.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "¡Transferencia exitosa! TX: {tx}", "wallet_trust_degraded": "Confianza de Hardware Degradada", "wallet_warning": "Advertencia", + "claim_node_no_signer": "Tu nodo debe estar iniciado antes de poder reclamar otro nodo. Inicia tu nodo e inténtalo de nuevo.", "attestation_actions_desc": "Acciones para este registro", "chat_badge_message": "MENSAJE", "chat_details": "tipo {type} · ámbito {scope} · estado {status}", diff --git a/client/shared/src/desktopMain/resources/localization/fa.json b/client/shared/src/desktopMain/resources/localization/fa.json index d9caea0..0fda01b 100644 --- a/client/shared/src/desktopMain/resources/localization/fa.json +++ b/client/shared/src/desktopMain/resources/localization/fa.json @@ -2930,6 +2930,7 @@ "wallet_transfer_success": "انتقال با موفقیت انجام شد! TX: {tx}", "wallet_trust_degraded": "افت اعتماد سخت‌افزاری", "wallet_warning": "هشدار", + "claim_node_no_signer": "گرهٔ شما باید پیش از ادعای گره‌ای دیگر راه‌اندازی شده باشد. گرهٔ خود را راه‌اندازی کنید و دوباره تلاش کنید.", "attestation_actions_desc": "اقدامات برای این رکورد", "chat_badge_message": "پیام", "chat_details": "نوع {type} · محدوده {scope} · وضعیت {status}", diff --git a/client/shared/src/desktopMain/resources/localization/fr.json b/client/shared/src/desktopMain/resources/localization/fr.json index 10abf12..5716835 100644 --- a/client/shared/src/desktopMain/resources/localization/fr.json +++ b/client/shared/src/desktopMain/resources/localization/fr.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfert réussi ! TX: {tx}", "wallet_trust_degraded": "Confiance Matérielle Dégradée", "wallet_warning": "Avertissement", + "claim_node_no_signer": "Votre nœud doit être démarré avant de pouvoir revendiquer un autre nœud. Démarrez votre nœud, puis réessayez.", "attestation_actions_desc": "Actions pour cet enregistrement", "chat_badge_message": "MESSAGE", "chat_details": "type {type} · portée {scope} · statut {status}", diff --git a/client/shared/src/desktopMain/resources/localization/ha.json b/client/shared/src/desktopMain/resources/localization/ha.json index 7ac4491..79baae8 100644 --- a/client/shared/src/desktopMain/resources/localization/ha.json +++ b/client/shared/src/desktopMain/resources/localization/ha.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Aika ya yi nasara! TX: {tx}", "wallet_trust_degraded": "Tabbas Kayan Aiki Ya Rage", "wallet_warning": "Gargaɗi", + "claim_node_no_signer": "Dole ne a fara kumburinka kafin ya iya karɓar wani kumburi. Fara kumburinka, sannan ka sake gwadawa.", "attestation_actions_desc": "Ayyuka don wannan rikodin", "chat_badge_message": "SAƘO", "chat_details": "nau'i {type} · iyaka {scope} · matsayi {status}", diff --git a/client/shared/src/desktopMain/resources/localization/hi.json b/client/shared/src/desktopMain/resources/localization/hi.json index 7654f45..d483015 100644 --- a/client/shared/src/desktopMain/resources/localization/hi.json +++ b/client/shared/src/desktopMain/resources/localization/hi.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "स्थानांतरण सफल! TX: {tx}", "wallet_trust_degraded": "हार्डवेयर ट्रस्ट कम हुआ", "wallet_warning": "चेतावनी", + "claim_node_no_signer": "किसी अन्य नोड का दावा करने से पहले आपका नोड शुरू होना चाहिए। अपना नोड शुरू करें, फिर दोबारा प्रयास करें।", "attestation_actions_desc": "इस रिकॉर्ड के लिए कार्रवाइयाँ", "chat_badge_message": "संदेश", "chat_details": "प्रकार {type} · दायरा {scope} · स्थिति {status}", diff --git a/client/shared/src/desktopMain/resources/localization/id.json b/client/shared/src/desktopMain/resources/localization/id.json index 809bf6a..aa8a9af 100644 --- a/client/shared/src/desktopMain/resources/localization/id.json +++ b/client/shared/src/desktopMain/resources/localization/id.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer berhasil! TX: {tx}", "wallet_trust_degraded": "Kepercayaan Perangkat Keras Menurun", "wallet_warning": "Peringatan", + "claim_node_no_signer": "Node Anda harus dijalankan terlebih dahulu sebelum dapat mengklaim node lain. Jalankan node Anda, lalu coba lagi.", "attestation_actions_desc": "Tindakan untuk catatan ini", "chat_badge_message": "PESAN", "chat_details": "jenis {type} · lingkup {scope} · status {status}", diff --git a/client/shared/src/desktopMain/resources/localization/it.json b/client/shared/src/desktopMain/resources/localization/it.json index 1748af9..38dd3e2 100644 --- a/client/shared/src/desktopMain/resources/localization/it.json +++ b/client/shared/src/desktopMain/resources/localization/it.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Trasferimento riuscito! TX: {tx}", "wallet_trust_degraded": "Fiducia Hardware Degradata", "wallet_warning": "Avviso", + "claim_node_no_signer": "Il tuo nodo deve essere avviato prima di poter rivendicare un altro nodo. Avvia il tuo nodo, quindi riprova.", "attestation_actions_desc": "Azioni per questo record", "chat_badge_message": "MESSAGGIO", "chat_details": "tipo {type} · ambito {scope} · stato {status}", diff --git a/client/shared/src/desktopMain/resources/localization/ja.json b/client/shared/src/desktopMain/resources/localization/ja.json index 07f3851..1dfa258 100644 --- a/client/shared/src/desktopMain/resources/localization/ja.json +++ b/client/shared/src/desktopMain/resources/localization/ja.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "送金成功!TX: {tx}", "wallet_trust_degraded": "ハードウェア信頼が低下しました", "wallet_warning": "警告", + "claim_node_no_signer": "他のノードの所有権を取得するには、まずお客様のノードを起動する必要があります。ノードを起動してから、もう一度お試しください。", "attestation_actions_desc": "このレコードに対する操作", "chat_badge_message": "メッセージ", "chat_details": "種別 {type} · スコープ {scope} · 状態 {status}", diff --git a/client/shared/src/desktopMain/resources/localization/ko.json b/client/shared/src/desktopMain/resources/localization/ko.json index a766a42..ac48476 100644 --- a/client/shared/src/desktopMain/resources/localization/ko.json +++ b/client/shared/src/desktopMain/resources/localization/ko.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "전송 성공! TX: {tx}", "wallet_trust_degraded": "하드웨어 신뢰 저하됨", "wallet_warning": "경고", + "claim_node_no_signer": "노드를 시작해야 다른 노드의 소유권을 주장할 수 있습니다. 노드를 시작한 후 다시 시도하세요.", "attestation_actions_desc": "이 레코드에 대한 작업", "chat_badge_message": "메시지", "chat_details": "유형 {type} · 범위 {scope} · 상태 {status}", diff --git a/client/shared/src/desktopMain/resources/localization/mr.json b/client/shared/src/desktopMain/resources/localization/mr.json index 0c7cf1f..2fa2e8f 100644 --- a/client/shared/src/desktopMain/resources/localization/mr.json +++ b/client/shared/src/desktopMain/resources/localization/mr.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer यशful! TX: {tx}", "wallet_trust_degraded": "हार्डवेअर विश्वास खालावला", "wallet_warning": "इशारा", + "claim_node_no_signer": "आणखी एका नोडचा दावा करण्यापूर्वी तुमचा नोड सुरू असणे आवश्यक आहे. तुमचा नोड सुरू करा आणि नंतर पुन्हा प्रयत्न करा.", "attestation_actions_desc": "या नोंदीसाठी क्रिया", "chat_badge_message": "संदेश", "chat_details": "प्रकार {type} · व्याप्ती {scope} · स्थिती {status}", diff --git a/client/shared/src/desktopMain/resources/localization/my.json b/client/shared/src/desktopMain/resources/localization/my.json index aa01418..72cc1a3 100644 --- a/client/shared/src/desktopMain/resources/localization/my.json +++ b/client/shared/src/desktopMain/resources/localization/my.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer အောင်မြင်ful! TX: {tx}", "wallet_trust_degraded": "Hardware ယုံကြည်မှု အားနည်းသွားပြီ", "wallet_warning": "သတိပေး", + "claim_node_no_signer": "အခြား Node ကို ပိုင်ဆိုင်မှုပြောရန် သင်၏ node ကို အရင်စတင်ထားရမည်။ သင်၏ node ကို စတင်ပြီး ထပ်မံကြိုးစားပါ။", "attestation_actions_desc": "ဤမှတ်တမ်းအတွက် လုပ်ဆောင်ချက်များ", "chat_badge_message": "မက်ဆေ့ချ်", "chat_details": "အမျိုးအစား {type} · scope {scope} · အခြေအနေ {status}", diff --git a/client/shared/src/desktopMain/resources/localization/pa.json b/client/shared/src/desktopMain/resources/localization/pa.json index ebf8145..da6082d 100644 --- a/client/shared/src/desktopMain/resources/localization/pa.json +++ b/client/shared/src/desktopMain/resources/localization/pa.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "ਟ੍ਰਾਂਸਫਰ ਸਫਲ! TX: {tx}", "wallet_trust_degraded": "ਹਾਰਡਵੇਅਰ ਭਰੋਸਾ ਘਟਿਆ", "wallet_warning": "ਚੇਤਾਵਨੀ", + "claim_node_no_signer": "ਕਿਸੇ ਹੋਰ ਨੋਡ ਦਾ ਦਾਅਵਾ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਤੁਹਾਡਾ ਨੋਡ ਸ਼ੁਰੂ ਹੋਣਾ ਲਾਜ਼ਮੀ ਹੈ। ਆਪਣਾ ਨੋਡ ਸ਼ੁਰੂ ਕਰੋ, ਫਿਰ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।", "attestation_actions_desc": "ਇਸ ਰਿਕਾਰਡ ਲਈ ਕਾਰਵਾਈਆਂ", "chat_badge_message": "ਸੁਨੇਹਾ", "chat_details": "ਕਿਸਮ {type} · ਦਾਇਰਾ {scope} · ਸਥਿਤੀ {status}", diff --git a/client/shared/src/desktopMain/resources/localization/pt.json b/client/shared/src/desktopMain/resources/localization/pt.json index fa5fa8c..d00c840 100644 --- a/client/shared/src/desktopMain/resources/localization/pt.json +++ b/client/shared/src/desktopMain/resources/localization/pt.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transferência bem-sucedida! TX: {tx}", "wallet_trust_degraded": "Confiança de Hardware Degradada", "wallet_warning": "Aviso", + "claim_node_no_signer": "Seu nó precisa ser iniciado antes que possa reivindicar outro nó. Inicie seu nó e tente novamente.", "attestation_actions_desc": "Ações para este registo", "chat_badge_message": "MENSAGEM", "chat_details": "tipo {type} · âmbito {scope} · estado {status}", diff --git a/client/shared/src/desktopMain/resources/localization/ru.json b/client/shared/src/desktopMain/resources/localization/ru.json index 484e28e..b4bbaeb 100644 --- a/client/shared/src/desktopMain/resources/localization/ru.json +++ b/client/shared/src/desktopMain/resources/localization/ru.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Перевод успешен! TX: {tx}", "wallet_trust_degraded": "Доверие к оборудованию ухудшено", "wallet_warning": "Предупреждение", + "claim_node_no_signer": "Ваш узел должен быть запущен, прежде чем он сможет заявить права на другой узел. Запустите узел и повторите попытку.", "attestation_actions_desc": "Действия для этой записи", "chat_badge_message": "СООБЩЕНИЕ", "chat_details": "тип {type} · область {scope} · статус {status}", diff --git a/client/shared/src/desktopMain/resources/localization/sw.json b/client/shared/src/desktopMain/resources/localization/sw.json index 522bfde..f05a8e1 100644 --- a/client/shared/src/desktopMain/resources/localization/sw.json +++ b/client/shared/src/desktopMain/resources/localization/sw.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Uhamisho umefanikiwa! TX: {tx}", "wallet_trust_degraded": "Kuaminika kwa Maunzi Kumedhoofika", "wallet_warning": "Onyo", + "claim_node_no_signer": "Nodi yako inahitaji kuanzishwa kabla ya kudai nodi nyingine. Anzisha nodi yako, kisha ujaribu tena.", "attestation_actions_desc": "Vitendo vya rekodi hii", "chat_badge_message": "UJUMBE", "chat_details": "aina {type} · wigo {scope} · hali {status}", diff --git a/client/shared/src/desktopMain/resources/localization/ta.json b/client/shared/src/desktopMain/resources/localization/ta.json index 34f5c0e..12dc310 100644 --- a/client/shared/src/desktopMain/resources/localization/ta.json +++ b/client/shared/src/desktopMain/resources/localization/ta.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "பரிமாற்றம் வெற்றிகரம்! TX: {tx}", "wallet_trust_degraded": "Hardware நம்பகத்தன்மை Degraded", "wallet_warning": "எச்சரிக்கை", + "claim_node_no_signer": "உங்கள் நோடு மற்றொரு நோடைக் கோரும் முன் தொடங்கப்பட்டிருக்க வேண்டும். உங்கள் நோடைத் தொடங்கி, மீண்டும் முயற்சிக்கவும்.", "attestation_actions_desc": "இந்தப் பதிவுக்கான செயல்கள்", "chat_badge_message": "செய்தி", "chat_details": "வகை {type} · எல்லை {scope} · நிலை {status}", diff --git a/client/shared/src/desktopMain/resources/localization/te.json b/client/shared/src/desktopMain/resources/localization/te.json index 78b6759..6a6cd1b 100644 --- a/client/shared/src/desktopMain/resources/localization/te.json +++ b/client/shared/src/desktopMain/resources/localization/te.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "బదిలీ విజయవంతమైంది! TX: {tx}", "wallet_trust_degraded": "హార్డ్‌వేర్ నమ్మకం తగ్గింది", "wallet_warning": "హెచ్చరిక", + "claim_node_no_signer": "మీ నోడ్ మరో నోడ్‌ను క్లెయిమ్ చేయడానికి ముందు ప్రారంభమై ఉండాలి. మీ నోడ్‌ను ప్రారంభించి, మళ్ళీ ప్రయత్నించండి.", "attestation_actions_desc": "ఈ రికార్డుకు చర్యలు", "chat_badge_message": "సందేశం", "chat_details": "రకం {type} · పరిధి {scope} · స్థితి {status}", diff --git a/client/shared/src/desktopMain/resources/localization/th.json b/client/shared/src/desktopMain/resources/localization/th.json index 78de09e..4cbc211 100644 --- a/client/shared/src/desktopMain/resources/localization/th.json +++ b/client/shared/src/desktopMain/resources/localization/th.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer สำเร็จful! TX: {tx}", "wallet_trust_degraded": "ความน่าเชื่อถือของฮาร์ดแวร์ลดลง", "wallet_warning": "คำเตือน", + "claim_node_no_signer": "โหนดของคุณต้องเริ่มทำงานก่อนจึงจะสามารถอ้างสิทธิ์โหนดอื่นได้ กรุณาเริ่มโหนดของคุณ แล้วลองอีกครั้ง", "attestation_actions_desc": "การดำเนินการสำหรับระเบียนนี้", "chat_badge_message": "ข้อความ", "chat_details": "ประเภท {type} · ขอบเขต {scope} · สถานะ {status}", diff --git a/client/shared/src/desktopMain/resources/localization/tr.json b/client/shared/src/desktopMain/resources/localization/tr.json index 9708b87..443adfc 100644 --- a/client/shared/src/desktopMain/resources/localization/tr.json +++ b/client/shared/src/desktopMain/resources/localization/tr.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Transfer başarılı! TX: {tx}", "wallet_trust_degraded": "Donanım Güveni Düşmüş", "wallet_warning": "Uyarı", + "claim_node_no_signer": "Başka bir düğümü sahiplenmeden önce kendi düğümünüz başlatılmış olmalıdır. Düğümünüzü başlatın ve tekrar deneyin.", "attestation_actions_desc": "Bu kayıt için eylemler", "chat_badge_message": "MESAJ", "chat_details": "tür {type} · kapsam {scope} · durum {status}", diff --git a/client/shared/src/desktopMain/resources/localization/uk.json b/client/shared/src/desktopMain/resources/localization/uk.json index fafcb63..a3ebdd0 100644 --- a/client/shared/src/desktopMain/resources/localization/uk.json +++ b/client/shared/src/desktopMain/resources/localization/uk.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Переказ успішний! TX: {tx}", "wallet_trust_degraded": "Апаратну довіру знижено", "wallet_warning": "Попередження", + "claim_node_no_signer": "Ваш вузол потрібно запустити, перш ніж він зможе заявити право на інший вузол. Запустіть свій вузол і спробуйте ще раз.", "attestation_actions_desc": "Дії для цього запису", "chat_badge_message": "ПОВІДОМЛЕННЯ", "chat_details": "тип {type} · область {scope} · статус {status}", diff --git a/client/shared/src/desktopMain/resources/localization/ur.json b/client/shared/src/desktopMain/resources/localization/ur.json index 7c17d31..39b48e6 100644 --- a/client/shared/src/desktopMain/resources/localization/ur.json +++ b/client/shared/src/desktopMain/resources/localization/ur.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "ٹرانسفر کامیاب! TX: {tx}", "wallet_trust_degraded": "ہارڈ ویئر اعتماد کمزور پڑ گیا", "wallet_warning": "انتباہ", + "claim_node_no_signer": "کسی اور نوڈ کا دعویٰ کرنے سے پہلے آپ کا نوڈ شروع ہونا ضروری ہے۔ اپنا نوڈ شروع کریں، پھر دوبارہ کوشش کریں۔", "attestation_actions_desc": "اس ریکارڈ کے لیے اقدامات", "chat_badge_message": "پیغام", "chat_details": "قسم {type} · دائرہ {scope} · حیثیت {status}", diff --git a/client/shared/src/desktopMain/resources/localization/vi.json b/client/shared/src/desktopMain/resources/localization/vi.json index 245869a..018b388 100644 --- a/client/shared/src/desktopMain/resources/localization/vi.json +++ b/client/shared/src/desktopMain/resources/localization/vi.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Chuyển khoản thành công! TX: {tx}", "wallet_trust_degraded": "Tin tưởng Suy giảm", "wallet_warning": "Cảnh báo", + "claim_node_no_signer": "Nút của bạn phải được khởi động trước khi có thể nhận quyền một nút khác. Hãy khởi động nút của bạn, rồi thử lại.", "attestation_actions_desc": "Các thao tác cho bản ghi này", "chat_badge_message": "TIN NHẮN", "chat_details": "loại {type} · phạm vi {scope} · trạng thái {status}", diff --git a/client/shared/src/desktopMain/resources/localization/yo.json b/client/shared/src/desktopMain/resources/localization/yo.json index d7f8400..0ebc93f 100644 --- a/client/shared/src/desktopMain/resources/localization/yo.json +++ b/client/shared/src/desktopMain/resources/localization/yo.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "Gbígbé ṣe àṣeyọrí! TX: {tx}", "wallet_trust_degraded": "Ìgbẹ́kẹ̀lé Ohun Ẹ̀rọ Ti Dín Kù", "wallet_warning": "Ìkìlọ̀", + "claim_node_no_signer": "Gbọ́dọ̀ bẹ̀rẹ̀ node rẹ kí ó tó lè gba node mìíràn. Bẹ̀rẹ̀ node rẹ, lẹ́yìn náà gbìyànjú lẹ́ẹ̀kan sí i.", "attestation_actions_desc": "Àwọn ìgbésẹ̀ fún àkọsílẹ̀ yìí", "chat_badge_message": "ÌFIRÁNṢẸ́", "chat_details": "irú {type} · ààlà {scope} · ipò {status}", diff --git a/client/shared/src/desktopMain/resources/localization/zh.json b/client/shared/src/desktopMain/resources/localization/zh.json index 2bcede3..060d438 100644 --- a/client/shared/src/desktopMain/resources/localization/zh.json +++ b/client/shared/src/desktopMain/resources/localization/zh.json @@ -2924,6 +2924,7 @@ "wallet_transfer_success": "转账成功!TX: {tx}", "wallet_trust_degraded": "硬件信任降级", "wallet_warning": "警告", + "claim_node_no_signer": "您的节点必须先启动才能认领另一个节点。请启动您的节点,然后重试。", "attestation_actions_desc": "此记录的操作", "chat_badge_message": "消息", "chat_details": "类型 {type} · 范围 {scope} · 状态 {status}",