diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..5c98f586 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,28 @@ +{ + "permissions": { + "allow": [ + "mcp__chrome-devtools__new_page", + "mcp__chrome-devtools__take_screenshot", + "Bash(cargo test --package atomic-server --test dht_resolve --no-run)", + "Bash(cargo test --package atomic-server --test dht_resolve -- --nocapture)", + "Bash(pnpm test)", + "Bash(cargo build -p atomic-server)", + "Bash(grep -n \"async fn get_resource_extended\" /Users/joep/dev/github/atomicdata-dev/atomic-server/lib/src/*.rs)", + "Bash(grep -n \"async fn query\\\\|fn query\" /Users/joep/dev/github/atomicdata-dev/atomic-server/server/src/*.rs)", + "Bash(grep -E \"\\\\.\\(rs\\)$\")", + "Bash(grep -n \"pub async fn handle_get_resource\\\\|pub async fn post_commit\" /Users/joep/dev/github/atomicdata-dev/atomic-server/server/src/handlers/*.rs)", + "Bash(cargo tauri --version)", + "Bash(echo $ANDROID_HOME)", + "Bash(adb devices)", + "Bash(echo $PATH)", + "Bash(cargo build:*)", + "Bash(cargo test:*)", + "Bash(for f:*)", + "Bash(do echo:*)", + "Read(//Users/joep/dev/github/atomicdata-dev/atomic-server/**)", + "Bash(done)", + "Bash(npx tsc:*)", + "mcp__charlotte__charlotte_screenshot" + ] + } +} diff --git a/.dagger/.gitattributes b/.dagger/.gitattributes index 82741846..675e60d4 100644 --- a/.dagger/.gitattributes +++ b/.dagger/.gitattributes @@ -1 +1,2 @@ /sdk/** linguist-generated +tar diff --git a/.gitignore b/.gitignore index 8f365917..203c8667 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ trace-*.json artifact server/assets_tmp .netlify +scratchpad diff --git a/.vscode/settings.json b/.vscode/settings.json index ade2c5a0..c7782666 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -24,12 +24,17 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, - "eslint.workingDirectories": [{ "directory": "browser" }], + "eslint.workingDirectories": [ + { + "directory": "browser" + } + ], "typescript.preferences.preferTypeOnlyAutoImports": true, "rustTestExplorer.rootCargoManifestFilePath": "./Cargo.toml", // This won't work in multi-root workspaces, could be fixed by using a rust-analyzer.toml once there is some more documentation on that. // For now you need to set this in your own vscode settings file. "rust-analyzer.cargo.extraEnv": { "ATOMICSERVER_SKIP_JS_BUILD": "true" - } + }, + "java.configuration.updateBuildConfiguration": "automatic" } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 780c3915..a3d67a44 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -50,9 +50,9 @@ "group": "test" }, { - "label": "run jaeger for tracing (using docker)", + "label": "run SigNoz for tracing locally (using docker)", "type": "shell", - "command": "docker run -d -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 -p 5775:5775/udp -p 6831:6831/udp -p 6832:6832/udp -p 5778:5778 -p 16686:16686 -p 14268:14268 -p 9411:9411 jaegertracing/all-in-one", + "command": "git clone https://github.com/SigNoz/signoz.git /tmp/signoz 2>/dev/null || true && cd /tmp/signoz/deploy && docker compose up", "group": "none", "problemMatcher": [] }, diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 00000000..cdb4a871 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,10 @@ +{ + "terminal": { + "shell": { + "with_arguments": { + "program": "/bin/zsh", + "args": ["--login"] + } + } + } +} diff --git a/.zed/tasks.json b/.zed/tasks.json new file mode 100644 index 00000000..76ded8c3 --- /dev/null +++ b/.zed/tasks.json @@ -0,0 +1,57 @@ +[ + { + "label": "run atomic-server (cargo run)", + "command": "cargo run --bin atomic-server", + "cwd": "$ZED_WORKTREE_ROOT" + }, + { + "label": "test atomic-server (cargo nextest run)", + "command": "cargo nextest run", + "cwd": "$ZED_WORKTREE_ROOT" + }, + { + "label": "run data-browser dev server (pnpm start)", + "command": "pnpm start", + "cwd": "$ZED_WORKTREE_ROOT/browser" + }, + { + "label": "test data-browser e2e", + "command": "pnpm test-e2e", + "cwd": "$ZED_WORKTREE_ROOT/browser" + }, + { + "label": "test end-to-end / E2E (npm playwright)", + "command": "cd server/e2e_tests/ && npm i && npm run test", + "cwd": "$ZED_WORKTREE_ROOT" + }, + { + "label": "build desktop atomic-server tauri", + "command": "cargo tauri build", + "cwd": "$ZED_WORKTREE_ROOT/desktop" + }, + { + "label": "dev desktop atomic-server tauri", + "command": "cargo tauri dev", + "cwd": "$ZED_WORKTREE_ROOT/desktop" + }, + { + "label": "benchmark criterion atomic-server", + "command": "cargo criterion", + "cwd": "$ZED_WORKTREE_ROOT/server" + }, + { + "label": "docs atomic data (mdbook serve)", + "command": "mdbook serve", + "cwd": "$ZED_WORKTREE_ROOT/docs" + }, + { + "label": "run SigNoz for tracing locally (docker)", + "command": "git clone https://github.com/SigNoz/signoz.git /tmp/signoz 2>/dev/null || true && cd /tmp/signoz/deploy && docker compose up", + "cwd": "$ZED_WORKTREE_ROOT" + }, + { + "label": "dagger call rust-build", + "command": "dagger call rust-build", + "cwd": "$ZED_WORKTREE_ROOT" + } +] diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..ff39aa93 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,33 @@ +# AGENTS.md + +Guidance for coding agents working in this repo. + +## Local Setup + +- `http://localhost:5173` — Vite dev server (frontend). +- `http://localhost:9883` — local Atomic Server. + +The frontend auto-updates via HMR. If changes don't appear, reload the page. If you edit `@tomic/lib` or `@tomic/react`, those packages may need a rebuild first. + +## Quick Dev Setup + +Navigate to `http://localhost:5173/app/dev-drive` to instantly create a fresh agent + drive on `localhost:9883` and switch to it. Only works in dev mode. + +In E2E tests, use `devDrive(page)` from `test-utils.ts`: + +```ts +await devDrive(page); // goes to /app/dev-drive and waits for the drive to be ready +``` + +## Charlotte / Browser Automation + +- Always operate the app at `localhost:5173`, not `9883` directly. +- Start every session by navigating to `http://localhost:5173/app/dev-drive` to get a clean, authenticated state. +- If the app shows `Unauthorized` or `Something went wrong`, navigate to `/app/dev-drive` to fix it. + +## Debugging Checklist + +- Is the frontend open on `5173`? +- Is the active drive/server `9883`? +- Is there a signed-in agent? +- Run `devDrive(page)` to reset to a clean state. diff --git a/CHANGELOG.md b/CHANGELOG.md index d4e54ae8..b6c1a079 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,19 +7,24 @@ See [STATUS.md](server/STATUS.md) to learn more about which features will remain ## UNRELEASED +- [#1139](https://github.com/ontola/atomic-server/issues/1139) AtomicServer can now create data without being dependent on a server! AtomicServer is now Local-First, using the new `did:ad` schema. +- #584 Replace ureq with reqwest (async HTTP calls) +- #420 Fix OTLP / OpenTelemetry, update docs from Jaeger to SigNoz, add metrics +- [#590](https://github.com/ontola/atomic-server/issues/590) Get rid of the `SERVER_URL` env var, which makes moving & setup easier. All resources are now relative to the hosted domain, and AtomicServer can be available from multiple domains at once. +- [#544](https://github.com/ontola/atomicdata-dev/atomic-server/issues/544) Stateless invites, using JWTs. Server setup now requires you to check the logs for the invite token. - We changed the binary format in which resources are stored. This means your data will be migrated the first time you run the server. This could take some time depending on the size of your database. -- [#1048](https://github.com/atomicdata-dev/atomic-server/issues/1048) Fix search index not removing old versions of resources. -- [#1056](https://github.com/atomicdata-dev/atomic-server/issues/1056) Switched from Earthly to Dagger for CI. Also made improvements to E2E test publishing and building docker images. -- [#979](https://github.com/atomicdata-dev/atomic-server/issues/979) Fix nested resource deletion, use transactions -- [#1057](https://github.com/atomicdata-dev/atomic-server/issues/1057) Fix double slashes in search bar -- [#986](https://github.com/atomicdata-dev/atomic-server/issues/986) CLI should use Agent in requests - get -- [#1047](https://github.com/atomicdata-dev/atomic-server/issues/1047) Search endpoint throws error for websocket requests -- [#958](https://github.com/atomicdata-dev/atomic-server/issues/958) Fix search in CLI / atomic_lib -- [#658](https://github.com/atomicdata-dev/atomic-server/issues/658) Added JSON datatype. -- [#1024](https://github.com/atomicdata-dev/atomic-server/issues/1024) Added URI datatype. -- [#998](https://github.com/atomicdata-dev/atomic-server/issues/998) Added YJS datatype. -- [#851](https://github.com/atomicdata-dev/atomic-server/issues/851) Deleting file resources now also deletes the file from the filesystem. -BREAKING: [#1107](https://github.com/atomicdata-dev/atomic-server/issues/1107) Named nested resources are no longer supported. Value::Resource and SubResource::Resource have been removed. If you need to include multiple resources in a response use an array. +- [#1048](https://github.com/ontola/atomic-server/issues/1048) Fix search index not removing old versions of resources. +- [#1056](https://github.com/ontola/atomic-server/issues/1056) Switched from Earthly to Dagger for CI. Also made improvements to E2E test publishing and building docker images. +- [#979](https://github.com/ontola/atomic-server/issues/979) Fix nested resource deletion, use transactions +- [#1057](https://github.com/ontola/atomic-server/issues/1057) Fix double slashes in search bar +- [#986](https://github.com/ontola/atomic-server/issues/986) CLI should use Agent in requests - get +- [#1047](https://github.com/ontola/atomic-server/issues/1047) Search endpoint throws error for websocket requests +- [#958](https://github.com/ontola/atomic-server/issues/958) Fix search in CLI / atomic_lib +- [#658](https://github.com/ontola/atomic-server/issues/658) Added JSON datatype. +- [#1024](https://github.com/ontola/atomic-server/issues/1024) Added URI datatype. +- [#998](https://github.com/ontola/atomic-server/issues/998) Added YJS datatype. +- [#851](https://github.com/ontola/atomic-server/issues/851) Deleting file resources now also deletes the file from the filesystem. +BREAKING: [#1107](https://github.com/ontola/atomic-server/issues/1107) Named nested resources are no longer supported. Value::Resource and SubResource::Resource have been removed. If you need to include multiple resources in a response use an array. BREAKING: `store.get_resource_extended()` now returns a `ResourceResponse` instead of a `Resource` due to the removal of named nested resources. Use `.into()` or `.to_single()` to convert to a `Resource`. ## [v0.40.2] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e35659e..ab431c38 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ Check out the [Roadmap](https://docs.atomicdata.dev/roadmap.html) if you want to - [Testing](#testing) - [Performance monitoring / benchmarks](#performance-monitoring--benchmarks) - [Tracing](#tracing) - - [Tracing with OpenTelemetry (and Jaeger)](#tracing-with-opentelemetry-and-jaeger) + - [Tracing with OpenTelemetry (and SigNoz)](#tracing-with-opentelemetry-and-signoz) - [Tracing with Chrome](#tracing-with-chrome) - [Criterion benchmarks](#criterion-benchmarks) - [Drill](#drill) @@ -158,26 +158,30 @@ For doing this, we have at least three tools: tracing, criterion and drill. There are two ways you can use `tracing` to get insights into performance. -#### Tracing with OpenTelemetry (and Jaeger) +#### Tracing with OpenTelemetry (and SigNoz) - Run the server with `--trace opentelemetry` and add `--log-level trace` to inspect more events -- Run an OpenTelemetry compatible service, such as Jaeger. See `docker run` command below or use the vscode task. -- Visit jaeger: `http://localhost:16686` +- Sign up for [SigNoz Cloud](https://signoz.io/) (free trial available) or run SigNoz locally with Docker +- Add the following to your `.env`: ```sh -docker run -d --platform linux/amd64 --name jaeger \ - -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \ - -p 5775:5775/udp \ - -p 6831:6831/udp \ - -p 6832:6832/udp \ - -p 5778:5778 \ - -p 4317:4317 \ - -p 16686:16686 \ - -p 14268:14268 \ - -p 9411:9411 \ - jaegertracing/all-in-one:latest +ATOMIC_TRACING=opentelemetry +OTEL_EXPORTER_OTLP_PROTOCOL=grpc +OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest..signoz.cloud:443 +OTEL_EXPORTER_OTLP_HEADERS=signoz-ingestion-key= ``` +- Visit SigNoz to inspect traces and logs: `https://app.signoz.io/` + +For local development without a cloud account, you can run SigNoz locally: + +```sh +git clone https://github.com/SigNoz/signoz.git +cd signoz/deploy && docker compose up +``` + +Then set `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317` (no TLS needed locally). + #### Tracing with Chrome - Use the `tracing::instrument` macro to make functions traceable. Check out the [tracing](https://docs.rs/tracing/latest/tracing/) docs for more info. diff --git a/Cargo.lock b/Cargo.lock index 751f9f38..27402ef9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -578,28 +578,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "async-trait" version = "0.1.89" @@ -665,14 +643,18 @@ dependencies = [ "directories", "dotenv", "futures", + "hex", "html2md", "image", + "indexmap 1.9.3", "instant-acme", "kuchikiki", "lol_html", - "opentelemetry 0.28.0", + "mainline", + "opentelemetry", + "opentelemetry-appender-tracing", "opentelemetry-otlp", - "opentelemetry_sdk 0.28.0", + "opentelemetry_sdk", "percent-encoding", "rand 0.8.5", "rcgen", @@ -682,20 +664,24 @@ dependencies = [ "rio_api", "rio_turtle", "rustls 0.20.9", + "rustls 0.23.31", "rustls-pemfile", "sanitize-filename", "serde", + "serde_jcs", "serde_json", "serde_with", + "sha1", "simple-server-timing-header", "static-files 0.2.5", "tantivy", "tokio", + "tonic", "tracing", "tracing-actix-web", "tracing-chrome", "tracing-log", - "tracing-opentelemetry 0.29.0", + "tracing-opentelemetry", "tracing-subscriber", "ureq", "url", @@ -725,11 +711,15 @@ dependencies = [ "criterion", "directories", "futures", + "hex", "iai", "lazy_static", + "mainline", "ntest", + "opentelemetry", "rand 0.8.5", "regex", + "reqwest 0.13.1", "ring 0.17.14", "rio_api", "rio_turtle", @@ -737,12 +727,12 @@ dependencies = [ "serde", "serde_jcs", "serde_json", + "sha1", "sled", "tokio", "toml 0.8.23", "tracing", "ulid", - "ureq", "url", "urlencoding", "yrs", @@ -801,11 +791,10 @@ dependencies = [ [[package]] name = "axum" -version = "0.7.9" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ - "async-trait", "axum-core", "bytes", "futures-util", @@ -818,29 +807,26 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "sync_wrapper", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", ] [[package]] name = "axum-core" -version = "0.4.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ - "async-trait", "bytes", - "futures-util", + "futures-core", "http 1.3.1", "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", @@ -924,6 +910,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" +dependencies = [ + "hybrid-array", +] + [[package]] name = "brotli" version = "8.0.2" @@ -952,7 +947,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", - "regex-automata 0.4.10", + "regex-automata", "serde", ] @@ -1221,7 +1216,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "inout", ] @@ -1423,36 +1418,36 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.126.1" +version = "0.126.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30054f4aef4d614d37f27d5b77e36e165f0b27a71563be348e7c9fcfac41eed8" +checksum = "19f28665a3cba7b8fe75d885c2a1c1bbc661b65685df34f7d93a4669ceb2e719" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.126.1" +version = "0.126.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beab56413879d4f515e08bcf118b1cb85f294129bb117057f573d37bfbb925a" +checksum = "6308845400e41d9d34acf8f2d13454b907012d9de5265c66f731570adf82019e" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.126.1" +version = "0.126.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d054747549a69b264d5299c8ca1b0dd45dc6bd0ee43f1edfcc42a8b12952c7a" +checksum = "93ed5df9b6cda90f2dd921760925079670ba6c86162efa4de9f6c6efea124384" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-bitset" -version = "0.126.1" +version = "0.126.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98b92d481b77a7dc9d07c96e24a16f29e0c9c27d042828fdf7e49e54ee9819bf" +checksum = "006fe8776f6d81acb83571f52e7737a54c6dec1ba75e2b7b5a68af15451f88ee" dependencies = [ "serde", "serde_derive", @@ -1460,9 +1455,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.126.1" +version = "0.126.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eeccfc043d599b0ef1806942707fc51cdd1c3965c343956dc975a55d82a920f" +checksum = "021b5a45c5ca4d414746a985c7241fea4202fd71aeef5a2891c0be32518e3201" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -1487,9 +1482,9 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.126.1" +version = "0.126.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1174cdb9d9d43b2bdaa612a07ed82af13db9b95526bc2c286c2aec4689bcc038" +checksum = "5350ad78964a8cc301bc83cbc9b5144ccb44e1c2f604b551cc8ec15c99900dcb" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -1500,24 +1495,24 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.126.1" +version = "0.126.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d572be73fae802eb115f45e7e67a9ed16acb4ee683b67c4086768786545419a" +checksum = "6918b5db84d5a9b09eb8fae05466cd57fb04d97a88ac47c24698830c8714747e" [[package]] name = "cranelift-control" -version = "0.126.1" +version = "0.126.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1587465cc84c5cc793b44add928771945f3132bbf6b3621ee9473c631a87156" +checksum = "ec4ea4593cd6ef06573d7a6bc5a4231368f96a5b57f65900b24553cca3284bcd" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.126.1" +version = "0.126.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063b83448b1343e79282c3c7cbda7ed5f0816f0b763a4c15f7cecb0a17d87ea6" +checksum = "bcca10e8c33eac67a45be4e09d236e274697831ca6bf4c4a927f7570eb8436a8" dependencies = [ "cranelift-bitset", "serde", @@ -1526,9 +1521,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.126.1" +version = "0.126.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4461c2d2ca48bc72883f5f5c3129d9aefac832df1db824af9db8db3efee109" +checksum = "0dcc8b7e922ab1a6ec4640be3533698e291a4111b83d96f8d9e3367162e290ef" dependencies = [ "cranelift-codegen", "log", @@ -1538,15 +1533,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.126.1" +version = "0.126.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acd811b25e18f14810d09c504e06098acc1d9dbfa24879bf0d6b6fb44415fc66" +checksum = "9db87d9e6fe9ba89a71434a06c9f19153f3dd273a1c5c9a6365bc4f019213d1b" [[package]] name = "cranelift-native" -version = "0.126.1" +version = "0.126.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2417046989d8d6367a55bbab2e406a9195d176f4779be4aa484d645887217d37" +checksum = "e6aa4002a6569b047ecb846f5a952d21b81963817a0c1dad064b69e5a80f5952" dependencies = [ "cranelift-codegen", "libc", @@ -1555,9 +1550,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.126.1" +version = "0.126.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d039de901c8d928222b8128e1b9a9ab27b82a7445cb749a871c75d9cb25c57d" +checksum = "289ab02de2733de3a857c98bdaace8f4dfab1cc1d322ba8637280ce2a7d15d8e" [[package]] name = "crc" @@ -1693,6 +1688,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "cssparser" version = "0.27.2" @@ -1720,6 +1724,33 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "curve25519-dalek" +version = "5.0.0-pre.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a434aec7908df6ca86cda069864d7686aea8afad979aadc9e30e50ac3e40b45a" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.11.0-rc.9", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "darling" version = "0.20.11" @@ -1853,11 +1884,21 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.0-rc.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff8de092798697546237a3a701e4174fe021579faec9b854379af9bf1e31962" +dependencies = [ + "block-buffer 0.11.0", + "crypto-common 0.2.1", +] + [[package]] name = "dircpy" version = "0.3.19" @@ -1946,6 +1987,15 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -1985,6 +2035,28 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "ed25519" +version = "3.0.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0-pre.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416904184c8542e5e4f6c052fdfb377164ab462706ce3a496641aa9ea6a1e172" +dependencies = [ + "curve25519-dalek", + "ed25519", + "sha2 0.11.0-rc.4", + "subtle", + "zeroize", +] + [[package]] name = "edit" version = "0.1.5" @@ -2168,6 +2240,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + [[package]] name = "find-msvc-tools" version = "0.1.5" @@ -2191,6 +2269,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2307,6 +2396,16 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -2566,7 +2665,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -2700,6 +2799,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b229d73f5803b562cc26e4da0396c8610a4ee209f4fac8fa4f8d709166dc45" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "0.14.32" @@ -2810,11 +2918,9 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.0", - "system-configuration", "tokio", "tower-service", "tracing", - "windows-registry", ] [[package]] @@ -3406,6 +3512,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "local-channel" version = "0.1.5" @@ -3476,6 +3588,12 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" + [[package]] name = "lru-slab" version = "0.1.2" @@ -3495,7 +3613,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1670343e58806300d87950e3401e820b519b9384281bbabfb15e3636689ffd69" dependencies = [ "crc", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -3513,6 +3631,29 @@ dependencies = [ "libc", ] +[[package]] +name = "mainline" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa5e16c1b587f47e3198a1393f8d8e231f301fbdd739cfd9c2c69872dfc8b0ac" +dependencies = [ + "crc", + "digest 0.11.0-rc.9", + "document-features", + "dyn-clone", + "ed25519-dalek", + "flume", + "futures-lite", + "getrandom 0.3.3", + "lru 0.16.3", + "serde", + "serde_bencode", + "serde_bytes", + "sha1_smol", + "thiserror 2.0.17", + "tracing", +] + [[package]] name = "markup5ever" version = "0.11.0" @@ -3555,11 +3696,11 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -3570,9 +3711,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "matchit" -version = "0.7.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "maybe-owned" @@ -3774,12 +3915,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -3899,9 +4039,9 @@ checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" [[package]] name = "opentelemetry" -version = "0.28.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "236e667b670a5cdf90c258f5a55794ec5ac5027e960c224bff8367a59e1e6426" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" dependencies = [ "futures-core", "futures-sink", @@ -3912,46 +4052,41 @@ dependencies = [ ] [[package]] -name = "opentelemetry" -version = "0.29.1" +name = "opentelemetry-appender-tracing" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e87237e2775f74896f9ad219d26a2081751187eb7c9f5c58dde20a23b95d16c" +checksum = "ef6a1ac5ca3accf562b8c306fa8483c85f4390f768185ab775f242f7fe8fdcc2" dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "pin-project-lite", - "thiserror 2.0.17", + "opentelemetry", "tracing", + "tracing-core", + "tracing-subscriber", ] [[package]] name = "opentelemetry-http" -version = "0.28.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8863faf2910030d139fb48715ad5ff2f35029fc5f244f6d5f689ddcf4d26253" +checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" dependencies = [ "async-trait", "bytes", "http 1.3.1", - "opentelemetry 0.28.0", + "opentelemetry", "reqwest 0.12.23", - "tracing", ] [[package]] name = "opentelemetry-otlp" -version = "0.28.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bef114c6d41bea83d6dc60eb41720eedd0261a67af57b66dd2b84ac46c01d91" +checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" dependencies = [ - "async-trait", - "futures-core", "http 1.3.1", - "opentelemetry 0.28.0", + "opentelemetry", "opentelemetry-http", "opentelemetry-proto", - "opentelemetry_sdk 0.28.0", + "opentelemetry_sdk", "prost", "reqwest 0.12.23", "thiserror 2.0.17", @@ -3962,59 +4097,34 @@ dependencies = [ [[package]] name = "opentelemetry-proto" -version = "0.28.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f8870d3024727e99212eb3bb1762ec16e255e3e6f58eeb3dc8db1aa226746d" +checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" dependencies = [ - "opentelemetry 0.28.0", - "opentelemetry_sdk 0.28.0", + "opentelemetry", + "opentelemetry_sdk", "prost", "tonic", + "tonic-prost", ] [[package]] name = "opentelemetry_sdk" -version = "0.28.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84dfad6042089c7fc1f6118b7040dc2eb4ab520abbf410b79dc481032af39570" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" dependencies = [ - "async-trait", "futures-channel", "futures-executor", "futures-util", - "glob", - "opentelemetry 0.28.0", + "opentelemetry", "percent-encoding", - "rand 0.8.5", - "serde_json", + "rand 0.9.2", "thiserror 2.0.17", "tokio", "tokio-stream", - "tracing", ] -[[package]] -name = "opentelemetry_sdk" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afdefb21d1d47394abc1ba6c57363ab141be19e27cc70d0e422b7f303e4d290b" -dependencies = [ - "futures-channel", - "futures-executor", - "futures-util", - "glob", - "opentelemetry 0.29.1", - "percent-encoding", - "rand 0.9.2", - "thiserror 2.0.17", -] - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "ownedbytes" version = "0.7.0" @@ -4143,10 +4253,10 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest", + "digest 0.10.7", "hmac", "password-hash", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -4155,7 +4265,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", + "digest 0.10.7", "hmac", ] @@ -4533,9 +4643,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.5" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" dependencies = [ "bytes", "prost-derive", @@ -4543,9 +4653,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.5" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", "itertools 0.14.0", @@ -4556,9 +4666,9 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a09eb45f768f3a0396e85822790d867000c8b5f11551e7268c279e991457b16" +checksum = "0412168ab18b7d37047011474788846d1be290ea548867789b5a8b45651004a7" dependencies = [ "cranelift-bitset", "log", @@ -4568,9 +4678,9 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e29368432b8b7a8a343b75a6914621fad905c95d5c5297449a6546c127224f7a" +checksum = "752233a382efa1026438aa8409c72489ebaa7ed94148bfabdf5282dc864276ef" dependencies = [ "proc-macro2", "quote", @@ -4948,9 +5058,9 @@ dependencies = [ [[package]] name = "regalloc2" -version = "0.13.3" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e249c660440317032a71ddac302f25f1d5dff387667bcc3978d1f77aa31ac34" +checksum = "08effbc1fa53aaebff69521a5c05640523fab037b34a4a2c109506bc938246fa" dependencies = [ "allocator-api2", "bumpalo", @@ -4968,17 +5078,8 @@ checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.10", - "regex-syntax 0.8.6", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -4989,7 +5090,7 @@ checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.6", + "regex-syntax", ] [[package]] @@ -4998,12 +5099,6 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30" -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.6" @@ -5035,7 +5130,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tower 0.5.2", + "tower", "tower-http", "tower-service", "url", @@ -5052,9 +5147,8 @@ checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" dependencies = [ "base64 0.22.1", "bytes", - "encoding_rs", "futures-core", - "h2 0.4.12", + "futures-util", "http 1.3.1", "http-body 1.0.1", "http-body-util", @@ -5063,22 +5157,25 @@ dependencies = [ "hyper-util", "js-sys", "log", - "mime", "percent-encoding", "pin-project-lite", "quinn", "rustls 0.23.31", "rustls-pki-types", "rustls-platform-verifier", + "serde", + "serde_json", "sync_wrapper", "tokio", "tokio-rustls 0.26.4", - "tower 0.5.2", + "tokio-util", + "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", ] @@ -5097,7 +5194,7 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", + "spin 0.5.2", "untrusted 0.7.1", "web-sys", "winapi", @@ -5571,6 +5668,26 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bencode" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70dfc7b7438b99896e7f8992363ab8e2c4ba26aa5ec675d32d1c3c2c33d413e" +dependencies = [ + "serde", + "serde_bytes", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -5716,9 +5833,15 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -5727,7 +5850,18 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7535f94fa3339fe9e5e9be6260a909e62af97f6e14b32345ccf79b92b8b81233" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.0-rc.9", ] [[package]] @@ -5760,6 +5894,12 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "3.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3" + [[package]] name = "simd-adler32" version = "0.3.7" @@ -5887,6 +6027,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -6000,27 +6149,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "system-deps" version = "6.2.2" @@ -6072,7 +6200,7 @@ dependencies = [ "itertools 0.12.1", "levenshtein_automata", "log", - "lru", + "lru 0.12.5", "lz4_flex", "measure_time", "memmap2", @@ -6146,7 +6274,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" dependencies = [ "byteorder", - "regex-syntax 0.8.6", + "regex-syntax", "utf8-ranges", ] @@ -6559,11 +6687,10 @@ checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] name = "tonic" -version = "0.12.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" dependencies = [ - "async-stream", "async-trait", "axum", "base64 0.22.1", @@ -6577,34 +6704,27 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "prost", - "socket2 0.5.10", + "rustls-native-certs 0.8.3", + "socket2 0.6.0", + "sync_wrapper", "tokio", + "tokio-rustls 0.26.4", "tokio-stream", - "tower 0.4.13", + "tower", "tower-layer", "tower-service", "tracing", ] [[package]] -name = "tower" -version = "0.4.13" +name = "tonic-prost" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" dependencies = [ - "futures-core", - "futures-util", - "indexmap 1.9.3", - "pin-project", - "pin-project-lite", - "rand 0.8.5", - "slab", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", + "bytes", + "prost", + "tonic", ] [[package]] @@ -6615,11 +6735,15 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "indexmap 2.12.1", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -6635,7 +6759,7 @@ dependencies = [ "http-body 1.0.1", "iri-string", "pin-project-lite", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", ] @@ -6654,9 +6778,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -6666,24 +6790,24 @@ dependencies = [ [[package]] name = "tracing-actix-web" -version = "0.7.19" +version = "0.7.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5360edd490ec8dee9fedfc6a9fd83ac2f01b3e1996e3261b9ad18a61971fe064" +checksum = "1ca6b15407f9bfcb35f82d0e79e603e1629ece4e91cc6d9e58f890c184dd20af" dependencies = [ "actix-web", "mutually_exclusive_features", - "opentelemetry 0.29.1", + "opentelemetry", "pin-project", "tracing", - "tracing-opentelemetry 0.30.0", + "tracing-opentelemetry", "uuid", ] [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -6703,9 +6827,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -6724,32 +6848,12 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721f2d2569dce9f3dfbbddee5906941e953bfcdf736a62da3377f5751650cc36" -dependencies = [ - "js-sys", - "once_cell", - "opentelemetry 0.28.0", - "opentelemetry_sdk 0.28.0", - "smallvec", - "tracing", - "tracing-core", - "tracing-log", - "tracing-subscriber", - "web-time", -] - -[[package]] -name = "tracing-opentelemetry" -version = "0.30.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd8e764bd6f5813fd8bebc3117875190c5b0415be8f7f8059bffb6ecd979c444" +checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" dependencies = [ "js-sys", - "once_cell", - "opentelemetry 0.29.1", - "opentelemetry_sdk 0.29.0", + "opentelemetry", "smallvec", "tracing", "tracing-core", @@ -6760,14 +6864,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -7191,6 +7295,19 @@ dependencies = [ "wasmparser 0.241.2", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.219.2" @@ -7253,9 +7370,9 @@ dependencies = [ [[package]] name = "wasmtime" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "511bc19c2d48f338007dc941cb40c833c4707023fdaf9ec9b97cf1d5a62d26bb" +checksum = "a667153732c6cfba625cf5adc5db60ea2849f9a027b012a48cdd81e691e7b70a" dependencies = [ "addr2line", "anyhow", @@ -7310,9 +7427,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b0d53657fea2a8cee8ed1866ad45d2e5bc21be958a626a1dd9b7de589851b3" +checksum = "fd342272a338b98ca2b5d82c0bd687f76e0214beeafbed107666bb16ff654a1e" dependencies = [ "anyhow", "cpp_demangle", @@ -7337,9 +7454,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-cache" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e065628d2a6eccb722de71c6d9b58771f5c3c4f9d35f6cb6d9d92370f4c2b4" +checksum = "4184b4dba5f5ba95eb219c745ff3b80c86eba479b54804e81ca7f9db91869567" dependencies = [ "anyhow", "base64 0.22.1", @@ -7349,7 +7466,7 @@ dependencies = [ "rustix 1.0.8", "serde", "serde_derive", - "sha2", + "sha2 0.10.9", "toml 0.9.8", "windows-sys 0.60.2", "zstd 0.13.3", @@ -7357,9 +7474,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-macro" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c933104f57d27dd1e6c7bd9ee5df3242bdd1962d9381bc08fa5d4e60e1f5ebdf" +checksum = "a0903eaf417c3f8250f5fd7e4f94ad195041d3d8d3d84fddcfcf778453c3e5c8" dependencies = [ "anyhow", "proc-macro2", @@ -7372,15 +7489,15 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-util" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63ef2a95a5dbaa70fc3ef682ea8997e51cdd819b4d157a1100477cf43949d454" +checksum = "11a336ff2954a447d4698b85ba1e9d6138076fa6b668e48fd9bf5da54712649a" [[package]] name = "wasmtime-internal-cranelift" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73122df6a8cf417ce486a94e844d3a60797217ce7ae69653e0ee9e28269e0fa5" +checksum = "e114a5f504df7784101a8fc15a25206d594ec5496c44ec9b925fd2193d03be0a" dependencies = [ "anyhow", "cfg-if", @@ -7406,9 +7523,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54ead059e58b54a7abbe0bfb9457b3833ebd2ad84326c248a835ff76d64c7c6f" +checksum = "c78d4e39c954198de2f9bd9937eb61408ed4419a6c75b5472fcce926d859cbe5" dependencies = [ "anyhow", "cc", @@ -7421,9 +7538,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af620a4ac1623298c90d3736644e12d66974951d1e38d0464798de85c984e17" +checksum = "2add04119fa43ce6e57f2638ab978a17adafbba738a2aa66f29c5bb528bd030b" dependencies = [ "cc", "object", @@ -7433,9 +7550,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97ccd36e25390258ce6720add639ffe5a7d81a5c904350aa08f5bbc60433d22" +checksum = "967b84e1a766a59955450473fd39a90c77529a0d4928b3bbae81b9c9cbccdc67" dependencies = [ "anyhow", "cfg-if", @@ -7445,24 +7562,24 @@ dependencies = [ [[package]] name = "wasmtime-internal-math" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1b856e1bbf0230ab560ba4204e944b141971adc4e6cdf3feb6979c1a7b7953" +checksum = "8d51480b15d802e7203630ea338da956f5e96b6ae6308db265d14d92a3c29870" dependencies = [ "libm", ] [[package]] name = "wasmtime-internal-slab" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8908e71a780b97cbd3d8f3a0c446ac8df963069e0f3f38c9eace4f199d4d3e65" +checksum = "7227392fed8096183a33ae25fade1b040f4abcf7a3943366467cbc3801d7ec20" [[package]] name = "wasmtime-internal-unwinder" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb9c2f8223a0ef96527f0446b80c7d0d9bb0577c7b918e3104bd6d4cdba1d101" +checksum = "d60c5615cf820bef46f78652d22dc45c9727af363406f78185d1661e78e3e00d" dependencies = [ "anyhow", "cfg-if", @@ -7473,9 +7590,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0fb82cdbffd6cafc812c734a22fa753102888b8760ecf6a08cbb50367a458a" +checksum = "47f6bf5957ba823cb170996073edf4596b26d5f44c53f9e96b586c64fa04f7e9" dependencies = [ "proc-macro2", "quote", @@ -7484,9 +7601,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-winch" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1cfd68149cef86afd9a6c9b51e461266dfa66b37b4c6fdf1201ddbf7f906271" +checksum = "b399a054107359137bbeba8a7795ca30b222d59df634d3d7db5a42408f9be7b5" dependencies = [ "anyhow", "cranelift-codegen", @@ -7502,9 +7619,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-wit-bindgen" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a628437073400148f1ba2b55beb60eb376dc5ca538745994c83332b037d1f3fa" +checksum = "62798d4fed29a560bbb2360669481f7419c704e6bf85b6c25b52f23c11bb0909" dependencies = [ "anyhow", "bitflags 2.10.0", @@ -7515,9 +7632,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "517604b1ce13a56ae3e360217095d7d4db90e84deaa3fba078877c2b80cc5851" +checksum = "e10672929acc96e8492d8e1e2fb02b69e1f22002aaea08dd366f790dfe11f5e9" dependencies = [ "anyhow", "async-trait", @@ -7546,9 +7663,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi-http" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63d735c8a0ef1bb49810f4da75acfdba2390cb4e9de7385bffb8cda77d20d401" +checksum = "274a4f9d168d037264848e4dfd05a1916fc61bf46d0bd75bc6e6d549ae1c3866" dependencies = [ "anyhow", "async-trait", @@ -7570,9 +7687,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi-io" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec66fc94ceb9497d62a3d082bd2cce10348975795516553df4cd89f7d5fc14b" +checksum = "145a2ae59e73be4a802524946250807bb9aada5e7932de071cba6ee24346b835" dependencies = [ "anyhow", "async-trait", @@ -7708,9 +7825,9 @@ dependencies = [ [[package]] name = "wiggle" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb9c745158119785cf3098c97151cfcc33104ade6489bfa158b73d3f5979fa24" +checksum = "9dd8188b23eea8625cc96b29b26ffea7ae82fd50cd2b3394c49f30109933cb25" dependencies = [ "anyhow", "bitflags 2.10.0", @@ -7722,9 +7839,9 @@ dependencies = [ [[package]] name = "wiggle-generate" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a98d02cd1ba87ca6039f28f4f4c0b53a9ff2684f5f2640f471af9bc608b9d9" +checksum = "1a019ec6a7531645e43786805c11c2e7920a2390aa23e067a16485b9bd16720c" dependencies = [ "anyhow", "heck 0.5.0", @@ -7736,9 +7853,9 @@ dependencies = [ [[package]] name = "wiggle-macro" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a111938ed6e662d5f5036bb3cac8d10d5bea77a536885d6d4a4667c9cba97a2" +checksum = "885e44efc8547387700b4bdf9caa66a9d04364f394e31bd3aa240cbce2d47296" dependencies = [ "proc-macro2", "quote", @@ -7779,9 +7896,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" -version = "39.0.1" +version = "39.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1de5a648102e39c8e817ed25e3820f4b9772f3c9c930984f32737be60e3156b" +checksum = "eac192a0d21224c027d56e69b91578f0f758dce26a1641e166312518c18e948a" dependencies = [ "anyhow", "cranelift-assembler-x64", @@ -7844,17 +7961,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-registry" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" -dependencies = [ - "windows-link 0.1.3", - "windows-result", - "windows-strings", -] - [[package]] name = "windows-result" version = "0.3.4" diff --git a/Cargo.toml b/Cargo.toml index c60c67ec..d44f7ce7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,5 @@ members = [ "plugin-examples/test-plugin", "atomic-plugin", ] + # Tauri build is deprecated, see -# https://github.com/atomicdata-dev/atomic-server/issues/718 -exclude = ["desktop"] diff --git a/DIDS-DRIVES-ROUTING.md b/DIDS-DRIVES-ROUTING.md new file mode 100644 index 00000000..e09dbf1f --- /dev/null +++ b/DIDS-DRIVES-ROUTING.md @@ -0,0 +1,66 @@ +# DID-Native Drives and Domain Routing Plan + +## Overview +Atomic Data is moving away from a fixed "main drive" at the root `/` of a server. Instead, every Drive is a first-class decentralized citizen identified by a DID, and human-readable access is handled via a **Domain-to-DID mapping** system. + +## Core Concepts + +### 1. Identity vs. Alias +- **Identity (Immutable)**: A Drive is identified by its DID: `did:ad:{genesis}`. +- **Alias (Mutable)**: A Domain or Subdomain (e.g., `joep.atomicdata.dev`, `localhost:9883`) is an alias that routes to a specific Drive DID. + +### 2. Agent-First Onboarding +Onboarding no longer starts with a "nameless" drive. It starts with the user: +1. **Create Agent**: Generate an Ed25519 keypair (`did:ad:agent:...`). +2. **Create Drive**: The Agent signs the genesis commit for a new Drive. +3. **Bind Alias**: The Server maps the current `Host` (e.g., `localhost`) to this Drive's DID. + +### 3. Multi-Tenancy & Routing +The server uses the HTTP `Host` header to determine which Drive to serve: +- `joep.atomicdata.dev` -> Serves Joep's Drive. +- `jane.atomicdata.dev` -> Serves Jane's Drive. +- `localhost:9883` -> Serves the developer's default Drive. + +### 4. Custom Path Routing +Human-readable access to resources within a Drive is handled via two strategies: +- **Flat Slugs**: If a resource has a `https://atomicdata.dev/properties/path` property (e.g., `path: "/my-blog-post"`), it is served directly at that URL relative to the Drive's domain. +- **Hierarchical Fallback**: If no explicit `path` is found, the server traverses the hierarchy using `PARENT` and `shortname` properties (e.g., `/folders/2026/note`). + +--- + +## Implementation Phases + +### Phase 1: Backend Routing & Mapping +- [x] **Mapping Store**: Sled `drive_mapping` tree in `lib/src/db.rs` — `add_drive_mapping` / `get_drive_did`. +- [x] **Host-Based Resolution**: `appstate.rs::get_drive_did_for_host` — checks explicit mapping, falls back to subdomain query. +- [x] **Relative Pathing**: `db.rs::get_resource_at_path` — traverses hierarchy relative to a Drive DID; supports both flat `path` lookups and hierarchical fallback. + +### Phase 2: Drive Lifecycle & Population +- [x] **Standard Initialization**: `atomic_lib::populate::bootstrap` is drive-agnostic and reusable. +- [x] **Drive Resource metadata**: `OnboardingPage.tsx` creates the genesis drive with `write` and `read` set to the agent's DID. + +### Phase 3: Server Setup & Onboarding UI +- [x] **Node Setup State**: `db.rs::is_uninitialized` + `get_resource.rs` injects `isUninitialized: true` on root responses. +- [x] **Setup UI**: `OnboardingPage.tsx` — generate agent keypair → genesis drive commit → set `INITIAL_DRIVE` on root → show secret. +- [x] **Alias Registration**: `handlers/commit.rs` — after commit, if new resource has `INITIAL_DRIVE` set, calls `store.add_drive_mapping(host, drive_did)` using the `Host` header. +- [x] **Authorization during onboarding**: `handlers/commit.rs` allows unauthenticated writes to `INITIAL_DRIVE` only when the specific host is uninitialized. + +### Phase 4: Decentralized Discovery +- [x] **JSON-AD Headers**: Added `Link: ; rel="canonical"` response header in `handlers/get_resource.rs` for DID subjects. +- [x] **DHT/Reticulum Announces**: `dht.rs` — periodic `announce_peer(SHA1(drive_did))` every 15 min; DHT fallback resolution in `get_resource.rs`. +- [x] **Explicit Subdomains**: `Subject::Internal` explicitly stores subdomains for reliable multi-tenant routing. + +--- + +## What to work on next (priority order) + +1. **Path Uniqueness Validation** — ensure that two resources in the same Drive cannot share the same `path` property during `commit` validation. +2. **Drive-Scoped Search** — update the search endpoint to respect the resolved Drive DID so results are filtered by the current "Tenant". +3. **End-to-end test** — write a server integration test that exercises the full onboarding flow: fresh DB → `isUninitialized` → genesis drive commit → `INITIAL_DRIVE` commit → drive mapping → resource resolution. +4. **Reticulum Pathfinding** — implement the binary announce/resolution logic for `did:ad` over the Reticulum mesh. + +## Success Criteria +1. Running `atomic-server` for the first time leads to an Agent/Drive creation flow. +2. Multiple drives can be accessed via subdomains on the same server instance. +3. Every drive has its own `/classes`, `/templates`, and `/files` collections. +4. Resources shared via `did:ad` identifiers are resolvable across different servers using the routing hints. diff --git a/atomic-plugin/src/bindings.rs b/atomic-plugin/src/bindings.rs index 6e9d7bed..3d7b7155 100644 --- a/atomic-plugin/src/bindings.rs +++ b/atomic-plugin/src/bindings.rs @@ -1,3 +1,4 @@ +#![allow(warnings, clippy::all)] // Generated by `wit-bindgen` 0.41.0. DO NOT EDIT! // Options used: // * runtime_path: "wit_bindgen_rt" diff --git a/atomic-plugin/src/lib.rs b/atomic-plugin/src/lib.rs index 8cfc41d0..8cd3db58 100644 --- a/atomic-plugin/src/lib.rs +++ b/atomic-plugin/src/lib.rs @@ -140,7 +140,7 @@ pub trait ClassExtender { fn class_url() -> Vec; /// Called when a resource is fetched from the server. You can modify the resource in place. - fn on_resource_get<'a>(resource: &'a mut Resource) -> Result, String> { + fn on_resource_get(resource: &mut Resource) -> Result, String> { Ok(Some(resource)) } diff --git a/browser/cli/src/store.ts b/browser/cli/src/store.ts index 6a119166..ca122cbd 100644 --- a/browser/cli/src/store.ts +++ b/browser/cli/src/store.ts @@ -11,7 +11,7 @@ const getCommandIndex = (): number | undefined => { return undefined; }; -const getAgent = (): Agent | undefined => { +const getAgent = async (): Promise => { let secret; const agentCommandIndex = getCommandIndex(); @@ -28,8 +28,8 @@ const getAgent = (): Agent | undefined => { export const store = new Store(); -const agent = getAgent(); - -if (agent) { - store.setAgent(agent); -} +getAgent().then(agent => { + if (agent) { + store.setAgent(agent); + } +}); diff --git a/browser/create-template/src/postprocess.ts b/browser/create-template/src/postprocess.ts index b919ff59..eae6a855 100644 --- a/browser/create-template/src/postprocess.ts +++ b/browser/create-template/src/postprocess.ts @@ -1,6 +1,7 @@ import path from 'node:path'; import fs from 'node:fs'; -import { ErrorType, isAtomicError, Store, type Resource } from '@tomic/lib'; +import { ErrorType, isAtomicError, Store } from '@tomic/lib'; +import type { Resource } from '@tomic/lib'; import { type ExecutionContext, type TemplateKey, diff --git a/browser/data-browser/frontend.log b/browser/data-browser/frontend.log new file mode 100644 index 00000000..aeecff0a --- /dev/null +++ b/browser/data-browser/frontend.log @@ -0,0 +1,31 @@ + +> @tomic/data-browser@0.41.0-beta.0 start /Users/joep/dev/github/atomicdata-dev/atomic-server/browser/data-browser +> vite + + + VITE v8.0.0 ready in 1634 ms + + ➜ Local: http://localhost:5173/ + ➜ Network: http://192.168.0.169:5173/ + ➜ Network: http://192.168.139.3:5173/ + ➜ Network: http://192.168.194.0:5173/ + ➜ Network: http://192.168.215.0:5173/ + ➜ press h + enter to show help +node:events:485 + throw er; // Unhandled 'error' event + ^ + +Error: read EIO + at TTY.onStreamRead (node:internal/stream_base_commons:216:20) +Emitted 'error' event on Interface instance at: + at ReadStream.onerror (node:internal/readline/interface:243:10) + at ReadStream.emit (node:events:507:28) + at emitErrorNT (node:internal/streams/destroy:170:8) + at emitErrorCloseNT (node:internal/streams/destroy:129:3) + at process.processTicksAndRejections (node:internal/process/task_queues:90:21) { + errno: -5, + code: 'EIO', + syscall: 'read' +} + +Node.js v23.11.0 diff --git a/browser/data-browser/package.json b/browser/data-browser/package.json index 52c59c31..493f0de4 100644 --- a/browser/data-browser/package.json +++ b/browser/data-browser/package.json @@ -19,6 +19,7 @@ "@emotion/is-prop-valid": "^1.4.0", "@floating-ui/dom": "^1.7.4", "@modelcontextprotocol/sdk": "^1.23.0", + "@noble/hashes": "^0.5.9", "@oddbird/css-anchor-positioning": "^0.6.1", "@openrouter/ai-sdk-provider": "^1.2.5", "@radix-ui/react-popover": "^1.1.15", @@ -47,8 +48,9 @@ "@tiptap/starter-kit": "^3.11.0", "@tiptap/suggestion": "^3.11.0", "@tiptap/y-tiptap": "^3.0.1", - "@tomic/react": "workspace:*", + "@tomic/lib": "workspace:*", "@tomic/plugin": "workspace:*", + "@tomic/react": "workspace:*", "@uiw/codemirror-theme-github": "^4.25.3", "@uiw/react-codemirror": "^4.25.3", "@wuchale/jsx": "^0.10.1", @@ -101,7 +103,7 @@ "lint-staged": "^10.5.4", "types-wm": "^1.1.0", "typescript": "^5.9.3", - "vite": "^7.3.1", + "vite": "^8.0.0", "vite-plugin-prismjs": "^0.0.11", "vite-plugin-pwa": "^1.1.0", "vite-plugin-webfont-dl": "^3.11.1" diff --git a/browser/data-browser/src/App.tsx b/browser/data-browser/src/App.tsx index d5b3f16b..ff0a64dc 100644 --- a/browser/data-browser/src/App.tsx +++ b/browser/data-browser/src/App.tsx @@ -33,6 +33,9 @@ const store = new Store({ serverUrl, }); +import { bootstrap } from './bootstrap'; +bootstrap(store); + await enableYjs(); store.parseMetaTags(); @@ -44,7 +47,7 @@ declare global { } // Fetch all the Properties and Classes - this helps speed up the app. -store.preloadPropsAndClasses(); +// store.preloadPropsAndClasses(); registerCustomCreateActions(); // Register global event handlers. diff --git a/browser/data-browser/src/bootstrap.ts b/browser/data-browser/src/bootstrap.ts new file mode 100644 index 00000000..49ea8d9f --- /dev/null +++ b/browser/data-browser/src/bootstrap.ts @@ -0,0 +1,40 @@ +import { JSONADParser, type Store } from '@tomic/react'; +import baseModels from '../../../lib/defaults/default_base_models.json'; +import defaultStore from '../../../lib/defaults/default_store.json'; +import tableDefaults from '../../../lib/defaults/table.json'; +import chatroomDefaults from '../../../lib/defaults/chatroom.json'; +import ontologiesDefaults from '../../../lib/defaults/ontologies.json'; +import aiDefaults from '../../../lib/defaults/ai.json'; + +/** + * Injects base models and default store resources into the store. + * This ensures that critical property definitions (like 'subdomain') are + * available even if the server has no Drive binding yet or the definitions haven't + * been uploaded to the live atomicdata.dev server yet. + */ +export function bootstrap(store: Store): void { + const parser = new JSONADParser(); + + const addBootstrapped = (json: unknown) => { + const resources = parser.parse(json); + + for (const r of resources) { + r.loading = false; + store.addResources(r, { skipCommitCompare: true }); + } + + return resources.length; + }; + + try { + const baseCount = addBootstrapped(baseModels); + const storeCount = addBootstrapped(defaultStore); + addBootstrapped(tableDefaults); + addBootstrapped(chatroomDefaults); + addBootstrapped(ontologiesDefaults); + addBootstrapped(aiDefaults); + + } catch (e) { + console.error('Failed to bootstrap store:', e); + } +} diff --git a/browser/data-browser/src/chunks/AI/AIChatMessageParts/UserMessage.tsx b/browser/data-browser/src/chunks/AI/AIChatMessageParts/UserMessage.tsx index 45b7a2e0..39f41339 100644 --- a/browser/data-browser/src/chunks/AI/AIChatMessageParts/UserMessage.tsx +++ b/browser/data-browser/src/chunks/AI/AIChatMessageParts/UserMessage.tsx @@ -14,7 +14,6 @@ export const UserMessage: React.FC = ({ message }) => { return ( - You {context && ( {context.map(item => ( @@ -55,12 +54,3 @@ const UserMessageWrapper = styled(MessageWrapper)` align-self: flex-end; box-shadow: ${p => p.theme.boxShadow}; `; - -const SenderName = styled.span` - display: inline-flex; - align-items: center; - gap: 1ch; - font-weight: bold; - font-size: 0.6rem; - color: ${p => p.theme.colors.textLight}; -`; diff --git a/browser/data-browser/src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx b/browser/data-browser/src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx index 44146d87..1b8f47ee 100644 --- a/browser/data-browser/src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx +++ b/browser/data-browser/src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx @@ -4,22 +4,14 @@ import { Resource, Store, core, + dataBrowser, + server, useArray, useStore, } from '@tomic/react'; -import { useCallback, useEffect, useState, type JSX } from 'react'; -import { styled } from 'styled-components'; -import { Button } from '@components/Button'; -import { - Dialog, - DialogActions, - DialogContent, - DialogTitle, - useDialog, -} from '@components/Dialog'; -import { FormValidationContextProvider } from '@components/forms/formValidation/FormValidationContextProvider'; +import { useCallback, useEffect, type JSX } from 'react'; import { randomString } from '@helpers/randomString'; -import { PropertyForm } from './PropertyForm'; +import { stringToSlug } from '@helpers/stringToSlug'; import { PropertyFormCategory } from './categories'; import { sortSubjectList } from '@views/OntologyPage/sortSubjectList'; @@ -39,15 +31,50 @@ const createSubjectWithBase = (base: string) => { const populatePropertyWithDefaults = async ( property: Resource, tableClass: Resource, + name: string, ) => { await property.set(core.properties.isA, [core.classes.property]); await property.set(core.properties.parent, tableClass.props.parent); - await property.set(core.properties.shortname, 'new-column', false); - await property.set(core.properties.name, '', false); - await property.set(core.properties.description, 'A column in a table'); + await property.set(core.properties.shortname, stringToSlug(name), false); + await property.set(core.properties.name, name, false); + await property.set(core.properties.description, ''); await property.set(core.properties.datatype, Datatype.STRING); +}; - await property.save(); +const applyCategoryDefaults = async ( + category: PropertyFormCategory | undefined, + resource: Resource, +) => { + switch (category) { + case 'number': + await resource.set(core.properties.datatype, Datatype.INTEGER); + break; + case 'date': + await resource.set(core.properties.datatype, Datatype.DATE); + break; + case 'checkbox': + await resource.set(core.properties.datatype, Datatype.BOOLEAN); + break; + case 'file': + await resource.set(core.properties.datatype, Datatype.ATOMIC_URL); + await resource.set(core.properties.classtype, server.classes.file); + break; + case 'json': + await resource.set(core.properties.datatype, Datatype.JSON); + break; + case 'select': + await resource.set(core.properties.datatype, Datatype.RESOURCEARRAY); + await resource.set(core.properties.classtype, dataBrowser.classes.tag); + await resource.addClasses(dataBrowser.classes.selectProperty); + break; + case 'relation': + await resource.set(core.properties.datatype, Datatype.ATOMIC_URL); + break; + case 'text': + default: + // STRING is already set in populatePropertyWithDefaults + break; + } }; const getChildren = (store: Store, resource: Resource) => @@ -55,20 +82,6 @@ const getChildren = (store: Store, resource: Resource) => res => res.get(core.properties.parent) === resource?.subject, ); -const destroyChildren = async (store: Store, resource: Resource) => { - const children = getChildren(store, resource); - - await Promise.all( - children.map(child => { - try { - child.destroy(); - } catch (e) { - return; - } - }), - ); -}; - const saveChildren = async (store: Store, resource: Resource) => { const children = getChildren(store, resource); await Promise.all(children.map(child => child.save())); @@ -80,10 +93,7 @@ export function NewPropertyDialog({ tableClassResource, bindShow, }: NewPropertyDialogProps): JSX.Element { - const [valid, setValid] = useState(false); - const store = useStore(); - const [resource, setResource] = useState(null); const [_properties, _setProperties, pushProp] = useArray( tableClassResource, core.properties.recommends, @@ -92,117 +102,58 @@ export function NewPropertyDialog({ }, ); - const handleUserCancelAction = useCallback(async () => { - if (!resource) { - return; - } - - try { - await destroyChildren(store, resource); - await resource.destroy(); - } finally { - // Server does not have this resource yet so it will nag at us. We set the state to null anyway. - setResource(null); - } - }, [resource, store]); - - const handleUserSuccessAction = useCallback(async () => { - if (!resource) { - return; - } - - const tableClassParent = await store.getResource( - tableClassResource.props.parent, - ); - - if (tableClassParent.hasClasses(core.classes.ontology)) { - await resource.set(core.properties.parent, tableClassParent.subject); - - const ontologyProps = - tableClassParent.get(core.properties.properties) ?? []; - - await tableClassParent.set( - core.properties.properties, - await sortSubjectList(store, [...ontologyProps, resource.subject]), + const savePropertyToTable = useCallback( + async (prop: Resource) => { + const tableClassParent = await store.getResource( + tableClassResource.props.parent, ); - await tableClassParent.save(); - } - - await resource.save(); - await saveChildren(store, resource); + if (tableClassParent.hasClasses(core.classes.ontology)) { + await prop.set(core.properties.parent, tableClassParent.subject); - pushProp([resource.subject]); - setResource(null); - }, [resource, store, tableClassResource, pushProp]); + const ontologyProps = + tableClassParent.get(core.properties.properties) ?? []; - const [dialogProps, show, hide] = useDialog({ - bindShow, - onCancel: handleUserCancelAction, - onSuccess: handleUserSuccessAction, - }); + await tableClassParent.set( + core.properties.properties, + await sortSubjectList(store, [...ontologyProps, prop.subject]), + ); - const createProperty = async () => { - const subject = createSubjectWithBase(tableClassResource.subject); - const propertyResource = store.getResourceLoading(subject, { - newResource: true, - }); + await tableClassParent.save(); + } - await populatePropertyWithDefaults(propertyResource, tableClassResource); + await prop.save(); + await saveChildren(store, prop); + pushProp([prop.subject]); + }, + [store, tableClassResource, pushProp], + ); - setResource(propertyResource); - }; + useEffect(() => { + if (!showDialog) return; - const handleCancelClick = useCallback(() => { - hide(); - }, [hide]); + const create = async () => { + const subject = createSubjectWithBase(tableClassResource.subject); + const propertyResource = store.getResourceLoading(subject, { + newResource: true, + }); - const handleCreateClick = useCallback(() => { - if (valid) { - hide(true); - } - }, [hide, valid]); + const name = selectedCategory ?? 'column'; + await populatePropertyWithDefaults( + propertyResource, + tableClassResource, + name, + ); + await applyCategoryDefaults( + selectedCategory as PropertyFormCategory, + propertyResource, + ); + await savePropertyToTable(propertyResource); + bindShow(false); + }; - useEffect(() => { - if (showDialog) { - createProperty().then(() => { - show(); - }); - } + create().catch(console.error); }, [showDialog]); - if (!resource) { - return <>; - } - - return ( - - - -

- New {selectedCategory} Column -

-
- - - - - - - -
-
- ); + return <>; } - -const Capitalize = styled('span')` - text-transform: capitalize; -`; diff --git a/browser/data-browser/src/chunks/TablePage/PropertyForm/PropertyForm.tsx b/browser/data-browser/src/chunks/TablePage/PropertyForm/PropertyForm.tsx index 5624a6c8..6f0bcef1 100644 --- a/browser/data-browser/src/chunks/TablePage/PropertyForm/PropertyForm.tsx +++ b/browser/data-browser/src/chunks/TablePage/PropertyForm/PropertyForm.tsx @@ -69,7 +69,7 @@ export function PropertyForm({ [onSubmit], ); - // If name was already set remove the error. + // Clear validation error if name is already set (pre-filled or existing property). useEffect(() => { if ( resource.subject !== unknownSubject && diff --git a/browser/data-browser/src/chunks/TablePage/TablePage.tsx b/browser/data-browser/src/chunks/TablePage/TablePage.tsx index ef265bf3..333c81b9 100644 --- a/browser/data-browser/src/chunks/TablePage/TablePage.tsx +++ b/browser/data-browser/src/chunks/TablePage/TablePage.tsx @@ -1,4 +1,4 @@ -import { useId, useState, type JSX } from 'react'; +import { useId, useMemo, useState, type JSX } from 'react'; import { ContainerFull } from '@components/Containers'; import { EditableTitle } from '@components/EditableTitle'; import type { ResourcePageProps } from '@views/ResourcePage'; @@ -15,15 +15,20 @@ export function TablePage({ resource }: ResourcePageProps): JSX.Element { const [showExportDialog, setShowExportDialog] = useState(false); - useCustomContextItems([ - DIVIDER, - { - id: 'export-csv', - label: 'Export to CSV', - onClick: () => setShowExportDialog(true), - icon: , - }, - ]); + const customMenuItems = useMemo( + () => [ + DIVIDER, + { + id: 'export-csv', + label: 'Export to CSV', + onClick: () => setShowExportDialog(true), + icon: , + }, + ], + [], + ); + + useCustomContextItems(customMenuItems); return ( diff --git a/browser/data-browser/src/chunks/TablePage/TableRow.tsx b/browser/data-browser/src/chunks/TablePage/TableRow.tsx index 1db898b0..9f363c71 100644 --- a/browser/data-browser/src/chunks/TablePage/TableRow.tsx +++ b/browser/data-browser/src/chunks/TablePage/TableRow.tsx @@ -2,6 +2,7 @@ import { memo, useEffect, useEffectEvent, + useMemo, useRef, useState, type JSX, @@ -11,13 +12,12 @@ import { DataBrowser, Property, Resource, - core, unknownSubject, useMemberFromCollection, useResource, + useStore, } from '@tomic/react'; import { TableCell } from './TableCell'; -import { randomSubject } from '../../helpers/randomString'; import { styled, keyframes } from 'styled-components'; import { useTableEditorContext } from '@chunks/TableEditor/TableEditorContext'; import { FaTriangleExclamation } from 'react-icons/fa6'; @@ -121,40 +121,39 @@ export function TableNewRow({ parent, invalidateTable, }: TableNewRowProps): JSX.Element { - const [subject] = useState(() => - randomSubject(parent.subject, 'row'), - ); - - const [loading, setLoading] = useState(true); - + const store = useStore(); + const [subject, setSubject] = useState(unknownSubject); const resource = useResource(subject, resourceOpts); - const resourceRef = useRef(resource); + const [loading, setLoading] = useState(true); + const rowClass = useMemo( + () => [parent.props.classtype], + [parent.props.classtype], + ); const onEditNextRow = useTableInvalidation(resource, invalidateTable); useMarkings(resource, index); useEffect(() => { - if (resource.subject === unknownSubject || resource.commitError) { - return; - } - - resourceRef.current - .set(core.properties.parent, parent.subject) - .then(() => - resourceRef.current.set(core.properties.isA, [parent.props.classtype]), - ) - .then(() => { + let cancelled = false; + + store + .newResource({ + parent: parent.subject, + isA: rowClass, + }) + .then(row => { + if (cancelled) { + return; + } + + setSubject(row.subject); setLoading(false); }); - // We can't add resource to the list because we modify the resource in the effect so it would cause a loop. - // We put resource in a ref so we don't need to add it to the list. - }, [ - resource.subject, - parent.subject, - parent.props.classtype, - resource.commitError, - ]); + return () => { + cancelled = true; + }; + }, [parent.subject, rowClass, store]); if (loading) { return ( diff --git a/browser/data-browser/src/chunks/TablePage/helpers/useHandlePaste.ts b/browser/data-browser/src/chunks/TablePage/helpers/useHandlePaste.ts index 5d773397..848ed938 100644 --- a/browser/data-browser/src/chunks/TablePage/helpers/useHandlePaste.ts +++ b/browser/data-browser/src/chunks/TablePage/helpers/useHandlePaste.ts @@ -7,7 +7,6 @@ import { } from '@tomic/react'; import { useCallback } from 'react'; import { CellPasteData } from '@chunks/TableEditor'; -import { randomSubject } from '@helpers/randomString'; import { appendStringToType } from '../dataTypeMaps'; import { HistoryItemBatch, @@ -50,7 +49,6 @@ export function useHandlePaste( shouldInvalidate = true; row = await store.newResource({ - subject: randomSubject(table.subject, 'row'), isA: tableClass.subject, parent: table.subject, propVals: { diff --git a/browser/data-browser/src/chunks/TablePage/useTableData.ts b/browser/data-browser/src/chunks/TablePage/useTableData.ts index cabb8929..99acd3f3 100644 --- a/browser/data-browser/src/chunks/TablePage/useTableData.ts +++ b/browser/data-browser/src/chunks/TablePage/useTableData.ts @@ -4,6 +4,7 @@ import { useCollection, UseCollectionResult, useResource, + useStore, useSubject, } from '@tomic/react'; import { useReducer } from 'react'; @@ -42,6 +43,7 @@ const useTableSorting = () => export function useTableData(resource: Resource): UseTableDataResult { const [sorting, setSortBy] = useTableSorting(); + const store = useStore(); const [classSubject] = useSubject(resource, core.properties.classtype); const tableClass = useResource(classSubject); @@ -57,7 +59,9 @@ export function useTableData(resource: Resource): UseTableDataResult { queryFilter, { pageSize: PAGE_SIZE, - server: new URL(resource.subject).origin, + server: resource.subject.startsWith('http') + ? new URL(resource.subject).origin + : store.getServerUrl(), }, ); diff --git a/browser/data-browser/src/components/AI/OpenRouterLoginButton.tsx b/browser/data-browser/src/components/AI/OpenRouterLoginButton.tsx index 8d8bd4bf..4092f79e 100644 --- a/browser/data-browser/src/components/AI/OpenRouterLoginButton.tsx +++ b/browser/data-browser/src/components/AI/OpenRouterLoginButton.tsx @@ -1,18 +1,19 @@ import { useEffect, useState } from 'react'; +import { sha256 } from '@noble/hashes/sha256'; import { ButtonLink } from '../ButtonLink'; import { paths } from '../../routes/paths'; +import { randomString } from '../../helpers/randomString'; const TEXT = 'Login with OpenRouter'; const AUTH_ENDPOINT = 'https://openrouter.ai/auth'; -async function createSHA256CodeChallenge(input: string): Promise { +function createSHA256CodeChallenge(input: string): string { const encoder = new TextEncoder(); const data = encoder.encode(input); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hash = sha256(data); // Convert ArrayBuffer to base64url string - const byteArray = new Uint8Array(hashBuffer); - const base64String = btoa(String.fromCharCode(...byteArray)); + const base64String = btoa(String.fromCharCode(...hash)); // Convert base64 to base64url return base64String @@ -38,14 +39,10 @@ export const OpenRouterLoginButton = () => { const [challenge, setChallenge] = useState(null); useEffect(() => { - const randomString = crypto.randomUUID(); - createSHA256CodeChallenge(randomString).then(generatedChallenge => { - setChallenge(generatedChallenge); - sessionStorage.setItem( - 'atomic.ai.openrouter-code-verifier', - randomString, - ); - }); + const verifier = crypto.randomUUID ? crypto.randomUUID() : randomString(32); + const generatedChallenge = createSHA256CodeChallenge(verifier); + setChallenge(generatedChallenge); + sessionStorage.setItem('atomic.ai.openrouter-code-verifier', verifier); }, []); if (!challenge) { diff --git a/browser/data-browser/src/components/Dropdown/index.tsx b/browser/data-browser/src/components/Dropdown/index.tsx index 864f8a8b..01066326 100644 --- a/browser/data-browser/src/components/Dropdown/index.tsx +++ b/browser/data-browser/src/components/Dropdown/index.tsx @@ -33,6 +33,7 @@ export type MenuItemMinimial = { id: string; icon?: ReactNode; disabled?: boolean; + header?: boolean; /** Keyboard shortcut helper */ shortcut?: string; }; @@ -323,8 +324,16 @@ export function DropdownMenu({ return ; } - const { label, onClick, helper, id, disabled, shortcut, icon } = - props; + const { + label, + onClick, + helper, + id, + disabled, + shortcut, + icon, + header, + } = props; return ( ); })} @@ -363,6 +373,7 @@ const DropdownPortal = ({ children }: PropsWithChildren) => { export interface MenuItemSidebarProps extends MenuItemMinimial { handleClickItem?: () => unknown; + header?: boolean; } interface MenuItemPropsExtended extends MenuItemSidebarProps { @@ -377,6 +388,7 @@ export function MenuItem({ shortcut, icon, label, + header, ...props }: MenuItemPropsExtended): JSX.Element { const ref = useRef(null); @@ -387,6 +399,15 @@ export function MenuItem({ } }, [selected]); + if (header) { + return ( + + {icon} + {label} + + ); + } + return ( p.theme.colors.main}; + opacity: 0.8; + + & svg { + color: ${p => p.theme.colors.main}; + } +`; + interface MenuItemStyledProps { selected: boolean; } diff --git a/browser/data-browser/src/components/InviteForm.tsx b/browser/data-browser/src/components/InviteForm.tsx index 7e135ae9..f1124348 100644 --- a/browser/data-browser/src/components/InviteForm.tsx +++ b/browser/data-browser/src/components/InviteForm.tsx @@ -7,6 +7,7 @@ import { core, server, } from '@tomic/react'; +import { generateInviteToken } from '@tomic/lib'; import { useCallback, useState } from 'react'; import toast from 'react-hot-toast'; import { ErrorLook } from './ErrorLook'; @@ -33,27 +34,40 @@ export function InviteForm({ target }: InviteFormProps) { const [err, setErr] = useState(undefined); const [agent] = useCurrentAgent(); const [saved, setSaved] = useState(false); + const [inviteUrl, setInviteUrl] = useState(undefined); - /** Stores the Invite, sends it to the server, shows the Subject to the User */ + /** Generates the signed token and constructs the invite URL */ const createInvite = useCallback(async () => { - await invite.set(core.properties.isA, [server.classes.invite]); - await invite.set(core.properties.read, [urls.instances.publicAgent]); - await invite.set(server.properties.target, target.subject); - try { if (!agent) { throw new Error('No agent found'); } - await invite.set(core.properties.parent, agent.subject); - await invite.save(); + const write = (await invite.get(server.properties.write)) as boolean; + const expiresAt = (await invite.get( + urls.properties.invite.expiresAt, + )) as number; + + const tokenBase64 = await generateInviteToken( + target.subject, + agent, + !!write, + expiresAt, + ); + + const baseUrl = store.getServerUrl(); + const finalUrl = `${baseUrl}/invites?token=${encodeURIComponent( + tokenBase64, + )}`; + + setInviteUrl(finalUrl); setSaved(true); - navigator.clipboard.writeText(invite.subject); + navigator.clipboard.writeText(finalUrl); toast.success('Copied to clipboard'); } catch (e) { setErr(e); } - }, [invite, agent, target]); + }, [invite, agent, target, store]); if (!saved) { return ( @@ -85,7 +99,7 @@ export function InviteForm({ target }: InviteFormProps) { return (

Invite created and copied to clipboard! 🚀

- +
); } diff --git a/browser/data-browser/src/components/NetworkIndicator.tsx b/browser/data-browser/src/components/NetworkIndicator.tsx index f3de1c94..7ddc02d5 100644 --- a/browser/data-browser/src/components/NetworkIndicator.tsx +++ b/browser/data-browser/src/components/NetworkIndicator.tsx @@ -1,12 +1,37 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { styled, keyframes } from 'styled-components'; import { MdSignalWifiOff } from 'react-icons/md'; import { useOnline } from '../hooks/useOnline'; import { lighten } from 'polished'; import toast from 'react-hot-toast'; +import { useStore } from '@tomic/react'; + +/** Returns false when the WebSocket has definitively closed (server unreachable). */ +function useWebSocketConnected(): boolean { + const store = useStore(); + const [connected, setConnected] = useState(true); + + useEffect(() => { + const interval = setInterval(() => { + const ws = store.getDefaultWebSocket(); + // CLOSED means the connection failed or dropped — CONNECTING means still trying + setConnected(ws?.readyState !== WebSocket.CLOSED); + }, 1000); + + return () => clearInterval(interval); + }, [store]); + + return connected; +} export function NetworkIndicator() { const isOnline = useOnline(); + const isWSConnected = useWebSocketConnected(); + const isConnected = isOnline && isWSConnected; + + const label = !isOnline + ? 'No internet connection' + : 'Server connection lost — reconnecting…'; useEffect(() => { if (!isOnline) { @@ -14,9 +39,16 @@ export function NetworkIndicator() { } }, [isOnline]); + useEffect(() => { + if (!isWSConnected) { + toast.error('Connection to server lost, reconnecting...'); + } + }, [isWSConnected]); + return ( - - + + + ); } @@ -36,6 +68,20 @@ const pulse = keyframes` } `; +const Label = styled.span` + font-size: 0.8rem; + font-weight: 500; + white-space: nowrap; + max-width: 0; + overflow: hidden; + opacity: 0; + transition: + max-width 0.25s ease, + opacity 0.2s ease, + margin 0.25s ease; + margin-left: 0; +`; + const Wrapper = styled.div` --shadow-color: ${p => lighten(0.15, p.theme.colors.alert)}; position: fixed; @@ -45,19 +91,30 @@ const Wrapper = styled.div` font-size: 1.5rem; color: ${p => p.theme.colors.alert}; pointer-events: ${p => (p.shown ? 'auto' : 'none')}; - transition: opacity 0.1s ease-in-out; + transition: + opacity 0.1s ease-in-out, + border-radius 0.25s ease, + padding 0.25s ease; opacity: ${p => (p.shown ? 1 : 0)}; background-color: ${p => p.theme.colors.bg}; border: 1px solid ${p => p.theme.colors.alert}; - border-radius: 50%; - display: grid; - place-items: center; + border-radius: 2rem; + display: flex; + align-items: center; box-shadow: ${p => p.theme.boxShadowSoft}; padding: 0.5rem; + cursor: default; svg { + flex-shrink: 0; animation: ${pulse} 1.5s alternate ease-in-out infinite; animation-play-state: ${p => (p.shown ? 'running' : 'paused')}; } + + &:hover ${Label}, &:focus-within ${Label} { + max-width: 16rem; + opacity: 1; + margin-left: 0.5rem; + } `; diff --git a/browser/data-browser/src/components/NewIdentitySection.tsx b/browser/data-browser/src/components/NewIdentitySection.tsx new file mode 100644 index 00000000..c976fa7c --- /dev/null +++ b/browser/data-browser/src/components/NewIdentitySection.tsx @@ -0,0 +1,140 @@ +import React, { useEffect, useState } from 'react'; +import { Agent, JSCryptoProvider, core, useStore } from '@tomic/react'; +import { useSettings } from '../helpers/AppSettings'; +import { saveAgentToIDB } from '../helpers/agentStorage'; +import { Button } from './Button'; +import { Column } from './Row'; +import { CodeBlock } from './CodeBlock'; +import { styled } from 'styled-components'; + +interface NewIdentitySectionProps { + /** Called after the agent and drive are created. Use this for any extra server-side steps (e.g. /setup). */ + onAfterCreate?: (driveDID: string) => Promise; + /** Called when the user clicks Done after copying their secret. */ + onDone: () => void; + doneLabel?: string; + /** If true, start creation immediately on mount without showing the button. */ + autoStart?: boolean; +} + +/** + * Shared UI for generating a new Agent + Drive and displaying the resulting secret. + * Used by both the Onboarding page and the Agent Settings page. + */ +export function NewIdentitySection({ + onAfterCreate, + onDone, + doneLabel = "Yes, I've stored it safely", + autoStart = false, +}: NewIdentitySectionProps) { + const store = useStore(); + const { baseURL, setAgent } = useSettings(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(); + const [generatedSecret, setGeneratedSecret] = useState(''); + const [hasCopiedSecret, setHasCopiedSecret] = useState(false); + + useEffect(() => { + if (autoStart) { + handleCreate(); + } + }, []); + + async function handleCreate() { + setLoading(true); + setError(undefined); + + try { + const agentKeys = await Agent.generateKeyPair(); + const agentDID = `did:ad:agent:${agentKeys.publicKey}`; + const agentProvider = new JSCryptoProvider(agentKeys.privateKey); + const newAgent = new Agent(agentProvider, agentDID); + + store.setAgent(newAgent); + + const driveName = new URL(baseURL).host; + const driveResource = await store.newResource({ + isA: 'https://atomicdata.dev/classes/Drive', + noParent: true, + propVals: { + [core.properties.name]: driveName, + [core.properties.write]: [agentDID], + [core.properties.read]: [agentDID], + }, + }); + + await driveResource.save(); + const driveDID = driveResource.subject; + + await onAfterCreate?.(driveDID); + + const finalSecret = Agent.buildSecret( + agentKeys.privateKey, + agentDID, + driveDID, + ); + + await saveAgentToIDB(finalSecret); + setAgent(newAgent); + setGeneratedSecret(finalSecret); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setLoading(false); + } + } + + if (generatedSecret) { + return ( + +

Your new identity is ready

+

+ IMPORTANT: Save this secret key. It is the only way + to access your data if you clear your browser cache or sign in from + another device. +

+ setHasCopiedSecret(true)} + /> + {hasCopiedSecret ? ( + <> +

+ Are you sure you{"'"}ve stored this secret somewhere safe? You + cannot recover it if you lose it. +

+ + + ) : ( + + )} +
+ ); + } + + return ( + +

Create a new identity

+

Generate a new self-sovereign Agent and Drive on this server.

+ {error && {error}} + +
+ ); +} + +const StyledCodeBlock = styled(CodeBlock)` + word-break: break-word; + + & button { + top: ${p => p.theme.size(1)}; + right: ${p => p.theme.size(1)}; + } +`; + +const ErrorText = styled.p` + color: ${p => p.theme.colors.alert}; + margin: 0; +`; diff --git a/browser/data-browser/src/components/ResourceUsage/UsageCard.tsx b/browser/data-browser/src/components/ResourceUsage/UsageCard.tsx index 1860df1e..87b5cc4a 100644 --- a/browser/data-browser/src/components/ResourceUsage/UsageCard.tsx +++ b/browser/data-browser/src/components/ResourceUsage/UsageCard.tsx @@ -47,6 +47,7 @@ export function UsageCard({
{title} diff --git a/browser/data-browser/src/components/SideBar/AppMenu.tsx b/browser/data-browser/src/components/SideBar/AppMenu.tsx index 7341475f..41049083 100644 --- a/browser/data-browser/src/components/SideBar/AppMenu.tsx +++ b/browser/data-browser/src/components/SideBar/AppMenu.tsx @@ -5,7 +5,9 @@ import { FaKeyboard, FaCirclePlus, FaUser, + FaCode, } from 'react-icons/fa6'; +import { isDev } from '../../config'; import { constructOpenURL } from '../../helpers/navigation'; import { useCurrentSubject } from '../../helpers/useCurrentSubject'; import { SideBarMenuItem } from './SideBarMenuItem'; @@ -33,7 +35,6 @@ export function AppMenu({ onItemClick }: AppMenuProps): JSX.Element { const [showInstallButton, setShowInstallButton] = useState(false); const [agent] = useCurrentAgent(); const agentResource = useResource(agent?.subject ?? unknownSubject); - const install = useCallback(() => { if (!event.current) { return; @@ -65,7 +66,7 @@ export function AppMenu({ onItemClick }: AppMenuProps): JSX.Element { label={ agent ? (agentResource.get(core.properties.name) ?? 'User Settings') - : 'Login' + : 'Login / New User' } helper='See and edit the current Agent / User (u)' path={paths.agentSettings} @@ -92,6 +93,15 @@ export function AppMenu({ onItemClick }: AppMenuProps): JSX.Element { path={paths.about} onClick={onItemClick} /> + {isDev() && ( + } + label='Dev Drive' + helper='Create a fresh agent + drive on localhost:9883' + path={paths.devDrive} + onClick={onItemClick} + /> + )} {showInstallButton && ( } diff --git a/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx b/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx index 17bf18f0..af4cae84 100644 --- a/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx +++ b/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx @@ -1,20 +1,25 @@ import { Resource, core, server, useResources } from '@tomic/react'; +import { useMemo } from 'react'; import { FaGear, FaHardDrive, FaPlus, FaSquareCheck, FaRegCircle, + FaServer, } from 'react-icons/fa6'; import { useSettings } from '../../helpers/AppSettings'; import { constructOpenURL } from '../../helpers/navigation'; import { useDriveHistory } from '../../hooks/useDriveHistory'; import { useSavedDrives } from '../../hooks/useSavedDrives'; import { paths } from '../../routes/paths'; -import { DIVIDER, DropdownMenu } from '../Dropdown'; +import { type DropdownItem, DIVIDER, DropdownMenu } from '../Dropdown'; import { buildDefaultTrigger } from '../Dropdown/DefaultTrigger'; import { useNewResourceUI } from '../forms/NewForm/useNewResourceUI'; import { useNavigateWithTransition } from '../../hooks/useNavigateWithTransition'; +import { serverURLStorage } from '../../helpers/serverURLStorage'; + +import { isURL } from '../../helpers/isURL'; const Trigger = buildDefaultTrigger(, 'Open Drive Settings'); @@ -30,7 +35,7 @@ function dedupeAFromB(a: Map, b: Map): Map { export function DriveSwitcher() { const navigate = useNavigateWithTransition(); - const { drive, setDrive, agent } = useSettings(); + const { drive, setDrive, agent, baseURL, setServer } = useSettings(); const [savedDrives] = useSavedDrives(); const [history, addToHistory] = useDriveHistory(savedDrives, 5); @@ -45,50 +50,88 @@ export function DriveSwitcher() { const createNewResource = useNewResourceUI(); - const items = [ - ...Array.from(savedDrivesMap.entries()) - .filter(([_, resource]) => !resource.error) - .map(([subject, resource]) => ({ - id: subject, - label: getTitle(resource), - helper: `Switch to ${getTitle(resource)}`, - disabled: subject === drive, - onClick: () => { - setDrive(subject); - navigate(constructOpenURL(subject)); - }, - icon: subject === drive ? : , + const knownServers = serverURLStorage.getKnownServers(); + const isHttpDrive = isURL(drive); + + const items = useMemo( + () => [ + ...Array.from(savedDrivesMap.entries()) + .filter(([_, resource]) => !resource.error) + .map(([subject, resource]) => ({ + id: subject, + label: getTitle(resource), + helper: `Switch to ${getTitle(resource)}`, + disabled: false, + onClick: (): void => { + setDrive(subject); + navigate(constructOpenURL(subject)); + }, + icon: subject === drive ? : , + })), + { + id: 'new-drive', + label: 'New Drive', + icon: , + helper: 'Create a new drive', + onClick: (): void => + createNewResource(server.classes.drive, agent?.subject ?? ''), + disabled: !agent, + }, + DIVIDER, + // Dedupe history from savedDrives bause not all savedDrives might be loaded yet. + ...Array.from(dedupeAFromB(historyMap, savedDrivesMap)) + .map(([subject, resource]) => ({ + label: getTitle(resource), + id: subject, + helper: `Switch to ${getTitle(resource)}`, + icon: subject === drive ? : , + onClick: buildHandleHistoryDriveClick(subject), + disabled: false, + })) + .slice(0, 5), + DIVIDER, + { + id: 'active-server-header', + label: isHttpDrive ? 'Gateway (Locked to Drive)' : 'Active Gateway', + icon: , + header: true, + onClick: () => undefined, + }, + ...knownServers.map(s => ({ + id: `server-${s}`, + label: s, + helper: isHttpDrive + ? 'Cannot change gateway for HTTP drives' + : `Connect via ${s}`, + disabled: isHttpDrive || s === baseURL, + icon: s === baseURL ? : , + onClick: (): void => setServer(s), })), - DIVIDER, - // Dedupe history from savedDrives bause not all savedDrives might be loaded yet. - ...Array.from(dedupeAFromB(historyMap, savedDrivesMap)) - .map(([subject, resource]) => ({ - label: getTitle(resource), - id: subject, - helper: `Switch to ${getTitle(resource)}`, - icon: subject === drive ? : , - onClick: buildHandleHistoryDriveClick(subject), - disabled: subject === drive, - })) - .slice(0, 5), - DIVIDER, - { - id: 'configure-drives', - label: 'Configure Drives', - icon: , - helper: 'Load drives not displayed in this list.', - onClick: () => navigate(paths.serverSettings), - }, - { - id: 'new-drive', - label: 'New Drive', - icon: , - helper: 'Create a new drive', - onClick: () => - createNewResource(server.classes.drive, agent?.subject ?? ''), - disabled: !agent, - }, - ]; + DIVIDER, + { + id: 'configure-drives', + label: 'Configure', + icon: , + helper: 'Load drives not displayed in this list.', + onClick: (): void => { + void navigate(paths.serverSettings); + }, + }, + ], + [ + agent, + baseURL, + createNewResource, + drive, + historyMap, + isHttpDrive, + knownServers, + navigate, + savedDrivesMap, + setDrive, + setServer, + ], + ); return ; } diff --git a/browser/data-browser/src/components/SideBar/SideBarDrive.tsx b/browser/data-browser/src/components/SideBar/SideBarDrive.tsx index bec9d44b..83c951f8 100644 --- a/browser/data-browser/src/components/SideBar/SideBarDrive.tsx +++ b/browser/data-browser/src/components/SideBar/SideBarDrive.tsx @@ -88,7 +88,7 @@ export function SideBarDrive({ }} > - {driveName}{' '} + {driveName} diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx index 79b1c882..15b66488 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx @@ -1,4 +1,9 @@ -import { core, useStore, server, dataBrowser } from '@tomic/react'; +import { + core, + useStore, + server, + dataBrowser, +} from '@tomic/react'; import { useState, useCallback, FormEvent, FC, useEffect, useId } from 'react'; import { styled } from 'styled-components'; import { stringToSlug } from '../../../../../helpers/stringToSlug'; @@ -16,6 +21,8 @@ import { CustomResourceDialogProps } from '../../useNewResourceUI'; import { useCreateAndNavigate } from '../../../../../hooks/useCreateAndNavigate'; import { useSettings } from '../../../../../helpers/AppSettings'; +const SUBDOMAIN = 'https://atomicdata.dev/properties/subdomain'; + export const NewDriveDialog: FC = ({ onClose, onCreated, @@ -23,8 +30,17 @@ export const NewDriveDialog: FC = ({ }) => { const store = useStore(); const nameFieldId = useId(); + const subdomainFieldId = useId(); const { setDrive } = useSettings(); const [name, setName] = useState(''); + const [subdomain, setSubdomain] = useState(''); + const [subdomainEdited, setSubdomainEdited] = useState(false); + + useEffect(() => { + if (!subdomainEdited) { + setSubdomain(stringToSlug(name)); + } + }, [name, subdomainEdited]); const createAndNavigate = useCreateAndNavigate(); @@ -45,20 +61,25 @@ export const NewDriveDialog: FC = ({ [core.properties.name]: name, [core.properties.write]: [agent.subject], [core.properties.read]: [agent.subject], + [SUBDOMAIN]: subdomain.trim(), }, { noParent: true, skipNavigation, onCreated: async resource => { // Add drive to the agents drive list. - const agentResource = await store.getResource(agent.subject!); - agentResource.push(server.properties.drives, [resource.subject]); - await agentResource.save(); + // DID agents may not have a local resource, so we ignore errors here. + try { + const agentResource = await store.getResource(agent.subject!); + agentResource.push(server.properties.drives, [resource.subject]); + await agentResource.save(); + } catch (_e) { + // Agent resource update failed (e.g., DID agents don't have a local resource) + } // Create a default ontology. const ontologyName = stringToSlug(name.trim()); const ontology = await store.newResource({ - subject: `${resource.subject}/defaultOntology`, isA: core.classes.ontology, parent: resource.subject, propVals: { @@ -93,6 +114,7 @@ export const NewDriveDialog: FC = ({ onClose(); }, [ name, + subdomain, createAndNavigate, onClose, setDrive, @@ -130,6 +152,19 @@ export const NewDriveDialog: FC = ({ /> + + + { + setSubdomain(e.target.value); + setSubdomainEdited(true); + }} + /> + + diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx index 0f6864e7..c9c33d1c 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx @@ -1,7 +1,8 @@ -import { dataBrowser, core, useStore } from '@tomic/react'; -import { useState, useCallback, useEffect, FormEvent, FC } from 'react'; +import { dataBrowser, core, type Server, useStore } from '@tomic/react'; +import { useState, useCallback, useEffect, useRef, FormEvent, FC } from 'react'; import { styled } from 'styled-components'; import { stringToSlug } from '../../../../../helpers/stringToSlug'; +import { useSettings } from '../../../../../helpers/AppSettings'; import { BetaBadge } from '../../../../BetaBadge'; import { Button } from '../../../../Button'; import { @@ -31,12 +32,14 @@ export const NewTableDialog: FC = ({ onCreated, }) => { const store = useStore(); + const { drive: driveSubject } = useSettings(); const [useExistingClass, setUseExistingClass] = useState(!!initialExistingClass); const [existingClass, setExistingClass] = useState( initialExistingClass, ); - const [name, setName] = useState(''); + const [name, setName] = useState('table'); + const nameInputRef = useRef(null); const addToOntology = useAddToOntology(); const createResourceAndNavigate = useCreateAndNavigate(); @@ -49,7 +52,16 @@ export const NewTableDialog: FC = ({ let classSubject: string; if (!useExistingClass) { + const drive = await store.getResource(driveSubject); + const ontologyParent = drive.props.defaultOntology; + const parentSubject = + ontologyParent && + !ontologyParent.startsWith('internal:') && + !ontologyParent.includes('unknown-subject') + ? ontologyParent + : driveSubject; const instanceResource = await store.newResource({ + parent: parentSubject, isA: core.classes.class, propVals: { [core.properties.shortname]: stringToSlug(name), @@ -94,6 +106,7 @@ export const NewTableDialog: FC = ({ skipNavigation, onCreated, store, + driveSubject, ]); const [dialogProps, show, hide, isOpen] = useDialog({ onCancel, onSuccess }); @@ -102,6 +115,13 @@ export const NewTableDialog: FC = ({ show(); }, []); + useEffect(() => { + if (isOpen) { + nameInputRef.current?.focus(); + nameInputRef.current?.select(); + } + }, [isOpen]); + const hasName = name.trim() !== ''; const saveDisabled = useExistingClass ? !hasName || !existingClass : !hasName; @@ -123,9 +143,9 @@ export const NewTableDialog: FC = ({ setName(e.target.value)} /> diff --git a/browser/data-browser/src/components/forms/NewForm/SubjectField.tsx b/browser/data-browser/src/components/forms/NewForm/SubjectField.tsx index a80025bf..5b0947ac 100644 --- a/browser/data-browser/src/components/forms/NewForm/SubjectField.tsx +++ b/browser/data-browser/src/components/forms/NewForm/SubjectField.tsx @@ -7,6 +7,8 @@ export interface SubjectFieldProps { error?: Error; value: string; onChange: (value: string) => void; + /** When true the field is read-only (e.g. for DID subjects). */ + readOnly?: boolean; } const getPath = (value: string) => { @@ -25,10 +27,34 @@ const normalizePath = (str: string) => { return '/' + str; }; -export function SubjectField({ error, value, onChange }: SubjectFieldProps) { - const [origin, path] = getPath(value); +export function SubjectField({ + error, + value, + onChange, + readOnly, +}: SubjectFieldProps) { + // DID subjects can't be parsed as URLs and are deterministic — show them + // as plain read-only text. + const isDID = value.startsWith('did:') || value.startsWith('_'); + const isReadOnly = isDID || readOnly; + + const [origin, path] = isReadOnly ? ['', ''] : getPath(value); const [inputValue, setInputValue] = useState(path); + if (isReadOnly) { + return ( + + + {value} + + + ); + } + const handleChange = (v: string) => { const subject = new URL(normalizePath(v), value); setInputValue(subject.pathname.slice(1)); @@ -62,6 +88,17 @@ const OriginPart = styled.span` color: ${p => p.theme.colors.textLight}; `; +const ReadOnlySubject = styled.span` + height: 2rem; + display: flex; + align-items: center; + padding-inline: 0.5rem; + font-family: monospace; + font-size: 0.85em; + color: ${p => p.theme.colors.textLight}; + word-break: break-all; +`; + const StyledInputStyled = styled(InputStyled)` && { border-radius: 0; diff --git a/browser/data-browser/src/handlers/sideBarHandler.ts b/browser/data-browser/src/handlers/sideBarHandler.ts index fe99bf88..3e65cb3d 100644 --- a/browser/data-browser/src/handlers/sideBarHandler.ts +++ b/browser/data-browser/src/handlers/sideBarHandler.ts @@ -34,6 +34,11 @@ export function buildSideBarNewResourceHandler(store: Store) { export function buildSideBarRemoveResourceHandler(store: Store) { // When a resource is deleted remove it from the parents subResources list. return async (subject: string) => { + // Temporary subjects are never persisted in subResources lists. + if (subject.startsWith('_new:') || subject.startsWith('_local:')) { + return; + } + const collection = new CollectionBuilder(store) .setProperty(dataBrowser.properties.subResources) .setValue(subject) diff --git a/browser/data-browser/src/helpers/AppSettings.tsx b/browser/data-browser/src/helpers/AppSettings.tsx index 90228511..9ae3e7df 100644 --- a/browser/data-browser/src/helpers/AppSettings.tsx +++ b/browser/data-browser/src/helpers/AppSettings.tsx @@ -4,6 +4,7 @@ import { useCallback, useContext, useMemo, + useEffect, type JSX, } from 'react'; import { DarkModeOption, useDarkMode } from './useDarkMode'; @@ -13,6 +14,7 @@ import { SIDEBAR_TOGGLE_WIDTH } from '../components/SideBar'; import { serverURLStorage } from './serverURLStorage'; import { useLocalStorage } from '../hooks/useLocalStorage'; import { errorHandler } from '../handlers/errorHandler'; +import { isDev } from '../config'; interface ProviderProps { children: ReactNode; @@ -52,12 +54,34 @@ export const AppSettingsContextProvider = ( const [sidebarKeyboardDndEnabled, setSidebarKeyboardDndEnabled] = useLocalStorage('sidebarKeyboardDndEnabled', false); + useEffect(() => { + const currentOrigin = isDev() + ? 'http://localhost:9883' + : window.location.origin; + + serverURLStorage.addKnownServer(currentOrigin); + }, []); + + const setServer = useCallback( + (newServer: string) => { + if (newServer.startsWith('http://') || newServer.startsWith('https://')) { + const url = new URL(newServer); + setBaseURL(url.origin); + serverURLStorage.set(url.origin); + } + }, + [setBaseURL], + ); + const setDrive = useCallback( (newDrive: string) => { - const url = new URL(newDrive); innerSetDrive(newDrive); - setBaseURL(url.origin); - serverURLStorage.set(url.origin); + + if (newDrive.startsWith('http://') || newDrive.startsWith('https://')) { + const url = new URL(newDrive); + setBaseURL(url.origin); + serverURLStorage.set(url.origin); + } }, [innerSetDrive, setBaseURL], ); @@ -104,6 +128,9 @@ export const AppSettingsContextProvider = ( setSidebarKeyboardDndEnabled, hideTemplates, setHideTemplates, + baseURL, + setBaseURL, + setServer, }), [ drive, @@ -127,6 +154,9 @@ export const AppSettingsContextProvider = ( setSidebarKeyboardDndEnabled, hideTemplates, setHideTemplates, + baseURL, + setBaseURL, + setServer, ], ); @@ -171,6 +201,12 @@ export interface AppSettings { setSidebarKeyboardDndEnabled: (b: boolean) => void; hideTemplates: boolean; setHideTemplates: (b: boolean) => void; + /** The URL of the currently active server / peer used for resolution. */ + baseURL: string; + /** Sets the active server / peer. */ + setBaseURL: (s: string) => void; + /** Robustly sets the server and adds it to the known list. */ + setServer: (s: string) => void; } const initialState: AppSettings = { @@ -195,6 +231,9 @@ const initialState: AppSettings = { setSidebarKeyboardDndEnabled: () => undefined, hideTemplates: false, setHideTemplates: () => undefined, + baseURL: '', + setBaseURL: () => undefined, + setServer: () => undefined, }; /** Hook for using App Settings, such as theme and darkmode */ diff --git a/browser/data-browser/src/helpers/serverURLStorage.ts b/browser/data-browser/src/helpers/serverURLStorage.ts index 42122ffc..248516cd 100644 --- a/browser/data-browser/src/helpers/serverURLStorage.ts +++ b/browser/data-browser/src/helpers/serverURLStorage.ts @@ -1,8 +1,12 @@ +import { isDev } from '../config'; + const ServerURLStorageKEY = 'serverUrl'; +const KnownServersKEY = 'knownServers'; export const serverURLStorage = { set(url: string) { localStorage.setItem(ServerURLStorageKEY, JSON.stringify(url)); + this.addKnownServer(url); }, get() { try { @@ -13,4 +17,42 @@ export const serverURLStorage = { return undefined; } }, + addKnownServer(url: string) { + if (!url) return; + try { + const urlObj = new URL(url); + const origin = urlObj.origin; + const known = this.getKnownServers(); + if (!known.includes(origin)) { + localStorage.setItem( + KnownServersKEY, + JSON.stringify([...known, origin]), + ); + } + } catch (e) { + // Not a valid URL, ignore + } + }, + getKnownServers(): string[] { + try { + const val = localStorage.getItem(KnownServersKEY); + if (!val) return []; + const servers = JSON.parse(val) as string[]; + + if (!isDev()) { + return servers; + } + + return servers.filter(server => server !== window.location.origin); + } catch (e) { + return []; + } + }, + removeKnownServer(url: string) { + const known = this.getKnownServers(); + localStorage.setItem( + KnownServersKEY, + JSON.stringify(known.filter((s: string) => s !== url)), + ); + }, }; diff --git a/browser/data-browser/src/hooks/useAddToOntology.ts b/browser/data-browser/src/hooks/useAddToOntology.ts index 8dc747a1..e6b5c4de 100644 --- a/browser/data-browser/src/hooks/useAddToOntology.ts +++ b/browser/data-browser/src/hooks/useAddToOntology.ts @@ -22,7 +22,12 @@ export function useAddToOntology(ontologySubject?: string) { return useCallback( async (resource: Resource) => { - if (ontology.subject === unknownSubject) { + const hasResolvedOntologySubject = + ontology.subject !== unknownSubject && + !ontology.subject.startsWith('internal:') && + !ontology.subject.includes('unknown-subject'); + + if (!hasResolvedOntologySubject) { await resource.set(core.properties.parent, driveSubject); resource.save(); diff --git a/browser/data-browser/src/hooks/useCreateAndNavigate.ts b/browser/data-browser/src/hooks/useCreateAndNavigate.ts index 547a3ad3..1f06dba2 100644 --- a/browser/data-browser/src/hooks/useCreateAndNavigate.ts +++ b/browser/data-browser/src/hooks/useCreateAndNavigate.ts @@ -1,4 +1,4 @@ -import { Core, JSONValue, Resource, useStore } from '@tomic/react'; +import { JSONValue, Resource, useStore } from '@tomic/react'; import { useCallback } from 'react'; import toast from 'react-hot-toast'; import { constructOpenURL } from '../helpers/navigation'; @@ -36,7 +36,9 @@ export function useCreateAndNavigate(): CreateAndNavigate { propVals, { parent, extraParams, onCreated, subject, noParent, skipNavigation }, ): Promise => { - const classResource = await store.getResource(isA); + const classTitle = store + .getResourceLoading(isA) + ?.title?.replace(/^https?:\/\/[^/]+\/classes\//, '') ?? 'Resource'; const resource = await store.newResource({ subject, @@ -50,7 +52,7 @@ export function useCreateAndNavigate(): CreateAndNavigate { await resource.save(); if (onCreated) { - onCreated(resource); + await onCreated(resource); } if (!skipNavigation) { @@ -59,7 +61,7 @@ export function useCreateAndNavigate(): CreateAndNavigate { }); } - toast.success(`${classResource.title} created`); + toast.success(`${classTitle} created`); store.notifyResourceManuallyCreated(resource); } catch (e) { store.notifyError(e); diff --git a/browser/data-browser/src/hooks/useDevDrive.ts b/browser/data-browser/src/hooks/useDevDrive.ts new file mode 100644 index 00000000..940fa063 --- /dev/null +++ b/browser/data-browser/src/hooks/useDevDrive.ts @@ -0,0 +1,62 @@ +import { Agent, JSCryptoProvider, core, server, useStore } from '@tomic/react'; +import { useCallback, useState } from 'react'; +import { useSettings } from '../helpers/AppSettings'; +import { saveAgentToIDB } from '../helpers/agentStorage'; +import { constructOpenURL } from '../helpers/navigation'; +import { useNavigateWithTransition } from './useNavigateWithTransition'; + +export const DEV_SERVER = 'http://localhost:9883'; +export const DEV_DRIVE_TESTID = 'dev-drive-button'; + +/** + * Creates a fresh agent and drive on the local dev server (localhost:9883) and + * switches to it. Only intended for development / E2E-test use. + */ +export function useDevDrive() { + const store = useStore(); + const { setAgent, setDrive, setServer } = useSettings(); + const navigate = useNavigateWithTransition(); + const [loading, setLoading] = useState(false); + + const createDevDrive = useCallback(async () => { + setLoading(true); + + try { + setServer(DEV_SERVER); + + const agentKeys = await Agent.generateKeyPair(); + const agentDID = `did:ad:agent:${agentKeys.publicKey}`; + const agentProvider = new JSCryptoProvider(agentKeys.privateKey); + const newAgent = new Agent(agentProvider, agentDID); + + store.setAgent(newAgent); + + const driveResource = await store.newResource({ + isA: server.classes.drive, + noParent: true, + propVals: { + [core.properties.name]: 'dev', + [core.properties.write]: [agentDID], + [core.properties.read]: [agentDID], + }, + }); + + await driveResource.save(); + + const finalSecret = Agent.buildSecret( + agentKeys.privateKey, + agentDID, + driveResource.subject, + ); + + await saveAgentToIDB(finalSecret); + setAgent(newAgent); + setDrive(driveResource.subject); + navigate(constructOpenURL(driveResource.subject)); + } finally { + setLoading(false); + } + }, [store, setAgent, setDrive, setServer, navigate]); + + return { createDevDrive, loading }; +} diff --git a/browser/data-browser/src/hooks/useDriveHistory.ts b/browser/data-browser/src/hooks/useDriveHistory.ts index ae6307ae..3d0dc39a 100644 --- a/browser/data-browser/src/hooks/useDriveHistory.ts +++ b/browser/data-browser/src/hooks/useDriveHistory.ts @@ -1,5 +1,4 @@ import { useCallback, useMemo } from 'react'; -import { useSavedDrives } from './useSavedDrives'; import { useLocalStorage } from './useLocalStorage'; const MAX_DRIVE_HISTORY = 5; @@ -12,7 +11,6 @@ export function useDriveHistory( addDriveToHistory: (drive: string) => void, removeFromHistory: (drive: string) => void, ] { - const [savedDrives] = useSavedDrives(); const [driveHistory, setDriveHistory] = useLocalStorage( 'driveHistory', [], @@ -31,7 +29,7 @@ export function useDriveHistory( ); }); }, - [savedDrives, setDriveHistory], + [setDriveHistory], ); const removeFromHistory = useCallback( diff --git a/browser/data-browser/src/hooks/useSavedDrives.ts b/browser/data-browser/src/hooks/useSavedDrives.ts index a58e1b2e..fdd9dc16 100644 --- a/browser/data-browser/src/hooks/useSavedDrives.ts +++ b/browser/data-browser/src/hooks/useSavedDrives.ts @@ -2,12 +2,20 @@ import { urls, useArray, useResource } from '@tomic/react'; import { useCallback, useMemo } from 'react'; import { isDev } from '../config'; import { useSettings } from '../helpers/AppSettings'; +import { serverURLStorage } from '../helpers/serverURLStorage'; -const rootDrives = [ - window.location.origin, - 'https://atomicdata.dev', - ...(isDev() ? ['http://localhost:9883'] : []), -]; +const getRootDrives = () => { + const known = serverURLStorage.getKnownServers(); + const current = isDev() ? 'http://localhost:9883' : window.location.origin; + + const roots = new Set([current, ...known]); + + if (isDev()) { + roots.add('http://localhost:9883'); + } + + return Array.from(roots); +}; const arrayOpts = { commit: true, @@ -26,7 +34,12 @@ export function useSavedDrives(): [ arrayOpts, ); - const extraDrives = useMemo(() => [...rootDrives, ...drives], [drives]); + const rootDrives = useMemo(() => getRootDrives(), []); + const extraDrives = useMemo(() => { + const all = new Set([...rootDrives, ...drives]); + + return Array.from(all); + }, [drives, rootDrives]); const add = useCallback( (drive: string) => { @@ -41,7 +54,7 @@ export function useSavedDrives(): [ }); } }, - [drives, setDrives], + [drives, setDrives, rootDrives], ); const remove = useCallback( @@ -57,7 +70,7 @@ export function useSavedDrives(): [ }); } }, - [drives, setDrives], + [drives, setDrives, rootDrives], ); return [extraDrives, add, remove]; diff --git a/browser/data-browser/src/index.tsx b/browser/data-browser/src/index.tsx index c8e3f889..94af88d3 100644 --- a/browser/data-browser/src/index.tsx +++ b/browser/data-browser/src/index.tsx @@ -1,7 +1,42 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; +import { sha256 } from '@noble/hashes/sha256'; import App from './App'; +/** + * Polyfill for crypto.subtle.digest in non-secure contexts (e.g., local IPs). + * Some dependencies like @openrouter/sdk and hashery use this, but browsers + * disable it on anything but localhost/HTTPS. + */ +if (typeof window !== 'undefined' && (!window.crypto || !window.crypto.subtle || !window.crypto.subtle.digest)) { + console.warn('Atomic Server: Providing a polyfill for crypto.subtle.digest in an insecure context.'); + + // Ensure the object hierarchy exists + if (!window.crypto) { + // @ts-ignore + window.crypto = {}; + } + if (!window.crypto.subtle) { + // @ts-ignore + window.crypto.subtle = {}; + } + + // Only patch if missing (though the outer IF already checks this) + if (!window.crypto.subtle.digest) { + window.crypto.subtle.digest = async (algorithm, data) => { + const algoStr = typeof algorithm === 'string' ? algorithm.toUpperCase() : (algorithm as any).name.toUpperCase(); + + if (algoStr === 'SHA-256' || algoStr === 'SHA256') { + const input = data instanceof Uint8Array ? data : new Uint8Array(data as ArrayBuffer); + const hash = sha256(input); + return hash.buffer; + } + + throw new Error(`Polyfill: Unsupported hash algorithm: ${algoStr}. Only SHA-256 is supported in this context.`); + }; + } +} + const root = createRoot(document.getElementById('root')!); root.render( diff --git a/browser/data-browser/src/locales/de.po b/browser/data-browser/src/locales/de.po index b5fb0519..1f4d6dd5 100644 --- a/browser/data-browser/src/locales/de.po +++ b/browser/data-browser/src/locales/de.po @@ -37,7 +37,6 @@ msgstr "Keine Ergebnisse" #: src/chunks/Plugins/NewPluginButton.tsx #: src/chunks/Plugins/UpdatePluginButton.tsx #: src/chunks/TablePage/PropertyForm/ExternalPropertyDialog.tsx -#: src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/ConfirmationDialog.tsx #: src/components/ParentPicker/ParentPickerDialog.tsx #: src/components/forms/EditFormDialog.tsx @@ -117,7 +116,6 @@ msgstr "Einladungstext (optional)" msgid "Limit Usages (optional)" msgstr "Nutzungen beschränken (optional)" -#: src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/InviteForm.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx @@ -160,9 +158,8 @@ msgstr "Der einfachste Weg, Linked Data zu erstellen und zu teilen." msgid "You are offline, changes might not be persisted." msgstr "Sie sind offline, Änderungen werden möglicherweise nicht gespeichert." -#: src/components/NetworkIndicator.tsx -msgid "No Internet Connection." -msgstr "Keine Internetverbindung." +#~ msgid "No Internet Connection." +#~ msgstr "Keine Internetverbindung." #: src/components/Parent.tsx msgid "Toggle AI panel" @@ -596,7 +593,6 @@ msgstr "<0/> Sie sind angemeldet als" msgid "Edit profile" msgstr "Profil bearbeiten" -#: src/routes/SettingsAgent.tsx #: src/views/InvitePage.tsx msgid "Agent Secret" msgstr "Agenten-Geheimnis" @@ -724,9 +720,8 @@ msgstr "Ruft die URL von Ihrem aktuellen Atomic-Server ({0}) ab, anstatt von der msgid "{0} endpoint" msgstr "{0} Endpunkt" -#: src/views/EndpointPage.tsx -msgid "Go" -msgstr "Los" +#~ msgid "Go" +#~ msgstr "Los" #: src/routes/Search/SearchRoute.tsx #: src/views/EndpointPage.tsx @@ -1032,6 +1027,7 @@ msgid "This URL will be used as the default Parent for imported resources." msgstr "Diese URL wird als Standard-Elternteil für importierte Ressourcen verwendet." #: src/views/ImporterPage.tsx +#: src/views/OnboardingPage.tsx msgid "Importing..." msgstr "Importiere..." @@ -1252,6 +1248,7 @@ msgid "Edit permissions and create invites." msgstr "Berechtigungen bearbeiten und Einladungen erstellen." #: src/components/ResourceContextMenu/index.tsx +#: src/routes/SettingsServer/index.tsx msgid "History" msgstr "Verlauf" @@ -1354,9 +1351,8 @@ msgstr "Sandbox, teste Komponenten isoliert" msgid "No tags found" msgstr "Keine Tags gefunden" -#: src/components/SideBar/AppMenu.tsx -msgid "Login" -msgstr "Anmelden" +#~ msgid "Login" +#~ msgstr "Anmelden" #: src/components/SideBar/AppMenu.tsx msgid "See and edit the current Agent / User (u)" @@ -1401,9 +1397,8 @@ msgstr "App-Menü" msgid "Switch to {0}" msgstr "Zu {0} wechseln" -#: src/components/SideBar/DriveSwitcher.tsx -msgid "Configure Drives" -msgstr "Laufwerke konfigurieren" +#~ msgid "Configure Drives" +#~ msgstr "Laufwerke konfigurieren" #: src/components/SideBar/DriveSwitcher.tsx msgid "Load drives not displayed in this list." @@ -1549,7 +1544,6 @@ msgstr "{0} bearbeiten" #: src/components/forms/EditFormDialog.tsx #: src/components/forms/NewForm/NewFormDialog.tsx #: src/components/forms/ResourceForm.tsx -#: src/routes/SettingsServer/index.tsx #: src/routes/Share/ShareRoute.tsx #: src/views/Article/ArticleDescription.tsx #: src/views/OntologyPage/NewClassButton.tsx @@ -2257,6 +2251,7 @@ msgid "Edit raw markdown" msgstr "Rohes Markdown bearbeiten" #: src/chunks/RTE/EditLinkForm.tsx +#: src/routes/SettingsServer/index.tsx msgid "Set" msgstr "Setzen" @@ -2414,17 +2409,14 @@ msgstr "Schreiben" msgid "Drive Configuration" msgstr "Laufwerkskonfiguration" -#: src/routes/SettingsServer/index.tsx -msgid "Current Drive" -msgstr "Aktuelles Laufwerk" +#~ msgid "Current Drive" +#~ msgstr "Aktuelles Laufwerk" -#: src/routes/SettingsServer/index.tsx -msgid "Saved" -msgstr "Gespeichert" +#~ msgid "Saved" +#~ msgstr "Gespeichert" -#: src/routes/SettingsServer/index.tsx -msgid "Other" -msgstr "Andere" +#~ msgid "Other" +#~ msgstr "Andere" #: src/components/SideBar/OntologySideBar/OntologiesPanel.tsx msgid "Invalid Resource" @@ -2528,6 +2520,7 @@ msgstr "Die Kennung der Ressource. Dies bestimmt standardmäßig auch, wo die Re #: src/chunks/RTE/CollaborativeEditor.tsx #: src/components/forms/NewForm/NewFormTitle.tsx +#: src/hooks/useCreateAndNavigate.ts #: src/views/Plugin/AssignRights.tsx msgid "Resource" msgstr "Ressource" @@ -2701,13 +2694,11 @@ msgstr "Hinzufügen" msgid "Edit Column" msgstr "Spalte bearbeiten" -#: src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx -msgid "A column in a table" -msgstr "Eine Spalte in einer Tabelle" +#~ msgid "A column in a table" +#~ msgstr "Eine Spalte in einer Tabelle" -#: src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx -msgid "New <0/> Column" -msgstr "Neue <0/> Spalte" +#~ msgid "New <0/> Column" +#~ msgstr "Neue <0/> Spalte" #: src/chunks/TablePage/PropertyForm/RelationPropertyForm.tsx msgid "Resource type:" @@ -2847,9 +2838,8 @@ msgstr "" "Der Titel wird verwendet, um das Subjekt zu erstellen. Bitte beachte, dass\n" "das Subjekt später nicht mehr geändert werden kann." -#: src/chunks/AI/AIChatMessageParts/UserMessage.tsx -msgid "You" -msgstr "Du" +#~ msgid "You" +#~ msgstr "Du" #. placeholder {0}: name #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx @@ -3301,18 +3291,16 @@ msgstr "Keine Plugins installiert" msgid "Sign Out" msgstr "Abmelden" -#. placeholder {0}: ' ' -#. placeholder {1}: "'s" -#: src/routes/SettingsAgent.tsx -msgid "" -"You can create your own Agent by hosting an{0} <0>atomic-server . Alternatively, you can use an Invite to get a guest Agent on\n" -"someone else{1} Atomic Server." -msgstr "Sie können Ihren eigenen Agent erstellen, indem Sie einen{0} <0>atomic-server hosten. Alternativ können Sie eine Einladung verwenden, um einen Gast-Agent auf dem Atomic Server eines anderen zu erhalten." +#~ msgid "" +#~ "You can create your own Agent by hosting an{0} <0>atomic-server . Alternatively, you can use an Invite to get a guest Agent on\n" +#~ "someone else{1} Atomic Server." +#~ msgstr "Sie können Ihren eigenen Agent erstellen, indem Sie einen{0} <0>atomic-server hosten. Alternativ können Sie eine Einladung verwenden, um einen Gast-Agent auf dem Atomic Server eines anderen zu erhalten." #: src/views/InvitePage.tsx msgid "Agent created!" msgstr "Agent erstellt!" +#: src/views/InvitePage.tsx #: src/views/InvitePage.tsx msgid "Continue" msgstr "Weiter" @@ -3321,12 +3309,11 @@ msgstr "Weiter" msgid "Copy secret to continue" msgstr "Geheimnis kopieren, um fortzufahren" -#: src/views/InvitePage.tsx -msgid "" -"IMPORTANT! Below is your agent secret, you use this to login. Save\n" -"it somewhere safe, the secret will not be show again and if you\n" -"lose it you will not be able to access this user again." -msgstr "WICHTIG! Unten ist Ihr Agenten-Geheimnis, welches Sie zum Einloggen verwenden. Speichern Sie es an einem sicheren Ort. Das Geheimnis wird nicht erneut angezeigt und wenn Sie es verlieren, können Sie nicht mehr auf diesen Benutzer zugreifen." +#~ msgid "" +#~ "IMPORTANT! Below is your agent secret, you use this to login. Save\n" +#~ "it somewhere safe, the secret will not be show again and if you\n" +#~ "lose it you will not be able to access this user again." +#~ msgstr "WICHTIG! Unten ist Ihr Agenten-Geheimnis, welches Sie zum Einloggen verwenden. Speichern Sie es an einem sicheren Ort. Das Geheimnis wird nicht erneut angezeigt und wenn Sie es verlieren, können Sie nicht mehr auf diesen Benutzer zugreifen." #: src/views/InvitePage.tsx msgid "Enter a name" @@ -3340,11 +3327,10 @@ msgstr "Agentenname" msgid "This drive is private, sign in to view it" msgstr "Dieses Laufwerk ist privat, melden Sie sich an, um es anzuzeigen" -#: src/routes/SettingsAgent.tsx -msgid "" -"An Agent is a user, consisting of a Subject (its URL) and Private Key.\n" -"Together, these can be used to edit data and sign Commits." -msgstr "Ein Agent ist ein Benutzer, der aus einem Subjekt (seiner URL) und einem privaten Schlüssel besteht. Zusammen können diese verwendet werden, um Daten zu bearbeiten und Commits zu signieren." +#~ msgid "" +#~ "An Agent is a user, consisting of a Subject (its URL) and Private Key.\n" +#~ "Together, these can be used to edit data and sign Commits." +#~ msgstr "Ein Agent ist ein Benutzer, der aus einem Subjekt (seiner URL) und einem privaten Schlüssel besteht. Zusammen können diese verwendet werden, um Daten zu bearbeiten und Commits zu signieren." #: src/views/Plugin/AssignRights.tsx msgid "Pick a resource" @@ -3425,3 +3411,345 @@ msgid "" "<0/> wants to modify a resource that is not\n" "contained in the current scope." msgstr "<0/> möchte eine Ressource ändern, die nicht im aktuellen Geltungsbereich enthalten ist." + +#: src/routes/Search/SearchRoute.tsx +msgid "Searching for <0/>..." +msgstr "Suche nach <0/>..." + +#: src/views/InvitePage.tsx +msgid "Failed to add invited drive to agent" +msgstr "Hinzufügen des eingeladenen Laufwerks zum Agenten fehlgeschlagen" + +#: src/views/InvitePage.tsx +msgid "Failed to persist agent after accepting invite" +msgstr "Fehler beim Speichern des Agenten nach Annahme der Einladung" + +#: src/views/InvitePage.tsx +msgid "Invite accepted, but no destination was returned." +msgstr "Einladung angenommen, aber kein Ziel zurückgegeben." + +#: src/components/forms/NewForm/SubjectField.tsx +msgid "The identifier of the resource. DID subjects are determined by the genesis commit signature." +msgstr "Der Bezeichner der Ressource. DID-Subjekte werden durch die Genesis-Commit-Signatur bestimmt." + +#: src/components/NetworkIndicator.tsx +msgid "Connection to server lost, reconnecting..." +msgstr "Verbindung zum Server verloren, es wird wieder verbunden ..." + +#~ msgid "Server connection lost." +#~ msgstr "" + +#: src/components/NetworkIndicator.tsx +msgid "No internet connection" +msgstr "Keine Internetverbindung" + +#: src/components/NetworkIndicator.tsx +msgid "Server connection lost — reconnecting…" +msgstr "Serververbindung verloren – es wird wieder verbunden ..." + +#: src/views/InvitePage.tsx +msgid "" +"IMPORTANT! Below is your agent secret, you use this to login.\n" +"Save it somewhere safe, the secret will not be show again and if\n" +"you lose it you will not be able to access this user again." +msgstr "" +"WICHTIG! Unten steht Ihr Agenten-Geheimnis, das Sie zum Anmelden verwenden.\n" +"Speichern Sie es an einem sicheren Ort, das Geheimnis wird nicht wieder angezeigt und wenn\n" +"Sie es verlieren, können Sie diesen Benutzer nicht wieder erreichen." + +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx +msgid "Subdomain" +msgstr "Subdomain" + +#~ msgid "This secret does not contain an initial drive link. You might need to set it up manually or create a new account." +#~ msgstr "" + +#~ msgid "Initial Drive" +#~ msgstr "" + +#~ msgid "My first decentralized drive" +#~ msgstr "" + +#~ msgid "Welcome to Atomic Server" +#~ msgstr "" + +#~ msgid "This server node is currently uninitialized for <0/>." +#~ msgstr "" + +#~ msgid "I already have an account (Paste Secret)" +#~ msgstr "" + +#~ msgid "Create a new Account & Drive" +#~ msgstr "" + +#~ msgid "Paste your Agent Secret" +#~ msgstr "" + +#~ msgid "Back" +#~ msgstr "" + +#: src/views/OnboardingPage.tsx +msgid "Import & Connect" +msgstr "Importieren & Verbinden" + +#~ msgid "Create a New Identity" +#~ msgstr "" + +#~ msgid "This will generate a new self-sovereign Agent (your ID) and a decentralized Drive (your data storage) anchored to this server." +#~ msgstr "" + +#: src/components/NewIdentitySection.tsx +msgid "Generating..." +msgstr "Wird generiert..." + +#~ msgid "Generate Identity" +#~ msgstr "" + +#~ msgid "Success! Your Identity is ready." +#~ msgstr "" + +#~ msgid "<0>IMPORTANT: Save this secret key. It is the only way to access your data if you clear your browser cache or sign in from another device." +#~ msgstr "" + +#~ msgid "Finish Setup" +#~ msgstr "" + +#~ msgid "or" +#~ msgstr "" + +#~ msgid "Active Server" +#~ msgstr "" + +#. placeholder {0}: s +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Connect via {0}" +msgstr "Verbinden über {0}" + +#: src/routes/SettingsServer/ServersCard.tsx +msgid "No known servers" +msgstr "Keine bekannten Server" + +#: src/routes/SettingsServer/index.tsx +msgid "Gateway Server" +msgstr "Gateway-Server" + +#~ msgid "The gateway server is used to resolve DIDs and fetch data from the network." +#~ msgstr "" + +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Active Gateway" +msgstr "Aktives Gateway" + +#: src/routes/SettingsServer/index.tsx +msgid "Saved Drives" +msgstr "Gespeicherte Laufwerke" + +#: src/routes/SettingsServer/index.tsx +msgid "Custom Drive URL" +msgstr "Benutzerdefinierte Laufwerks-URL" + +#: src/routes/SettingsServer/index.tsx +msgid "Enter a Drive DID or URL" +msgstr "Geben Sie eine Laufwerks-DID oder URL ein" + +#: src/routes/SettingsServer/index.tsx +msgid "Add Gateway by URL" +msgstr "Gateway per URL hinzufügen" + +#: src/routes/SettingsServer/index.tsx +msgid "Set Active" +msgstr "Als aktiv festlegen" + +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Configure" +msgstr "Konfigurieren" + +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Gateway (Locked to Drive)" +msgstr "Gateway (an Laufwerk gebunden)" + +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Cannot change gateway for HTTP drives" +msgstr "Gateway für HTTP-Laufwerke kann nicht geändert werden" + +#. placeholder {0}: ' ' +#: src/routes/SettingsServer/index.tsx +msgid "" +"The gateway is currently locked to{0} <0/> because you are using an\n" +"HTTP-based drive." +msgstr "" +"Das Gateway ist derzeit auf{0} <0/> gesperrt, da Sie ein\n" +"HTTP-basiertes Laufwerk verwenden." + +#: src/routes/SettingsServer/index.tsx +msgid "" +"The gateway server is used to resolve DIDs and fetch data from the\n" +"network." +msgstr "" +"Der Gateway-Server wird verwendet, um DIDs aufzulösen und Daten aus dem\n" +"Netzwerk abzurufen." + +#: src/routes/SettingsServer/index.tsx +msgid "Locked" +msgstr "Gesperrt" + +#~ msgid "This secret does not contain an initial drive, and no drives were found on this server. Please create a new account." +#~ msgstr "" + +#~ msgid "Initial drive for {0}" +#~ msgstr "" + +#: src/views/OnboardingPage.tsx +msgid "Welcome to Atomic Data" +msgstr "Willkommen bei Atomic Data" + +#~ msgid "Create a new Identity & Drive" +#~ msgstr "" + +#~ msgid "Login via existing server" +#~ msgstr "" + +#~ msgid "If you have an Atomic Data secret from another device or server, paste it here to anchor your identity to this node." +#~ msgstr "" + +#~ msgid "This will generate a new self-sovereign <0>Agent (your global ID) and a decentralized <1>Drive (your storage) anchored to this server." +#~ msgstr "" + +#~ msgid "Create Identity" +#~ msgstr "" + +#. placeholder {0}: ' ' +#: src/views/OnboardingPage.tsx +msgid "This server node is currently uninitialized for{0} <0/>." +msgstr "Dieser Serverknoten ist derzeit für{0} <0/> nicht initialisiert." + +#~ msgid "" +#~ "If you have an Atomic Data secret from another device or\n" +#~ "server, paste it here to anchor your identity to this node." +#~ msgstr "" + +#~ msgid "This will generate a new self-sovereign <0>Agent{0} (your global ID) and a decentralized <1>Drive{1} (your storage) anchored to this server." +#~ msgstr "" + +#~ msgid "" +#~ "<0>IMPORTANT: Save this secret key. It is the\n" +#~ "only way to access your data if you clear your browser cache\n" +#~ "or sign in from another device." +#~ msgstr "" + +#~ msgid "Option 1: Create a New Identity" +#~ msgstr "" + +#~ msgid "" +#~ "This will generate a new self-sovereign{0} <0>Agent (your global ID) and a decentralized{1} <1>Drive (your storage) anchored to this\n" +#~ "server." +#~ msgstr "" + +#~ msgid "Option 2: Use an existing Identity" +#~ msgstr "" + +#: src/views/OnboardingPage.tsx +msgid "" +"Paste your Atomic Data secret key below to connect your\n" +"existing identity to this node." +msgstr "" +"Fügen Sie Ihren Atomic Data Geheimschlüssel unten ein, um Ihre\n" +"bestehende Identität mit diesem Knoten zu verbinden." + +#: src/components/NewIdentitySection.tsx +msgid "Your new identity is ready" +msgstr "Ihre neue Identität ist bereit" + +#~ msgid "" +#~ "<0>IMPORTANT: Save this secret key. It is the only\n" +#~ "way to access your data if you clear your browser cache or sign in\n" +#~ "from another device." +#~ msgstr "" + +#~ msgid "Done" +#~ msgstr "" + +#: src/components/NewIdentitySection.tsx +#: src/routes/SettingsAgent.tsx +msgid "Create a new identity" +msgstr "Neue Identität erstellen" + +#: src/components/NewIdentitySection.tsx +#: src/routes/SettingsAgent.tsx +msgid "Generate a new self-sovereign Agent and Drive on this server." +msgstr "Generieren Sie einen neuen selbst-souveränen Agenten und Drive auf diesem Server." + +#: src/components/NewIdentitySection.tsx +#: src/routes/SettingsAgent.tsx +msgid "Create new identity" +msgstr "Neue Identität erstellen" + +#: src/routes/SettingsAgent.tsx +msgid "Sign in with existing secret" +msgstr "Mit bestehendem Geheimnis anmelden" + +#~ msgid "Don{0}t have a server yet? You can use an{1} <0>atomic-server{2} or an Invite from someone else{3} server." +#~ msgstr "" + +#: src/views/OnboardingPage.tsx +msgid "Use an existing identity" +msgstr "Eine bestehende Identität verwenden" + +#: src/components/NewIdentitySection.tsx +msgid "" +"<0>IMPORTANT: Save this secret key. It is the only way\n" +"to access your data if you clear your browser cache or sign in from\n" +"another device." +msgstr "" +"<0>WICHTIG: Speichern Sie diesen geheimen Schlüssel. Es ist die einzige Möglichkeit,\n" +"auf Ihre Daten zuzugreifen, wenn Sie Ihren Browser-Cache leeren oder sich von\n" +"einem anderen Gerät anmelden." + +#~ msgid "Are you sure you{0}ve stored this secret somewhere safe? You cannot recover it if you lose it." +#~ msgstr "Sind Sie sicher, dass Sie dieses Geheimnis an einem sicheren Ort gespeichert haben? Sie können es nicht wiederherstellen, wenn Sie es verlieren." + +#: src/components/NewIdentitySection.tsx +msgid "Copy the secret key to continue" +msgstr "Geheimen Schlüssel kopieren, um fortzufahren" + +#: src/views/OnboardingPage.tsx +msgid "Yes, I've stored it safely" +msgstr "Ja, ich habe es sicher gespeichert" + +#: src/components/SideBar/AppMenu.tsx +#: src/routes/SettingsAgent.tsx +msgid "Login / New User" +msgstr "Anmelden / Neuer Benutzer" + +#~ msgid "This host is not bound to a Drive yet:{0} <0/>." +#~ msgstr "" + +#. placeholder {0}: ' ' +#: src/views/ErrorPage.tsx +msgid "If this host has not been bound to a Drive yet, continue at{0} <0>the onboarding page ." +msgstr "" + +#. placeholder {0}: "'" +#: src/components/NewIdentitySection.tsx +msgid "" +"Are you sure you{0}ve stored this secret somewhere safe? You\n" +"cannot recover it if you lose it." +msgstr "" + +#~ msgid "Setting up..." +#~ msgstr "" + +#: src/components/SideBar/AppMenu.tsx +msgid "Dev Drive" +msgstr "" + +#~ msgid "Create a new agent + drive on localhost:9883 and switch to it" +#~ msgstr "" + +#: src/routes/DevDriveRoute.tsx +msgid "Setting up dev drive..." +msgstr "" + +#: src/components/SideBar/AppMenu.tsx +msgid "Create a fresh agent + drive on localhost:9883" +msgstr "" diff --git a/browser/data-browser/src/locales/en.po b/browser/data-browser/src/locales/en.po index b1455633..7ed6aa5b 100644 --- a/browser/data-browser/src/locales/en.po +++ b/browser/data-browser/src/locales/en.po @@ -282,16 +282,14 @@ msgstr "Usage" msgid "Drive Configuration" msgstr "Drive Configuration" -#: src/routes/SettingsServer/index.tsx -msgid "Current Drive" -msgstr "Current Drive" +#~ msgid "Current Drive" +#~ msgstr "Current Drive" #: src/chunks/RTE/ImagePicker.tsx #: src/chunks/TablePage/PropertyForm/EditPropertyDialog.tsx #: src/components/forms/EditFormDialog.tsx #: src/components/forms/NewForm/NewFormDialog.tsx #: src/components/forms/ResourceForm.tsx -#: src/routes/SettingsServer/index.tsx #: src/routes/Share/ShareRoute.tsx #: src/views/Article/ArticleDescription.tsx #: src/views/OntologyPage/NewClassButton.tsx @@ -299,13 +297,11 @@ msgstr "Current Drive" msgid "Save" msgstr "Save" -#: src/routes/SettingsServer/index.tsx -msgid "Saved" -msgstr "Saved" +#~ msgid "Saved" +#~ msgstr "Saved" -#: src/routes/SettingsServer/index.tsx -msgid "Other" -msgstr "Other" +#~ msgid "Other" +#~ msgstr "Other" #: src/routes/Share/ShareRoute.tsx msgid "Permissions for" @@ -360,7 +356,6 @@ msgstr "<0/> You{0}re signed in as" msgid "Edit profile" msgstr "Edit profile" -#: src/routes/SettingsAgent.tsx #: src/views/InvitePage.tsx msgid "Agent Secret" msgstr "Agent Secret" @@ -617,6 +612,7 @@ msgid "This URL will be used as the default Parent for imported resources." msgstr "This URL will be used as the default Parent for imported resources." #: src/views/ImporterPage.tsx +#: src/views/OnboardingPage.tsx msgid "Importing..." msgstr "Importing..." @@ -645,7 +641,6 @@ msgstr "Name" #: src/chunks/Plugins/NewPluginButton.tsx #: src/chunks/Plugins/UpdatePluginButton.tsx #: src/chunks/TablePage/PropertyForm/ExternalPropertyDialog.tsx -#: src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/ConfirmationDialog.tsx #: src/components/ParentPicker/ParentPickerDialog.tsx #: src/components/forms/EditFormDialog.tsx @@ -666,7 +661,6 @@ msgstr "Name" msgid "Cancel" msgstr "Cancel" -#: src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/InviteForm.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx @@ -1148,9 +1142,8 @@ msgstr "({0} usages left)" msgid "{0} endpoint" msgstr "{0} endpoint" -#: src/views/EndpointPage.tsx -msgid "Go" -msgstr "Go" +#~ msgid "Go" +#~ msgstr "Go" #: src/views/CollectionPage.tsx msgid "CollectionDisplayStyle" @@ -1292,9 +1285,8 @@ msgstr "Copy" msgid "You are offline, changes might not be persisted." msgstr "You are offline, changes might not be persisted." -#: src/components/NetworkIndicator.tsx -msgid "No Internet Connection." -msgstr "No Internet Connection." +#~ msgid "No Internet Connection." +#~ msgstr "No Internet Connection." #: src/components/AI/MCP/MCPServersManager.tsx msgid "No MCP servers configured" @@ -1364,9 +1356,8 @@ msgstr "<0/> This field is calculated server-side." msgid "New resource" msgstr "New resource" -#: src/components/SideBar/AppMenu.tsx -msgid "Login" -msgstr "Login" +#~ msgid "Login" +#~ msgstr "Login" #: src/components/SideBar/AppMenu.tsx msgid "See and edit the current Agent / User (u)" @@ -1472,6 +1463,7 @@ msgstr "The identifier of the resource. This also determines where the resource #: src/chunks/RTE/CollaborativeEditor.tsx #: src/components/forms/NewForm/NewFormTitle.tsx +#: src/hooks/useCreateAndNavigate.ts #: src/views/Plugin/AssignRights.tsx msgid "Resource" msgstr "Resource" @@ -1566,6 +1558,7 @@ msgid "Edit permissions and create invites." msgstr "Edit permissions and create invites." #: src/components/ResourceContextMenu/index.tsx +#: src/routes/SettingsServer/index.tsx msgid "History" msgstr "History" @@ -1892,9 +1885,8 @@ msgstr "<0/> Resource with error" msgid "Switch to {0}" msgstr "Switch to {0}" -#: src/components/SideBar/DriveSwitcher.tsx -msgid "Configure Drives" -msgstr "Configure Drives" +#~ msgid "Configure Drives" +#~ msgstr "Configure Drives" #: src/components/SideBar/DriveSwitcher.tsx msgid "Load drives not displayed in this list." @@ -2237,13 +2229,11 @@ msgstr "Add external property" msgid "Add" msgstr "Add" -#: src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx -msgid "A column in a table" -msgstr "A column in a table" +#~ msgid "A column in a table" +#~ msgstr "A column in a table" -#: src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx -msgid "New <0/> Column" -msgstr "New <0/> Column" +#~ msgid "New <0/> Column" +#~ msgstr "New <0/> Column" #: src/views/FolderPage/GridItem/ChatRoomGridItem.tsx msgid "Empty Chat" @@ -2767,9 +2757,8 @@ msgstr "Enter valid JSON..." msgid "ID: {0} Name: {1} Description: {2} Tools: {3}" msgstr "ID: {0} Name: {1} Description: {2} Tools: {3}" -#: src/chunks/AI/AIChatMessageParts/UserMessage.tsx -msgid "You" -msgstr "You" +#~ msgid "You" +#~ msgstr "You" #: src/chunks/AI/AgentConfigItem.tsx msgid "Default agent" @@ -2844,6 +2833,7 @@ msgid "No result" msgstr "No result" #: src/chunks/RTE/EditLinkForm.tsx +#: src/routes/SettingsServer/index.tsx msgid "Set" msgstr "Set" @@ -3309,20 +3299,18 @@ msgstr "No plugins installed" msgid "Sign Out" msgstr "Sign Out" -#. placeholder {0}: ' ' -#. placeholder {1}: "'s" -#: src/routes/SettingsAgent.tsx -msgid "" -"You can create your own Agent by hosting an{0} <0>atomic-server . Alternatively, you can use an Invite to get a guest Agent on\n" -"someone else{1} Atomic Server." -msgstr "" -"You can create your own Agent by hosting an{0} <0>atomic-server . Alternatively, you can use an Invite to get a guest Agent on\n" -"someone else{1} Atomic Server." +#~ msgid "" +#~ "You can create your own Agent by hosting an{0} <0>atomic-server . Alternatively, you can use an Invite to get a guest Agent on\n" +#~ "someone else{1} Atomic Server." +#~ msgstr "" +#~ "You can create your own Agent by hosting an{0} <0>atomic-server . Alternatively, you can use an Invite to get a guest Agent on\n" +#~ "someone else{1} Atomic Server." #: src/views/InvitePage.tsx msgid "Agent created!" msgstr "Agent created!" +#: src/views/InvitePage.tsx #: src/views/InvitePage.tsx msgid "Continue" msgstr "Continue" @@ -3331,15 +3319,14 @@ msgstr "Continue" msgid "Copy secret to continue" msgstr "Copy secret to continue" -#: src/views/InvitePage.tsx -msgid "" -"IMPORTANT! Below is your agent secret, you use this to login. Save\n" -"it somewhere safe, the secret will not be show again and if you\n" -"lose it you will not be able to access this user again." -msgstr "" -"IMPORTANT! Below is your agent secret, you use this to login. Save\n" -"it somewhere safe, the secret will not be show again and if you\n" -"lose it you will not be able to access this user again." +#~ msgid "" +#~ "IMPORTANT! Below is your agent secret, you use this to login. Save\n" +#~ "it somewhere safe, the secret will not be show again and if you\n" +#~ "lose it you will not be able to access this user again." +#~ msgstr "" +#~ "IMPORTANT! Below is your agent secret, you use this to login. Save\n" +#~ "it somewhere safe, the secret will not be show again and if you\n" +#~ "lose it you will not be able to access this user again." #: src/views/InvitePage.tsx msgid "Enter a name" @@ -3353,13 +3340,12 @@ msgstr "Agent Name" msgid "This drive is private, sign in to view it" msgstr "This drive is private, sign in to view it" -#: src/routes/SettingsAgent.tsx -msgid "" -"An Agent is a user, consisting of a Subject (its URL) and Private Key.\n" -"Together, these can be used to edit data and sign Commits." -msgstr "" -"An Agent is a user, consisting of a Subject (its URL) and Private Key.\n" -"Together, these can be used to edit data and sign Commits." +#~ msgid "" +#~ "An Agent is a user, consisting of a Subject (its URL) and Private Key.\n" +#~ "Together, these can be used to edit data and sign Commits." +#~ msgstr "" +#~ "An Agent is a user, consisting of a Subject (its URL) and Private Key.\n" +#~ "Together, these can be used to edit data and sign Commits." #: src/views/Plugin/AssignRights.tsx msgid "Pick a resource" @@ -3444,3 +3430,357 @@ msgid "" msgstr "" "<0/> wants to modify a resource that is not\n" "contained in the current scope." + +#: src/views/InvitePage.tsx +msgid "Failed to add invited drive to agent" +msgstr "Failed to add invited drive to agent" + +#: src/views/InvitePage.tsx +msgid "Failed to persist agent after accepting invite" +msgstr "Failed to persist agent after accepting invite" + +#: src/routes/Search/SearchRoute.tsx +msgid "Searching for <0/>..." +msgstr "Searching for <0/>..." + +#: src/views/InvitePage.tsx +msgid "Invite accepted, but no destination was returned." +msgstr "Invite accepted, but no destination was returned." + +#: src/components/forms/NewForm/SubjectField.tsx +msgid "The identifier of the resource. DID subjects are determined by the genesis commit signature." +msgstr "The identifier of the resource. DID subjects are determined by the genesis commit signature." + +#: src/components/NetworkIndicator.tsx +msgid "Connection to server lost, reconnecting..." +msgstr "Connection to server lost, reconnecting..." + +#~ msgid "Server connection lost." +#~ msgstr "Server connection lost." + +#: src/components/NetworkIndicator.tsx +msgid "No internet connection" +msgstr "No internet connection" + +#: src/components/NetworkIndicator.tsx +msgid "Server connection lost — reconnecting…" +msgstr "Server connection lost — reconnecting…" + +#: src/views/InvitePage.tsx +msgid "" +"IMPORTANT! Below is your agent secret, you use this to login.\n" +"Save it somewhere safe, the secret will not be show again and if\n" +"you lose it you will not be able to access this user again." +msgstr "" +"IMPORTANT! Below is your agent secret, you use this to login.\n" +"Save it somewhere safe, the secret will not be show again and if\n" +"you lose it you will not be able to access this user again." + +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx +msgid "Subdomain" +msgstr "Subdomain" + +#~ msgid "This secret does not contain an initial drive link. You might need to set it up manually or create a new account." +#~ msgstr "This secret does not contain an initial drive link. You might need to set it up manually or create a new account." + +#~ msgid "Initial Drive" +#~ msgstr "Initial Drive" + +#~ msgid "My first decentralized drive" +#~ msgstr "My first decentralized drive" + +#~ msgid "Welcome to Atomic Server" +#~ msgstr "Welcome to Atomic Server" + +#~ msgid "This server node is currently uninitialized for <0/>." +#~ msgstr "This server node is currently uninitialized for <0/>." + +#~ msgid "I already have an account (Paste Secret)" +#~ msgstr "I already have an account (Paste Secret)" + +#~ msgid "Create a new Account & Drive" +#~ msgstr "Create a new Account & Drive" + +#~ msgid "Paste your Agent Secret" +#~ msgstr "Paste your Agent Secret" + +#~ msgid "Back" +#~ msgstr "Back" + +#: src/views/OnboardingPage.tsx +msgid "Import & Connect" +msgstr "Import & Connect" + +#~ msgid "Create a New Identity" +#~ msgstr "Create a New Identity" + +#~ msgid "This will generate a new self-sovereign Agent (your ID) and a decentralized Drive (your data storage) anchored to this server." +#~ msgstr "This will generate a new self-sovereign Agent (your ID) and a decentralized Drive (your data storage) anchored to this server." + +#: src/components/NewIdentitySection.tsx +msgid "Generating..." +msgstr "Generating..." + +#~ msgid "Generate Identity" +#~ msgstr "Generate Identity" + +#~ msgid "Success! Your Identity is ready." +#~ msgstr "Success! Your Identity is ready." + +#~ msgid "<0>IMPORTANT: Save this secret key. It is the only way to access your data if you clear your browser cache or sign in from another device." +#~ msgstr "<0>IMPORTANT: Save this secret key. It is the only way to access your data if you clear your browser cache or sign in from another device." + +#~ msgid "Finish Setup" +#~ msgstr "Finish Setup" + +#~ msgid "or" +#~ msgstr "or" + +#~ msgid "Active Server" +#~ msgstr "Active Server" + +#. placeholder {0}: s +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Connect via {0}" +msgstr "Connect via {0}" + +#: src/routes/SettingsServer/ServersCard.tsx +msgid "No known servers" +msgstr "No known servers" + +#: src/routes/SettingsServer/index.tsx +msgid "Gateway Server" +msgstr "Gateway Server" + +#~ msgid "The gateway server is used to resolve DIDs and fetch data from the network." +#~ msgstr "The gateway server is used to resolve DIDs and fetch data from the network." + +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Active Gateway" +msgstr "Active Gateway" + +#: src/routes/SettingsServer/index.tsx +msgid "Saved Drives" +msgstr "Saved Drives" + +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Configure" +msgstr "Configure" + +#: src/routes/SettingsServer/index.tsx +msgid "Custom Drive URL" +msgstr "Custom Drive URL" + +#: src/routes/SettingsServer/index.tsx +msgid "Enter a Drive DID or URL" +msgstr "Enter a Drive DID or URL" + +#: src/routes/SettingsServer/index.tsx +msgid "Add Gateway by URL" +msgstr "Add Gateway by URL" + +#: src/routes/SettingsServer/index.tsx +msgid "Set Active" +msgstr "Set Active" + +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Gateway (Locked to Drive)" +msgstr "Gateway (Locked to Drive)" + +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Cannot change gateway for HTTP drives" +msgstr "Cannot change gateway for HTTP drives" + +#. placeholder {0}: ' ' +#: src/routes/SettingsServer/index.tsx +msgid "" +"The gateway is currently locked to{0} <0/> because you are using an\n" +"HTTP-based drive." +msgstr "" +"The gateway is currently locked to{0} <0/> because you are using an\n" +"HTTP-based drive." + +#: src/routes/SettingsServer/index.tsx +msgid "" +"The gateway server is used to resolve DIDs and fetch data from the\n" +"network." +msgstr "" +"The gateway server is used to resolve DIDs and fetch data from the\n" +"network." + +#: src/routes/SettingsServer/index.tsx +msgid "Locked" +msgstr "Locked" + +#~ msgid "This secret does not contain an initial drive, and no drives were found on this server. Please create a new account." +#~ msgstr "This secret does not contain an initial drive, and no drives were found on this server. Please create a new account." + +#~ msgid "Initial drive for {0}" +#~ msgstr "Initial drive for {0}" + +#: src/views/OnboardingPage.tsx +msgid "Welcome to Atomic Data" +msgstr "Welcome to Atomic Data" + +#~ msgid "Create a new Identity & Drive" +#~ msgstr "Create a new Identity & Drive" + +#~ msgid "Login via existing server" +#~ msgstr "Login via existing server" + +#~ msgid "If you have an Atomic Data secret from another device or server, paste it here to anchor your identity to this node." +#~ msgstr "If you have an Atomic Data secret from another device or server, paste it here to anchor your identity to this node." + +#~ msgid "This will generate a new self-sovereign <0>Agent (your global ID) and a decentralized <1>Drive (your storage) anchored to this server." +#~ msgstr "This will generate a new self-sovereign <0>Agent (your global ID) and a decentralized <1>Drive (your storage) anchored to this server." + +#~ msgid "Create Identity" +#~ msgstr "Create Identity" + +#. placeholder {0}: ' ' +#: src/views/OnboardingPage.tsx +msgid "This server node is currently uninitialized for{0} <0/>." +msgstr "This server node is currently uninitialized for{0} <0/>." + +#~ msgid "" +#~ "If you have an Atomic Data secret from another device or\n" +#~ "server, paste it here to anchor your identity to this node." +#~ msgstr "" +#~ "If you have an Atomic Data secret from another device or\n" +#~ "server, paste it here to anchor your identity to this node." + +#~ msgid "This will generate a new self-sovereign <0>Agent{0} (your global ID) and a decentralized <1>Drive{1} (your storage) anchored to this server." +#~ msgstr "This will generate a new self-sovereign <0>Agent{0} (your global ID) and a decentralized <1>Drive{1} (your storage) anchored to this server." + +#~ msgid "" +#~ "<0>IMPORTANT: Save this secret key. It is the\n" +#~ "only way to access your data if you clear your browser cache\n" +#~ "or sign in from another device." +#~ msgstr "" +#~ "<0>IMPORTANT: Save this secret key. It is the\n" +#~ "only way to access your data if you clear your browser cache\n" +#~ "or sign in from another device." + +#~ msgid "Option 1: Create a New Identity" +#~ msgstr "Option 1: Create a New Identity" + +#~ msgid "" +#~ "This will generate a new self-sovereign{0} <0>Agent (your global ID) and a decentralized{1} <1>Drive (your storage) anchored to this\n" +#~ "server." +#~ msgstr "" +#~ "This will generate a new self-sovereign{0} <0>Agent (your global ID) and a decentralized{1} <1>Drive (your storage) anchored to this\n" +#~ "server." + +#~ msgid "Option 2: Use an existing Identity" +#~ msgstr "Option 2: Use an existing Identity" + +#: src/views/OnboardingPage.tsx +msgid "" +"Paste your Atomic Data secret key below to connect your\n" +"existing identity to this node." +msgstr "" +"Paste your Atomic Data secret key below to connect your\n" +"existing identity to this node." + +#: src/components/NewIdentitySection.tsx +msgid "Your new identity is ready" +msgstr "Your new identity is ready" + +#~ msgid "" +#~ "<0>IMPORTANT: Save this secret key. It is the only\n" +#~ "way to access your data if you clear your browser cache or sign in\n" +#~ "from another device." +#~ msgstr "" +#~ "<0>IMPORTANT: Save this secret key. It is the only\n" +#~ "way to access your data if you clear your browser cache or sign in\n" +#~ "from another device." + +#~ msgid "Done" +#~ msgstr "Done" + +#: src/components/NewIdentitySection.tsx +#: src/routes/SettingsAgent.tsx +msgid "Create a new identity" +msgstr "Create a new identity" + +#: src/components/NewIdentitySection.tsx +#: src/routes/SettingsAgent.tsx +msgid "Generate a new self-sovereign Agent and Drive on this server." +msgstr "Generate a new self-sovereign Agent and Drive on this server." + +#: src/components/NewIdentitySection.tsx +#: src/routes/SettingsAgent.tsx +msgid "Create new identity" +msgstr "Create new identity" + +#: src/routes/SettingsAgent.tsx +msgid "Sign in with existing secret" +msgstr "Sign in with existing secret" + +#~ msgid "Don{0}t have a server yet? You can use an{1} <0>atomic-server{2} or an Invite from someone else{3} server." +#~ msgstr "Don{0}t have a server yet? You can use an{1} <0>atomic-server{2} or an Invite from someone else{3} server." + +#: src/views/OnboardingPage.tsx +msgid "Use an existing identity" +msgstr "Use an existing identity" + +#: src/components/NewIdentitySection.tsx +msgid "" +"<0>IMPORTANT: Save this secret key. It is the only way\n" +"to access your data if you clear your browser cache or sign in from\n" +"another device." +msgstr "" +"<0>IMPORTANT: Save this secret key. It is the only way\n" +"to access your data if you clear your browser cache or sign in from\n" +"another device." + +#~ msgid "Are you sure you{0}ve stored this secret somewhere safe? You cannot recover it if you lose it." +#~ msgstr "Are you sure you{0}ve stored this secret somewhere safe? You cannot recover it if you lose it." + +#: src/components/NewIdentitySection.tsx +msgid "Copy the secret key to continue" +msgstr "Copy the secret key to continue" + +#: src/views/OnboardingPage.tsx +msgid "Yes, I've stored it safely" +msgstr "Yes, I've stored it safely" + +#: src/components/SideBar/AppMenu.tsx +#: src/routes/SettingsAgent.tsx +msgid "Login / New User" +msgstr "Login / New User" + +#~ msgid "This host is not bound to a Drive yet:{0} <0/>." +#~ msgstr "This host is not bound to a Drive yet:{0} <0/>." + +#. placeholder {0}: ' ' +#: src/views/ErrorPage.tsx +msgid "If this host has not been bound to a Drive yet, continue at{0} <0>the onboarding page ." +msgstr "If this host has not been bound to a Drive yet, continue at{0} <0>the onboarding page ." + +#. placeholder {0}: "'" +#: src/components/NewIdentitySection.tsx +msgid "" +"Are you sure you{0}ve stored this secret somewhere safe? You\n" +"cannot recover it if you lose it." +msgstr "" +"Are you sure you{0}ve stored this secret somewhere safe? You\n" +"cannot recover it if you lose it." + +#~ msgid "Setting up..." +#~ msgstr "Setting up..." + +#: src/components/SideBar/AppMenu.tsx +msgid "Dev Drive" +msgstr "Dev Drive" + +#~ msgid "Create a new agent + drive on localhost:9883 and switch to it" +#~ msgstr "Create a new agent + drive on localhost:9883 and switch to it" + +#: src/routes/DevDriveRoute.tsx +msgid "Setting up dev drive..." +msgstr "Setting up dev drive..." + +#: src/components/SideBar/AppMenu.tsx +msgid "Create a fresh agent + drive on localhost:9883" +msgstr "Create a fresh agent + drive on localhost:9883" diff --git a/browser/data-browser/src/locales/es.po b/browser/data-browser/src/locales/es.po index 65f6b411..3f5d64c7 100644 --- a/browser/data-browser/src/locales/es.po +++ b/browser/data-browser/src/locales/es.po @@ -30,7 +30,6 @@ msgstr "No hay clases" #: src/chunks/Plugins/NewPluginButton.tsx #: src/chunks/Plugins/UpdatePluginButton.tsx #: src/chunks/TablePage/PropertyForm/ExternalPropertyDialog.tsx -#: src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/ConfirmationDialog.tsx #: src/components/ParentPicker/ParentPickerDialog.tsx #: src/components/forms/EditFormDialog.tsx @@ -95,7 +94,6 @@ msgstr "Texto de invitación (opcional)" msgid "Limit Usages (optional)" msgstr "Limitar usos (opcional)" -#: src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/InviteForm.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx @@ -182,9 +180,8 @@ msgstr "Cargando {0}" msgid "You are offline, changes might not be persisted." msgstr "Estás sin conexión, es posible que los cambios no se guarden." -#: src/components/NetworkIndicator.tsx -msgid "No Internet Connection." -msgstr "Sin conexión a Internet." +#~ msgid "No Internet Connection." +#~ msgstr "Sin conexión a Internet." #: src/components/SkipNav.tsx msgid "Skip Navigation?" @@ -581,7 +578,6 @@ msgstr "<0/> Has iniciado sesión como" msgid "Edit profile" msgstr "Editar perfil" -#: src/routes/SettingsAgent.tsx #: src/views/InvitePage.tsx msgid "Agent Secret" msgstr "Secreto del Agente" @@ -733,9 +729,8 @@ msgstr "Introduce un JSON válido..." msgid "{0} endpoint" msgstr "Endpoint {0}" -#: src/views/EndpointPage.tsx -msgid "Go" -msgstr "Ir" +#~ msgid "Go" +#~ msgstr "Ir" #: src/routes/Search/SearchRoute.tsx #: src/views/EndpointPage.tsx @@ -1019,6 +1014,7 @@ msgid "This URL will be used as the default Parent for imported resources." msgstr "Esta URL se utilizará como el padre predeterminado para los recursos importados." #: src/views/ImporterPage.tsx +#: src/views/OnboardingPage.tsx msgid "Importing..." msgstr "Importando..." @@ -1061,7 +1057,6 @@ msgstr "Texto alternativo" #: src/components/forms/EditFormDialog.tsx #: src/components/forms/NewForm/NewFormDialog.tsx #: src/components/forms/ResourceForm.tsx -#: src/routes/SettingsServer/index.tsx #: src/routes/Share/ShareRoute.tsx #: src/views/Article/ArticleDescription.tsx #: src/views/OntologyPage/NewClassButton.tsx @@ -1135,6 +1130,7 @@ msgid "Toggle inline code" msgstr "Activar/desactivar código en línea" #: src/chunks/RTE/EditLinkForm.tsx +#: src/routes/SettingsServer/index.tsx msgid "Set" msgstr "Establecer" @@ -1338,6 +1334,7 @@ msgid "Edit permissions and create invites." msgstr "Editar permisos y crear invitaciones." #: src/components/ResourceContextMenu/index.tsx +#: src/routes/SettingsServer/index.tsx msgid "History" msgstr "Historial" @@ -1462,9 +1459,8 @@ msgstr "Nuevo recurso" msgid "Switch to {0}" msgstr "Cambiar a {0}" -#: src/components/SideBar/DriveSwitcher.tsx -msgid "Configure Drives" -msgstr "Configurar unidades" +#~ msgid "Configure Drives" +#~ msgstr "Configurar unidades" #: src/components/SideBar/DriveSwitcher.tsx msgid "Load drives not displayed in this list." @@ -1898,17 +1894,14 @@ msgstr "Nada que mostrar" msgid "Drive Configuration" msgstr "Configuración de la unidad" -#: src/routes/SettingsServer/index.tsx -msgid "Current Drive" -msgstr "Unidad actual" +#~ msgid "Current Drive" +#~ msgstr "Unidad actual" -#: src/routes/SettingsServer/index.tsx -msgid "Saved" -msgstr "Guardado" +#~ msgid "Saved" +#~ msgstr "Guardado" -#: src/routes/SettingsServer/index.tsx -msgid "Other" -msgstr "Otro" +#~ msgid "Other" +#~ msgstr "Otro" #: src/views/Article/ArticleDescription.tsx msgid "<0/> Add Content" @@ -2237,9 +2230,8 @@ msgstr "Exportar a CSV" msgid "Attached File" msgstr "Archivo adjunto" -#: src/chunks/AI/AIChatMessageParts/UserMessage.tsx -msgid "You" -msgstr "Tú" +#~ msgid "You" +#~ msgstr "Tú" #: src/chunks/AI/AIChatMessageParts/ReasoningMessage.tsx msgid "Thinking..." @@ -2457,6 +2449,7 @@ msgstr "Inicializando recurso" #: src/chunks/RTE/CollaborativeEditor.tsx #: src/components/forms/NewForm/NewFormTitle.tsx +#: src/hooks/useCreateAndNavigate.ts #: src/views/Plugin/AssignRights.tsx msgid "Resource" msgstr "Recurso" @@ -2638,13 +2631,11 @@ msgstr "Añadir propiedad externa" msgid "Add" msgstr "Añadir" -#: src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx -msgid "A column in a table" -msgstr "Una columna en una tabla" +#~ msgid "A column in a table" +#~ msgstr "Una columna en una tabla" -#: src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx -msgid "New <0/> Column" -msgstr "Nueva <0/> Columna" +#~ msgid "New <0/> Column" +#~ msgstr "Nueva <0/> Columna" #: src/chunks/TablePage/PropertyForm/TextPropertyForm.tsx msgid "Text Format:" @@ -2808,9 +2799,8 @@ msgstr "Una ontología es una colección de clases y propiedades que juntas desc msgid "Shortname" msgstr "Shortname" -#: src/components/SideBar/AppMenu.tsx -msgid "Login" -msgstr "Iniciar sesión" +#~ msgid "Login" +#~ msgstr "Iniciar sesión" #: src/components/SideBar/AppMenu.tsx msgid "See and edit the current Agent / User (u)" @@ -3281,18 +3271,16 @@ msgstr "Error al guardar los cambios" msgid "Sign Out" msgstr "Cerrar sesión" -#. placeholder {0}: ' ' -#. placeholder {1}: "'s" -#: src/routes/SettingsAgent.tsx -msgid "" -"You can create your own Agent by hosting an{0} <0>atomic-server . Alternatively, you can use an Invite to get a guest Agent on\n" -"someone else{1} Atomic Server." -msgstr "Puedes crear tu propio Agente alojando un <0>atomic-server{0}. Alternativamente, puedes usar una invitación para obtener un Agente invitado en el Atomic Server de otra persona{1}." +#~ msgid "" +#~ "You can create your own Agent by hosting an{0} <0>atomic-server . Alternatively, you can use an Invite to get a guest Agent on\n" +#~ "someone else{1} Atomic Server." +#~ msgstr "Puedes crear tu propio Agente alojando un <0>atomic-server{0}. Alternativamente, puedes usar una invitación para obtener un Agente invitado en el Atomic Server de otra persona{1}." #: src/views/InvitePage.tsx msgid "Agent created!" msgstr "¡Agente creado!" +#: src/views/InvitePage.tsx #: src/views/InvitePage.tsx msgid "Continue" msgstr "Continuar" @@ -3301,12 +3289,11 @@ msgstr "Continuar" msgid "Copy secret to continue" msgstr "Copiar secreto para continuar" -#: src/views/InvitePage.tsx -msgid "" -"IMPORTANT! Below is your agent secret, you use this to login. Save\n" -"it somewhere safe, the secret will not be show again and if you\n" -"lose it you will not be able to access this user again." -msgstr "¡IMPORTANTE! Abajo está tu secreto de agente, lo usas para iniciar sesión. Guárdalo en un lugar seguro, el secreto no se mostrará de nuevo y si lo pierdes no podrás acceder a este usuario de nuevo." +#~ msgid "" +#~ "IMPORTANT! Below is your agent secret, you use this to login. Save\n" +#~ "it somewhere safe, the secret will not be show again and if you\n" +#~ "lose it you will not be able to access this user again." +#~ msgstr "¡IMPORTANTE! Abajo está tu secreto de agente, lo usas para iniciar sesión. Guárdalo en un lugar seguro, el secreto no se mostrará de nuevo y si lo pierdes no podrás acceder a este usuario de nuevo." #: src/views/InvitePage.tsx msgid "Enter a name" @@ -3320,13 +3307,12 @@ msgstr "Nombre del Agente" msgid "This drive is private, sign in to view it" msgstr "Esta unidad es privada, inicia sesión para verla" -#: src/routes/SettingsAgent.tsx -msgid "" -"An Agent is a user, consisting of a Subject (its URL) and Private Key.\n" -"Together, these can be used to edit data and sign Commits." -msgstr "" -"Un agente es un usuario, que consiste en un Asunto (su URL) y una Clave Privada.\n" -"Juntos, estos pueden ser usados para editar datos y firmar Commits." +#~ msgid "" +#~ "An Agent is a user, consisting of a Subject (its URL) and Private Key.\n" +#~ "Together, these can be used to edit data and sign Commits." +#~ msgstr "" +#~ "Un agente es un usuario, que consiste en un Asunto (su URL) y una Clave Privada.\n" +#~ "Juntos, estos pueden ser usados para editar datos y firmar Commits." #: src/views/Plugin/AssignRights.tsx msgid "Pick a resource" @@ -3411,3 +3397,345 @@ msgid "" msgstr "" "<0/> quiere modificar un recurso que no está\n" "contenido en el ámbito actual." + +#: src/routes/Search/SearchRoute.tsx +msgid "Searching for <0/>..." +msgstr "Buscando <0/>..." + +#: src/views/InvitePage.tsx +msgid "Failed to add invited drive to agent" +msgstr "Fallo al añadir la unidad invitada al agente" + +#: src/views/InvitePage.tsx +msgid "Failed to persist agent after accepting invite" +msgstr "Fallo al persistir el agente después de aceptar la invitación" + +#: src/views/InvitePage.tsx +msgid "Invite accepted, but no destination was returned." +msgstr "Invitación aceptada, pero no se devolvió ningún destino." + +#: src/components/forms/NewForm/SubjectField.tsx +msgid "The identifier of the resource. DID subjects are determined by the genesis commit signature." +msgstr "El identificador del recurso. Los sujetos DID se determinan por la firma del commit de génesis." + +#: src/components/NetworkIndicator.tsx +msgid "Connection to server lost, reconnecting..." +msgstr "Conexión con el servidor perdida, reconectando..." + +#~ msgid "Server connection lost." +#~ msgstr "" + +#: src/components/NetworkIndicator.tsx +msgid "No internet connection" +msgstr "No hay conexión a internet" + +#: src/components/NetworkIndicator.tsx +msgid "Server connection lost — reconnecting…" +msgstr "Conexión con el servidor perdida — reconectando…" + +#: src/views/InvitePage.tsx +msgid "" +"IMPORTANT! Below is your agent secret, you use this to login.\n" +"Save it somewhere safe, the secret will not be show again and if\n" +"you lose it you will not be able to access this user again." +msgstr "" +"¡IMPORTANTE! Abajo está tu secreto de agente, lo usas para iniciar sesión.\n" +"Guárdalo en un lugar seguro, el secreto no se mostrará de nuevo y si\n" +"lo pierdes no podrás acceder a este usuario de nuevo." + +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx +msgid "Subdomain" +msgstr "Subdominio" + +#~ msgid "This secret does not contain an initial drive link. You might need to set it up manually or create a new account." +#~ msgstr "" + +#~ msgid "Initial Drive" +#~ msgstr "" + +#~ msgid "My first decentralized drive" +#~ msgstr "" + +#~ msgid "Welcome to Atomic Server" +#~ msgstr "" + +#~ msgid "This server node is currently uninitialized for <0/>." +#~ msgstr "" + +#~ msgid "I already have an account (Paste Secret)" +#~ msgstr "" + +#~ msgid "Create a new Account & Drive" +#~ msgstr "" + +#~ msgid "Paste your Agent Secret" +#~ msgstr "" + +#~ msgid "Back" +#~ msgstr "" + +#: src/views/OnboardingPage.tsx +msgid "Import & Connect" +msgstr "Importar y conectar" + +#~ msgid "Create a New Identity" +#~ msgstr "" + +#~ msgid "This will generate a new self-sovereign Agent (your ID) and a decentralized Drive (your data storage) anchored to this server." +#~ msgstr "" + +#: src/components/NewIdentitySection.tsx +msgid "Generating..." +msgstr "Generando..." + +#~ msgid "Generate Identity" +#~ msgstr "" + +#~ msgid "Success! Your Identity is ready." +#~ msgstr "" + +#~ msgid "<0>IMPORTANT: Save this secret key. It is the only way to access your data if you clear your browser cache or sign in from another device." +#~ msgstr "" + +#~ msgid "Finish Setup" +#~ msgstr "" + +#~ msgid "or" +#~ msgstr "" + +#~ msgid "Active Server" +#~ msgstr "" + +#. placeholder {0}: s +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Connect via {0}" +msgstr "Conectar vía {0}" + +#: src/routes/SettingsServer/ServersCard.tsx +msgid "No known servers" +msgstr "No hay servidores conocidos" + +#: src/routes/SettingsServer/index.tsx +msgid "Gateway Server" +msgstr "Servidor de puerta de enlace" + +#~ msgid "The gateway server is used to resolve DIDs and fetch data from the network." +#~ msgstr "" + +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Active Gateway" +msgstr "Puerta de enlace activa" + +#: src/routes/SettingsServer/index.tsx +msgid "Saved Drives" +msgstr "Unidades guardadas" + +#: src/routes/SettingsServer/index.tsx +msgid "Custom Drive URL" +msgstr "URL de unidad personalizada" + +#: src/routes/SettingsServer/index.tsx +msgid "Enter a Drive DID or URL" +msgstr "Introduce un DID o URL de unidad" + +#: src/routes/SettingsServer/index.tsx +msgid "Add Gateway by URL" +msgstr "Añadir puerta de enlace por URL" + +#: src/routes/SettingsServer/index.tsx +msgid "Set Active" +msgstr "Establecer como activo" + +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Configure" +msgstr "Configurar" + +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Gateway (Locked to Drive)" +msgstr "Puerta de enlace (Bloqueada a la unidad)" + +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Cannot change gateway for HTTP drives" +msgstr "No se puede cambiar la puerta de enlace para unidades HTTP" + +#. placeholder {0}: ' ' +#: src/routes/SettingsServer/index.tsx +msgid "" +"The gateway is currently locked to{0} <0/> because you are using an\n" +"HTTP-based drive." +msgstr "" +"La puerta de enlace está actualmente bloqueada a{0} <0/> porque estás usando una\n" +"unidad basada en HTTP." + +#: src/routes/SettingsServer/index.tsx +msgid "" +"The gateway server is used to resolve DIDs and fetch data from the\n" +"network." +msgstr "" +"El servidor de la puerta de enlace se utiliza para resolver DIDs y obtener datos de la\n" +"red." + +#: src/routes/SettingsServer/index.tsx +msgid "Locked" +msgstr "Bloqueado" + +#~ msgid "This secret does not contain an initial drive, and no drives were found on this server. Please create a new account." +#~ msgstr "" + +#~ msgid "Initial drive for {0}" +#~ msgstr "" + +#: src/views/OnboardingPage.tsx +msgid "Welcome to Atomic Data" +msgstr "Bienvenido a Atomic Data" + +#~ msgid "Create a new Identity & Drive" +#~ msgstr "" + +#~ msgid "Login via existing server" +#~ msgstr "" + +#~ msgid "If you have an Atomic Data secret from another device or server, paste it here to anchor your identity to this node." +#~ msgstr "" + +#~ msgid "This will generate a new self-sovereign <0>Agent (your global ID) and a decentralized <1>Drive (your storage) anchored to this server." +#~ msgstr "" + +#~ msgid "Create Identity" +#~ msgstr "" + +#. placeholder {0}: ' ' +#: src/views/OnboardingPage.tsx +msgid "This server node is currently uninitialized for{0} <0/>." +msgstr "Este nodo de servidor no está inicializado para{0} <0/>." + +#~ msgid "" +#~ "If you have an Atomic Data secret from another device or\n" +#~ "server, paste it here to anchor your identity to this node." +#~ msgstr "" + +#~ msgid "This will generate a new self-sovereign <0>Agent{0} (your global ID) and a decentralized <1>Drive{1} (your storage) anchored to this server." +#~ msgstr "" + +#~ msgid "" +#~ "<0>IMPORTANT: Save this secret key. It is the\n" +#~ "only way to access your data if you clear your browser cache\n" +#~ "or sign in from another device." +#~ msgstr "" + +#~ msgid "Option 1: Create a New Identity" +#~ msgstr "" + +#~ msgid "" +#~ "This will generate a new self-sovereign{0} <0>Agent (your global ID) and a decentralized{1} <1>Drive (your storage) anchored to this\n" +#~ "server." +#~ msgstr "" + +#~ msgid "Option 2: Use an existing Identity" +#~ msgstr "" + +#: src/views/OnboardingPage.tsx +msgid "" +"Paste your Atomic Data secret key below to connect your\n" +"existing identity to this node." +msgstr "" +"Pega tu clave secreta de Atomic Data a continuación para conectar tu\n" +"identidad existente a este nodo." + +#: src/components/NewIdentitySection.tsx +msgid "Your new identity is ready" +msgstr "Tu nueva identidad está lista" + +#~ msgid "" +#~ "<0>IMPORTANT: Save this secret key. It is the only\n" +#~ "way to access your data if you clear your browser cache or sign in\n" +#~ "from another device." +#~ msgstr "" + +#~ msgid "Done" +#~ msgstr "" + +#: src/components/NewIdentitySection.tsx +#: src/routes/SettingsAgent.tsx +msgid "Create a new identity" +msgstr "Crear una nueva identidad" + +#: src/components/NewIdentitySection.tsx +#: src/routes/SettingsAgent.tsx +msgid "Generate a new self-sovereign Agent and Drive on this server." +msgstr "Genera un nuevo Agente y Drive auto-soberano en este servidor." + +#: src/components/NewIdentitySection.tsx +#: src/routes/SettingsAgent.tsx +msgid "Create new identity" +msgstr "Crear nueva identidad" + +#: src/routes/SettingsAgent.tsx +msgid "Sign in with existing secret" +msgstr "Iniciar sesión con secreto existente" + +#~ msgid "Don{0}t have a server yet? You can use an{1} <0>atomic-server{2} or an Invite from someone else{3} server." +#~ msgstr "" + +#: src/views/OnboardingPage.tsx +msgid "Use an existing identity" +msgstr "Usar una identidad existente" + +#: src/components/NewIdentitySection.tsx +msgid "" +"<0>IMPORTANT: Save this secret key. It is the only way\n" +"to access your data if you clear your browser cache or sign in from\n" +"another device." +msgstr "" +"<0>IMPORTANTE: Guarda esta clave secreta. Es la única forma\n" +"de acceder a tus datos si borras el caché de tu navegador o inicias sesión desde\n" +"otro dispositivo." + +#~ msgid "Are you sure you{0}ve stored this secret somewhere safe? You cannot recover it if you lose it." +#~ msgstr "¿Estás seguro de que has guardado este secreto en un lugar seguro? No podrás recuperarlo si lo pierdes." + +#: src/components/NewIdentitySection.tsx +msgid "Copy the secret key to continue" +msgstr "Copia la clave secreta para continuar" + +#: src/views/OnboardingPage.tsx +msgid "Yes, I've stored it safely" +msgstr "Sí, lo he guardado de forma segura" + +#: src/components/SideBar/AppMenu.tsx +#: src/routes/SettingsAgent.tsx +msgid "Login / New User" +msgstr "Iniciar sesión / Nuevo usuario" + +#~ msgid "This host is not bound to a Drive yet:{0} <0/>." +#~ msgstr "" + +#. placeholder {0}: ' ' +#: src/views/ErrorPage.tsx +msgid "If this host has not been bound to a Drive yet, continue at{0} <0>the onboarding page ." +msgstr "" + +#. placeholder {0}: "'" +#: src/components/NewIdentitySection.tsx +msgid "" +"Are you sure you{0}ve stored this secret somewhere safe? You\n" +"cannot recover it if you lose it." +msgstr "" + +#~ msgid "Setting up..." +#~ msgstr "" + +#: src/components/SideBar/AppMenu.tsx +msgid "Dev Drive" +msgstr "" + +#~ msgid "Create a new agent + drive on localhost:9883 and switch to it" +#~ msgstr "" + +#: src/routes/DevDriveRoute.tsx +msgid "Setting up dev drive..." +msgstr "" + +#: src/components/SideBar/AppMenu.tsx +msgid "Create a fresh agent + drive on localhost:9883" +msgstr "" diff --git a/browser/data-browser/src/locales/fr.po b/browser/data-browser/src/locales/fr.po index 86caca73..3ce3df9a 100644 --- a/browser/data-browser/src/locales/fr.po +++ b/browser/data-browser/src/locales/fr.po @@ -30,7 +30,6 @@ msgstr "Aucune classe" #: src/chunks/Plugins/NewPluginButton.tsx #: src/chunks/Plugins/UpdatePluginButton.tsx #: src/chunks/TablePage/PropertyForm/ExternalPropertyDialog.tsx -#: src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/ConfirmationDialog.tsx #: src/components/ParentPicker/ParentPickerDialog.tsx #: src/components/forms/EditFormDialog.tsx @@ -95,7 +94,6 @@ msgstr "Texte d'invitation (facultatif)" msgid "Limit Usages (optional)" msgstr "Limiter les utilisations (facultatif)" -#: src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx #: src/components/InviteForm.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx #: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx @@ -182,9 +180,8 @@ msgstr "Chargement {0}" msgid "You are offline, changes might not be persisted." msgstr "Vous êtes hors ligne, les modifications pourraient ne pas être conservées." -#: src/components/NetworkIndicator.tsx -msgid "No Internet Connection." -msgstr "Aucune connexion Internet." +#~ msgid "No Internet Connection." +#~ msgstr "Aucune connexion Internet." #: src/components/SkipNav.tsx msgid "Skip Navigation?" @@ -594,7 +591,6 @@ msgstr "<0/> Vous êtes connecté en tant que" msgid "Edit profile" msgstr "Modifier le profil" -#: src/routes/SettingsAgent.tsx #: src/views/InvitePage.tsx msgid "Agent Secret" msgstr "Secret de l'Agent" @@ -750,9 +746,8 @@ msgstr "Entrez un JSON valide..." msgid "{0} endpoint" msgstr "Point de terminaison {0}" -#: src/views/EndpointPage.tsx -msgid "Go" -msgstr "Go" +#~ msgid "Go" +#~ msgstr "Go" #: src/routes/Search/SearchRoute.tsx #: src/views/EndpointPage.tsx @@ -1036,6 +1031,7 @@ msgid "This URL will be used as the default Parent for imported resources." msgstr "Cette URL sera utilisée comme parent par défaut pour les ressources importées." #: src/views/ImporterPage.tsx +#: src/views/OnboardingPage.tsx msgid "Importing..." msgstr "Importation..." @@ -1078,7 +1074,6 @@ msgstr "Texte alternatif" #: src/components/forms/EditFormDialog.tsx #: src/components/forms/NewForm/NewFormDialog.tsx #: src/components/forms/ResourceForm.tsx -#: src/routes/SettingsServer/index.tsx #: src/routes/Share/ShareRoute.tsx #: src/views/Article/ArticleDescription.tsx #: src/views/OntologyPage/NewClassButton.tsx @@ -1152,6 +1147,7 @@ msgid "Toggle inline code" msgstr "Activer/désactiver le code en ligne" #: src/chunks/RTE/EditLinkForm.tsx +#: src/routes/SettingsServer/index.tsx msgid "Set" msgstr "Définir" @@ -1351,6 +1347,7 @@ msgid "Edit permissions and create invites." msgstr "Modifier les permissions et créer des invitations." #: src/components/ResourceContextMenu/index.tsx +#: src/routes/SettingsServer/index.tsx msgid "History" msgstr "Historique" @@ -1475,9 +1472,8 @@ msgstr "Nouvelle ressource" msgid "Switch to {0}" msgstr "Basculer vers {0}" -#: src/components/SideBar/DriveSwitcher.tsx -msgid "Configure Drives" -msgstr "Configurer les lecteurs" +#~ msgid "Configure Drives" +#~ msgstr "Configurer les lecteurs" #: src/components/SideBar/DriveSwitcher.tsx msgid "Load drives not displayed in this list." @@ -1911,17 +1907,14 @@ msgstr "Rien à afficher" msgid "Drive Configuration" msgstr "Configuration du lecteur" -#: src/routes/SettingsServer/index.tsx -msgid "Current Drive" -msgstr "Lecteur actuel" +#~ msgid "Current Drive" +#~ msgstr "Lecteur actuel" -#: src/routes/SettingsServer/index.tsx -msgid "Saved" -msgstr "Enregistré" +#~ msgid "Saved" +#~ msgstr "Enregistré" -#: src/routes/SettingsServer/index.tsx -msgid "Other" -msgstr "Autre" +#~ msgid "Other" +#~ msgstr "Autre" #: src/views/Article/ArticleDescription.tsx msgid "<0/> Add Content" @@ -2250,9 +2243,8 @@ msgstr "Exporter au format CSV" msgid "Attached File" msgstr "Fichier joint" -#: src/chunks/AI/AIChatMessageParts/UserMessage.tsx -msgid "You" -msgstr "Vous" +#~ msgid "You" +#~ msgstr "Vous" #: src/chunks/AI/AIChatMessageParts/ReasoningMessage.tsx msgid "Thinking..." @@ -2470,6 +2462,7 @@ msgstr "Initialisation de la ressource" #: src/chunks/RTE/CollaborativeEditor.tsx #: src/components/forms/NewForm/NewFormTitle.tsx +#: src/hooks/useCreateAndNavigate.ts #: src/views/Plugin/AssignRights.tsx msgid "Resource" msgstr "Ressource" @@ -2651,13 +2644,11 @@ msgstr "Ajouter une propriété externe" msgid "Add" msgstr "Ajouter" -#: src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx -msgid "A column in a table" -msgstr "Une colonne dans un tableau" +#~ msgid "A column in a table" +#~ msgstr "Une colonne dans un tableau" -#: src/chunks/TablePage/PropertyForm/NewPropertyDialog.tsx -msgid "New <0/> Column" -msgstr "Nouvelle <0/> Colonne" +#~ msgid "New <0/> Column" +#~ msgstr "Nouvelle <0/> Colonne" #: src/chunks/TablePage/PropertyForm/TextPropertyForm.tsx msgid "Text Format:" @@ -2823,9 +2814,8 @@ msgstr "" msgid "Shortname" msgstr "Shortname" -#: src/components/SideBar/AppMenu.tsx -msgid "Login" -msgstr "Connexion" +#~ msgid "Login" +#~ msgstr "Connexion" #: src/components/SideBar/AppMenu.tsx msgid "See and edit the current Agent / User (u)" @@ -3300,18 +3290,16 @@ msgstr "Aucun plugin installé" msgid "Sign Out" msgstr "Se déconnecter" -#. placeholder {0}: ' ' -#. placeholder {1}: "'s" -#: src/routes/SettingsAgent.tsx -msgid "" -"You can create your own Agent by hosting an{0} <0>atomic-server . Alternatively, you can use an Invite to get a guest Agent on\n" -"someone else{1} Atomic Server." -msgstr "Vous pouvez créer votre propre agent en hébergeant un <0>atomic-server. Vous pouvez également utiliser une invitation pour obtenir un agent invité sur le serveur atomique de quelqu'un d'autre." +#~ msgid "" +#~ "You can create your own Agent by hosting an{0} <0>atomic-server . Alternatively, you can use an Invite to get a guest Agent on\n" +#~ "someone else{1} Atomic Server." +#~ msgstr "Vous pouvez créer votre propre agent en hébergeant un <0>atomic-server. Vous pouvez également utiliser une invitation pour obtenir un agent invité sur le serveur atomique de quelqu'un d'autre." #: src/views/InvitePage.tsx msgid "Agent created!" msgstr "Agent créé !" +#: src/views/InvitePage.tsx #: src/views/InvitePage.tsx msgid "Continue" msgstr "Continuer" @@ -3320,12 +3308,11 @@ msgstr "Continuer" msgid "Copy secret to continue" msgstr "Copier le secret pour continuer" -#: src/views/InvitePage.tsx -msgid "" -"IMPORTANT! Below is your agent secret, you use this to login. Save\n" -"it somewhere safe, the secret will not be show again and if you\n" -"lose it you will not be able to access this user again." -msgstr "IMPORTANT ! Ci-dessous se trouve votre secret d'agent, vous l'utilisez pour vous connecter. Sauvegardez-le dans un endroit sûr, le secret ne sera plus affiché et si vous le perdez, vous ne pourrez plus accéder à cet utilisateur." +#~ msgid "" +#~ "IMPORTANT! Below is your agent secret, you use this to login. Save\n" +#~ "it somewhere safe, the secret will not be show again and if you\n" +#~ "lose it you will not be able to access this user again." +#~ msgstr "IMPORTANT ! Ci-dessous se trouve votre secret d'agent, vous l'utilisez pour vous connecter. Sauvegardez-le dans un endroit sûr, le secret ne sera plus affiché et si vous le perdez, vous ne pourrez plus accéder à cet utilisateur." #: src/views/InvitePage.tsx msgid "Enter a name" @@ -3339,11 +3326,10 @@ msgstr "Nom de l'agent" msgid "This drive is private, sign in to view it" msgstr "Ce lecteur est privé, connectez-vous pour le consulter" -#: src/routes/SettingsAgent.tsx -msgid "" -"An Agent is a user, consisting of a Subject (its URL) and Private Key.\n" -"Together, these can be used to edit data and sign Commits." -msgstr "Un agent est un utilisateur, composé d'un sujet (son URL) et d'une clé privée. Ensemble, ceux-ci peuvent être utilisés pour modifier des données et signer des Commits." +#~ msgid "" +#~ "An Agent is a user, consisting of a Subject (its URL) and Private Key.\n" +#~ "Together, these can be used to edit data and sign Commits." +#~ msgstr "Un agent est un utilisateur, composé d'un sujet (son URL) et d'une clé privée. Ensemble, ceux-ci peuvent être utilisés pour modifier des données et signer des Commits." #: src/views/Plugin/AssignRights.tsx msgid "Pick a resource" @@ -3424,3 +3410,342 @@ msgid "" "<0/> wants to modify a resource that is not\n" "contained in the current scope." msgstr "<0/> veut modifier une ressource qui n'est pas contenue dans la portée actuelle." + +#: src/routes/Search/SearchRoute.tsx +msgid "Searching for <0/>..." +msgstr "Recherche de <0/>..." + +#: src/views/InvitePage.tsx +msgid "Failed to add invited drive to agent" +msgstr "Échec de l'ajout du disque invité à l'agent" + +#: src/views/InvitePage.tsx +msgid "Failed to persist agent after accepting invite" +msgstr "Échec de la persistance de l'agent après avoir accepté l'invitation" + +#: src/views/InvitePage.tsx +msgid "Invite accepted, but no destination was returned." +msgstr "Invitation acceptée, mais aucune destination n'a été renvoyée." + +#: src/components/forms/NewForm/SubjectField.tsx +msgid "The identifier of the resource. DID subjects are determined by the genesis commit signature." +msgstr "L'identifiant de la ressource. Les sujets DID sont déterminés par la signature de validation de la genèse." + +#: src/components/NetworkIndicator.tsx +msgid "Connection to server lost, reconnecting..." +msgstr "Connexion au serveur perdue, reconnexion en cours..." + +#~ msgid "Server connection lost." +#~ msgstr "" + +#: src/components/NetworkIndicator.tsx +msgid "No internet connection" +msgstr "Aucune connexion Internet" + +#: src/components/NetworkIndicator.tsx +msgid "Server connection lost — reconnecting…" +msgstr "Connexion au serveur perdue — reconnexion…" + +#: src/views/InvitePage.tsx +msgid "" +"IMPORTANT! Below is your agent secret, you use this to login.\n" +"Save it somewhere safe, the secret will not be show again and if\n" +"you lose it you will not be able to access this user again." +msgstr "" +"IMPORTANT ! Ci-dessous se trouve votre secret d'agent, vous l'utilisez pour vous connecter.\n" +"Gardez-le en lieu sûr, le secret ne sera plus affiché et si\n" +"vous le perdez, vous ne pourrez plus accéder à cet utilisateur." + +#: src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx +msgid "Subdomain" +msgstr "Sous-domaine" + +#~ msgid "This secret does not contain an initial drive link. You might need to set it up manually or create a new account." +#~ msgstr "" + +#~ msgid "Initial Drive" +#~ msgstr "" + +#~ msgid "My first decentralized drive" +#~ msgstr "" + +#~ msgid "Welcome to Atomic Server" +#~ msgstr "" + +#~ msgid "This server node is currently uninitialized for <0/>." +#~ msgstr "" + +#~ msgid "I already have an account (Paste Secret)" +#~ msgstr "" + +#~ msgid "Create a new Account & Drive" +#~ msgstr "" + +#~ msgid "Paste your Agent Secret" +#~ msgstr "" + +#~ msgid "Back" +#~ msgstr "" + +#: src/views/OnboardingPage.tsx +msgid "Import & Connect" +msgstr "Importer et connecter" + +#~ msgid "Create a New Identity" +#~ msgstr "" + +#~ msgid "This will generate a new self-sovereign Agent (your ID) and a decentralized Drive (your data storage) anchored to this server." +#~ msgstr "" + +#: src/components/NewIdentitySection.tsx +msgid "Generating..." +msgstr "Génération en cours..." + +#~ msgid "Generate Identity" +#~ msgstr "" + +#~ msgid "Success! Your Identity is ready." +#~ msgstr "" + +#~ msgid "<0>IMPORTANT: Save this secret key. It is the only way to access your data if you clear your browser cache or sign in from another device." +#~ msgstr "" + +#~ msgid "Finish Setup" +#~ msgstr "" + +#~ msgid "or" +#~ msgstr "" + +#~ msgid "Active Server" +#~ msgstr "" + +#. placeholder {0}: s +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Connect via {0}" +msgstr "Se connecter via {0}" + +#: src/routes/SettingsServer/ServersCard.tsx +msgid "No known servers" +msgstr "Aucun serveur connu" + +#: src/routes/SettingsServer/index.tsx +msgid "Gateway Server" +msgstr "Serveur passerelle" + +#~ msgid "The gateway server is used to resolve DIDs and fetch data from the network." +#~ msgstr "" + +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Active Gateway" +msgstr "Passerelle active" + +#: src/routes/SettingsServer/index.tsx +msgid "Saved Drives" +msgstr "Lecteurs enregistrés" + +#: src/routes/SettingsServer/index.tsx +msgid "Custom Drive URL" +msgstr "URL de lecteur personnalisée" + +#: src/routes/SettingsServer/index.tsx +msgid "Enter a Drive DID or URL" +msgstr "Saisissez un DID ou une URL de lecteur" + +#: src/routes/SettingsServer/index.tsx +msgid "Add Gateway by URL" +msgstr "Ajouter une passerelle par URL" + +#: src/routes/SettingsServer/index.tsx +msgid "Set Active" +msgstr "Définir comme actif" + +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Configure" +msgstr "Configurer" + +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Gateway (Locked to Drive)" +msgstr "Passerelle (verrouillée sur le lecteur)" + +#: src/components/SideBar/DriveSwitcher.tsx +msgid "Cannot change gateway for HTTP drives" +msgstr "Impossible de changer de passerelle pour les lecteurs HTTP" + +#. placeholder {0}: ' ' +#: src/routes/SettingsServer/index.tsx +msgid "" +"The gateway is currently locked to{0} <0/> because you are using an\n" +"HTTP-based drive." +msgstr "" +"La passerelle est actuellement verrouillée sur{0} <0/> car vous utilisez un lecteur\n" +"basé sur HTTP." + +#: src/routes/SettingsServer/index.tsx +msgid "" +"The gateway server is used to resolve DIDs and fetch data from the\n" +"network." +msgstr "" +"Le serveur passerelle est utilisé pour résoudre les DID et récupérer les données du\n" +"réseau." + +#: src/routes/SettingsServer/index.tsx +msgid "Locked" +msgstr "Verrouillé" + +#~ msgid "This secret does not contain an initial drive, and no drives were found on this server. Please create a new account." +#~ msgstr "" + +#~ msgid "Initial drive for {0}" +#~ msgstr "" + +#: src/views/OnboardingPage.tsx +msgid "Welcome to Atomic Data" +msgstr "Bienvenue dans Atomic Data" + +#~ msgid "Create a new Identity & Drive" +#~ msgstr "" + +#~ msgid "Login via existing server" +#~ msgstr "" + +#~ msgid "If you have an Atomic Data secret from another device or server, paste it here to anchor your identity to this node." +#~ msgstr "" + +#~ msgid "This will generate a new self-sovereign <0>Agent (your global ID) and a decentralized <1>Drive (your storage) anchored to this server." +#~ msgstr "" + +#~ msgid "Create Identity" +#~ msgstr "" + +#. placeholder {0}: ' ' +#: src/views/OnboardingPage.tsx +msgid "This server node is currently uninitialized for{0} <0/>." +msgstr "Ce nœud de serveur n'est pas encore initialisé pour{0} <0/>." + +#~ msgid "" +#~ "If you have an Atomic Data secret from another device or\n" +#~ "server, paste it here to anchor your identity to this node." +#~ msgstr "" + +#~ msgid "This will generate a new self-sovereign <0>Agent{0} (your global ID) and a decentralized <1>Drive{1} (your storage) anchored to this server." +#~ msgstr "" + +#~ msgid "" +#~ "<0>IMPORTANT: Save this secret key. It is the\n" +#~ "only way to access your data if you clear your browser cache\n" +#~ "or sign in from another device." +#~ msgstr "" + +#~ msgid "Option 1: Create a New Identity" +#~ msgstr "" + +#~ msgid "" +#~ "This will generate a new self-sovereign{0} <0>Agent (your global ID) and a decentralized{1} <1>Drive (your storage) anchored to this\n" +#~ "server." +#~ msgstr "" + +#~ msgid "Option 2: Use an existing Identity" +#~ msgstr "" + +#: src/views/OnboardingPage.tsx +msgid "" +"Paste your Atomic Data secret key below to connect your\n" +"existing identity to this node." +msgstr "" +"Collez votre clé secrète de données atomiques ci-dessous pour connecter votre\n" +"identité existante à ce nœud." + +#: src/components/NewIdentitySection.tsx +msgid "Your new identity is ready" +msgstr "Votre nouvelle identité est prête" + +#~ msgid "" +#~ "<0>IMPORTANT: Save this secret key. It is the only\n" +#~ "way to access your data if you clear your browser cache or sign in\n" +#~ "from another device." +#~ msgstr "" + +#~ msgid "Done" +#~ msgstr "" + +#: src/components/NewIdentitySection.tsx +#: src/routes/SettingsAgent.tsx +msgid "Create a new identity" +msgstr "Créer une nouvelle identité" + +#: src/components/NewIdentitySection.tsx +#: src/routes/SettingsAgent.tsx +msgid "Generate a new self-sovereign Agent and Drive on this server." +msgstr "Générer un nouvel Agent et Drive auto-souverains sur ce serveur." + +#: src/components/NewIdentitySection.tsx +#: src/routes/SettingsAgent.tsx +msgid "Create new identity" +msgstr "Créer une nouvelle identité" + +#: src/routes/SettingsAgent.tsx +msgid "Sign in with existing secret" +msgstr "Se connecter avec un secret existant" + +#~ msgid "Don{0}t have a server yet? You can use an{1} <0>atomic-server{2} or an Invite from someone else{3} server." +#~ msgstr "" + +#: src/views/OnboardingPage.tsx +msgid "Use an existing identity" +msgstr "Utiliser une identité existante" + +#: src/components/NewIdentitySection.tsx +msgid "" +"<0>IMPORTANT: Save this secret key. It is the only way\n" +"to access your data if you clear your browser cache or sign in from\n" +"another device." +msgstr "<0>IMPORTANT : Enregistrez cette clé secrète. C'est le seul moyen d'accéder à vos données si vous videz le cache de votre navigateur ou si vous vous connectez depuis un autre appareil." + +#~ msgid "Are you sure you{0}ve stored this secret somewhere safe? You cannot recover it if you lose it." +#~ msgstr "Êtes-vous sûr d'avoir stocké ce secret en lieu sûr ? Vous ne pourrez pas le récupérer si vous le perdez." + +#: src/components/NewIdentitySection.tsx +msgid "Copy the secret key to continue" +msgstr "Copiez la clé secrète pour continuer" + +#: src/views/OnboardingPage.tsx +msgid "Yes, I've stored it safely" +msgstr "Oui, je l'ai stocké en toute sécurité" + +#: src/components/SideBar/AppMenu.tsx +#: src/routes/SettingsAgent.tsx +msgid "Login / New User" +msgstr "Connexion / Nouvel utilisateur" + +#~ msgid "This host is not bound to a Drive yet:{0} <0/>." +#~ msgstr "" + +#. placeholder {0}: ' ' +#: src/views/ErrorPage.tsx +msgid "If this host has not been bound to a Drive yet, continue at{0} <0>the onboarding page ." +msgstr "" + +#. placeholder {0}: "'" +#: src/components/NewIdentitySection.tsx +msgid "" +"Are you sure you{0}ve stored this secret somewhere safe? You\n" +"cannot recover it if you lose it." +msgstr "" + +#~ msgid "Setting up..." +#~ msgstr "" + +#: src/components/SideBar/AppMenu.tsx +msgid "Dev Drive" +msgstr "" + +#~ msgid "Create a new agent + drive on localhost:9883 and switch to it" +#~ msgstr "" + +#: src/routes/DevDriveRoute.tsx +msgid "Setting up dev drive..." +msgstr "" + +#: src/components/SideBar/AppMenu.tsx +msgid "Create a fresh agent + drive on localhost:9883" +msgstr "" diff --git a/browser/data-browser/src/routes/DataRoute.tsx b/browser/data-browser/src/routes/DataRoute.tsx index c1434e33..0b882cc0 100644 --- a/browser/data-browser/src/routes/DataRoute.tsx +++ b/browser/data-browser/src/routes/DataRoute.tsx @@ -1,5 +1,10 @@ import { useState, type JSX } from 'react'; -import { useResource, signRequest, HeadersObject } from '@tomic/react'; +import { + useResource, + signRequest, + HeadersObject, + useStore, +} from '@tomic/react'; import AllProps from '../components/AllProps'; import { ContainerNarrow } from '../components/Containers'; @@ -40,6 +45,7 @@ function Data(): JSX.Element { const [err, setErr] = useState(undefined); const { agent } = useSettings(); const navigate = useNavigateWithTransition(); + const store = useStore(); if (!subject) { No subject passed; @@ -58,17 +64,25 @@ function Data(): JSX.Element { } async function fetchAs(contentType: string) { + if (!subject) return; + let headers: HeadersObject = {}; headers['Accept'] = contentType; + let url = subject; + + if (subject.startsWith('did:')) { + url = `${store.getServerUrl()}/did?subject=${encodeURIComponent(subject)}`; + } + if (agent) { - headers = await signRequest(subject!, agent, headers); + headers = await signRequest(url, agent, headers); } setTextResponseLoading(true); try { - const resp = await window.fetch(subject!, { headers: headers }); + const resp = await window.fetch(url, { headers: headers }); const body = await resp.text(); setTextResponseLoading(false); setTextResponse(body); diff --git a/browser/data-browser/src/routes/DevDriveRoute.tsx b/browser/data-browser/src/routes/DevDriveRoute.tsx new file mode 100644 index 00000000..65c79dab --- /dev/null +++ b/browser/data-browser/src/routes/DevDriveRoute.tsx @@ -0,0 +1,17 @@ +import { createLazyRoute } from '@tanstack/react-router'; +import { useEffect } from 'react'; +import { useDevDrive } from '../hooks/useDevDrive'; + +const DevDriveRoute: React.FC = () => { + const { createDevDrive } = useDevDrive(); + + useEffect(() => { + createDevDrive(); + }, []); + + return

Setting up dev drive...

; +}; + +export const devDriveRouteLazy = createLazyRoute('/app/dev-drive')({ + component: DevDriveRoute, +}); diff --git a/browser/data-browser/src/routes/NewResource/NewRoute.tsx b/browser/data-browser/src/routes/NewResource/NewRoute.tsx index 058dc370..98456cf9 100644 --- a/browser/data-browser/src/routes/NewResource/NewRoute.tsx +++ b/browser/data-browser/src/routes/NewResource/NewRoute.tsx @@ -1,5 +1,5 @@ import { useResource, core } from '@tomic/react'; -import { useCallback, type JSX } from 'react'; +import { Fragment, useCallback, type JSX } from 'react'; import { constructOpenURL } from '../../helpers/navigation'; import { @@ -123,13 +123,13 @@ function NewResourceSelector() { /> {showTemplates && ( - <> +

Templates

- +
)} diff --git a/browser/data-browser/src/routes/OnboardingRoute.tsx b/browser/data-browser/src/routes/OnboardingRoute.tsx new file mode 100644 index 00000000..4173747f --- /dev/null +++ b/browser/data-browser/src/routes/OnboardingRoute.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { createRoute } from '@tanstack/react-router'; +import { appRoute } from './RootRoutes'; +import { pathNames } from './paths'; +import { OnboardingPage } from '../views/OnboardingPage'; + +export const OnboardingRoute = createRoute({ + path: pathNames.onboarding, + component: () => , + getParentRoute: () => appRoute, +}); diff --git a/browser/data-browser/src/routes/RootRoutes.tsx b/browser/data-browser/src/routes/RootRoutes.tsx index a21a4a20..6e9247fd 100644 --- a/browser/data-browser/src/routes/RootRoutes.tsx +++ b/browser/data-browser/src/routes/RootRoutes.tsx @@ -33,10 +33,7 @@ const TopRouteComponent: React.FC = () => { // We use the useLocation hook to get the pathname and searchStr because the window.location is not reactive. const subject = window.location.origin + pathname + searchStr; - // Remove trailing slash from subject - const cleanedSubject = subject.endsWith('/') ? subject.slice(0, -1) : subject; - - return ; + return ; }; export const topRoute = createRoute({ diff --git a/browser/data-browser/src/routes/Router.tsx b/browser/data-browser/src/routes/Router.tsx index bebd03f3..f0fbd8b2 100644 --- a/browser/data-browser/src/routes/Router.tsx +++ b/browser/data-browser/src/routes/Router.tsx @@ -18,6 +18,19 @@ import { unavailableLazyRoute } from './UnavailableLazyRoute'; import { ImportRoute } from './ImportRoute'; import { HistoryRoute } from './History/HistoryRoute'; import { LinkOpenRouter } from './LinkOpenRouter'; +import { OnboardingRoute } from './OnboardingRoute'; + +const DevDriveRoute = createRoute({ + getParentRoute: () => appRoute, + path: pathNames.devDrive, + // @ts-expect-error - Mismatch between unavailable route name and dev-drive route name +}).lazy(() => { + if (isDev()) { + return import('./DevDriveRoute').then(mod => mod.devDriveRouteLazy); + } else { + return Promise.resolve(unavailableLazyRoute); + } +}); const PruneTestsRoute = createRoute({ getParentRoute: () => appRoute, @@ -54,6 +67,7 @@ const routeTree = rootRoute.addChildren({ DataRoute, EditRoute, ImportRoute, + OnboardingRoute, ShareRoute, AboutRoute, TokenRoute, @@ -61,6 +75,7 @@ const routeTree = rootRoute.addChildren({ NewRoute, PruneTestsRoute, SandboxRoute, + DevDriveRoute, LinkOpenRouter, }), topRoute, diff --git a/browser/data-browser/src/routes/Search/SearchRoute.tsx b/browser/data-browser/src/routes/Search/SearchRoute.tsx index 50f3f476..8058fe00 100644 --- a/browser/data-browser/src/routes/Search/SearchRoute.tsx +++ b/browser/data-browser/src/routes/Search/SearchRoute.tsx @@ -118,10 +118,6 @@ export function Search(): JSX.Element { heading = 'Loading results...'; } - if (results.length > 0) { - heading = undefined; - } - const showHelperMessage = !query && filterIsEmpty; useOnValueChange(() => { @@ -141,6 +137,10 @@ export function Search(): JSX.Element { {heading ? ( heading + ) : loading ? ( + <> + Searching for {query}... + ) : ( <> {results.length}{' '} @@ -166,7 +166,11 @@ export function Search(): JSX.Element { )} - + {results.map((subject, index) => ( { const { agent, setAgent } = useSettings(); const [error, setError] = useState(undefined); const navigate = useNavigateWithTransition(); + const [showCreate, setShowCreate] = useState(false); function handleSignOut() { setAgent(undefined); @@ -41,7 +43,6 @@ const SettingsAgent: React.FunctionComponent = () => { saveAgentToIDB(undefined); } - /** When the Secret updates, parse it and try if the */ async function handleUpdateSecret(updateSecret: string) { setError(undefined); @@ -50,8 +51,6 @@ const SettingsAgent: React.FunctionComponent = () => { setAgent(newAgent); saveAgentToIDB(updateSecret); - // This will fail and throw if the agent is not public, which is by default - // await newAgent.checkPublicKey(); } catch (e) { const err = new Error('Invalid secret. ' + e); setError(err); @@ -61,12 +60,10 @@ const SettingsAgent: React.FunctionComponent = () => { return (
-

User Settings

-

- An Agent is a user, consisting of a Subject (its URL) and Private Key. - Together, these can be used to edit data and sign Commits. -

- {agent ? ( +

{agent ? 'User Settings' : 'Login / New User'}

+ {showCreate ? ( + setShowCreate(false)} /> + ) : agent ? ( {agent.subject?.startsWith('http://localhost') && ( @@ -98,38 +95,50 @@ const SettingsAgent: React.FunctionComponent = () => { ) : ( - <> -

- You can create your own Agent by hosting an{' '} - - atomic-server - - . Alternatively, you can use an Invite to get a guest Agent on - someone else{"'s"} Atomic Server. -

- - - - handleUpdateSecret(e.target.value)} - type='password' - disabled={agent !== undefined} - name='secret' - id='current-password' - autoComplete='current-password' - spellCheck='false' - /> - - - + + +

Create a new identity

+

+ Generate a new self-sovereign Agent and Drive on this server. +

+ +
+ + +

Sign in with existing secret

+ + + + handleUpdateSecret(e.target.value)} + type='password' + disabled={agent !== undefined} + name='secret' + id='current-password' + autoComplete='current-password' + spellCheck='false' + /> + + +
+
)}
); }; + +const Divider = styled.hr` + width: 100%; + border: none; + border-top: 1px solid ${p => p.theme.colors.bg2}; + margin: 0; +`; diff --git a/browser/data-browser/src/routes/SettingsServer/ServersCard.tsx b/browser/data-browser/src/routes/SettingsServer/ServersCard.tsx new file mode 100644 index 00000000..401821f1 --- /dev/null +++ b/browser/data-browser/src/routes/SettingsServer/ServersCard.tsx @@ -0,0 +1,50 @@ +import { Card, CardInsideFull, CardRow } from '../../components/Card'; +import { styled } from 'styled-components'; +import { useSettings } from '../../helpers/AppSettings'; +import { DriveRow } from './DriveRow'; + +import type { JSX } from 'react'; + +export interface ServerCardProps { + servers: string[]; + onServerSelect: (server: string) => void; + onServerRemove: (server: string) => void; + disabled?: boolean; +} + +export function ServersCard({ + servers, + onServerSelect, + onServerRemove, + disabled, +}: ServerCardProps): JSX.Element { + const { baseURL } = useSettings(); + + if (servers.length === 0) { + return No known servers; + } + + return ( + + + {servers.map((origin, i) => { + return ( + + + + ); + })} + + + ); +} + +const ContainerCard = styled(Card)` + container-type: inline-size; + padding-block: 0; +`; diff --git a/browser/data-browser/src/routes/SettingsServer/index.tsx b/browser/data-browser/src/routes/SettingsServer/index.tsx index c84b7423..56009ab8 100644 --- a/browser/data-browser/src/routes/SettingsServer/index.tsx +++ b/browser/data-browser/src/routes/SettingsServer/index.tsx @@ -10,6 +10,7 @@ import { ContainerWide } from '../../components/Containers'; import { Column, Row } from '../../components/Row'; import { useDriveHistory } from '../../hooks/useDriveHistory'; import { DrivesCard } from './DrivesCard'; +import { ServersCard } from './ServersCard'; import { styled } from 'styled-components'; import { useSavedDrives } from '../../hooks/useSavedDrives'; import { constructOpenURL } from '../../helpers/navigation'; @@ -19,6 +20,8 @@ import { useNavigateWithTransition } from '../../hooks/useNavigateWithTransition import { createRoute } from '@tanstack/react-router'; import { pathNames } from '../paths'; import { appRoute } from '../RootRoutes'; +import { serverURLStorage } from '../../helpers/serverURLStorage'; +import { isURL } from '../../helpers/isURL'; export const ServerSettingsRoute = createRoute({ path: pathNames.serverSettings, @@ -28,63 +31,135 @@ export const ServerSettingsRoute = createRoute({ function SettingsServer(): JSX.Element { const currentDriveId = useId(); - const { drive: baseURL, setDrive: setBaseURL } = useSettings(); + const currentServerId = useId(); + const { drive, setDrive, baseURL, setServer } = useSettings(); const navigate = useNavigateWithTransition(); - const [baseUrlInput, setBaseUrlInput] = useState(baseURL); - const [baseUrlErr, setErrBaseUrl] = useState(); + + const isHttpDrive = isURL(drive); + + const [driveInput, setDriveInput] = useState(drive); + const [driveErr, setDriveErr] = useState(); + + const [serverInput, setServerInput] = useState(baseURL); + const [serverErr, setServerErr] = useState(); const [savedDrives] = useSavedDrives(); + const [knownServers, setKnownServers] = useState( + serverURLStorage.getKnownServers(), + ); const [history, addDriveToHistory, removeFromHistory] = useDriveHistory(savedDrives); - function handleSetBaseUrl(url: string) { + function handleSetDrive(url: string) { try { - setBaseURL(url); - setBaseUrlInput(url); + setDrive(url); + setDriveInput(url); addDriveToHistory(url); navigate(constructOpenURL(url)); } catch (e) { - setErrBaseUrl(e); + setDriveErr(e); + } + } + + function handleSetServer(url: string) { + try { + setServer(url); + setServerInput(url); + setKnownServers(serverURLStorage.getKnownServers()); + } catch (e) { + setServerErr(e); } } + function handleRemoveServer(url: string) { + serverURLStorage.removeKnownServer(url); + setKnownServers(serverURLStorage.getKnownServers()); + } + return (
Drive Configuration - Current Drive + + Saved Drives + handleSetDrive(subject)} + /> + + Custom Drive URL setBaseUrlInput(e.target.value)} + data-testid='drive-url-input' + value={driveInput} + onChange={e => setDriveInput(e.target.value)} + placeholder='Enter a Drive DID or URL' /> - {baseUrlErr && {baseUrlErr?.message}} - Saved - handleSetBaseUrl(subject)} - /> - Other + {driveErr && {driveErr?.message}} + + History handleSetBaseUrl(subject)} + onDriveSelect={subject => handleSetDrive(subject)} onDriveRemove={subject => removeFromHistory(subject)} /> + + Gateway Server + {isHttpDrive ? ( +

+ The gateway is currently locked to{' '} + {new URL(drive).origin} because you are using an + HTTP-based drive. +

+ ) : ( +

+ The gateway server is used to resolve DIDs and fetch data from the + network. +

+ )} + + + + Add Gateway by URL + + + setServerInput(e.target.value)} + placeholder='https://example.com' + /> + + + + {serverErr && {serverErr?.message}}
diff --git a/browser/data-browser/src/routes/paths.tsx b/browser/data-browser/src/routes/paths.tsx index d918eb03..9abc0264 100644 --- a/browser/data-browser/src/routes/paths.tsx +++ b/browser/data-browser/src/routes/paths.tsx @@ -15,12 +15,14 @@ export const pathNames = { edit: '/edit', about: '/about', import: '/import', + onboarding: '/onboarding', history: '/history', allVersions: '/all-versions', sandbox: '/sandbox', fetchBookmark: '/fetch-bookmark', pruneTests: '/prunetests', linkOpenRouter: '/link-openrouter', + devDrive: '/dev-drive', } as const; export const paths = { agentSettings: `${pathNames.app}${pathNames.agentSettings}`, @@ -36,10 +38,12 @@ export const paths = { edit: `${pathNames.app}${pathNames.edit}`, about: `${pathNames.app}${pathNames.about}`, import: `${pathNames.app}${pathNames.import}`, + onboarding: `${pathNames.app}${pathNames.onboarding}`, history: `${pathNames.app}${pathNames.history}`, allVersions: `${pathNames.app}${pathNames.allVersions}`, sandbox: `${pathNames.app}${pathNames.sandbox}`, fetchBookmark: pathNames.fetchBookmark, pruneTests: `${pathNames.app}${pathNames.pruneTests}`, linkOpenRouter: `${pathNames.app}${pathNames.linkOpenRouter}`, + devDrive: `${pathNames.app}${pathNames.devDrive}`, } as const; diff --git a/browser/data-browser/src/styling.tsx b/browser/data-browser/src/styling.tsx index 82510cb9..7050cf9d 100644 --- a/browser/data-browser/src/styling.tsx +++ b/browser/data-browser/src/styling.tsx @@ -79,8 +79,10 @@ size.raw = (multiplier: number) => `${multiplier}rem`; /** Construct a StyledComponents theme object */ export const buildTheme = (darkMode: boolean, mainIn: string): DefaultTheme => { - const main = darkMode ? lighten(0.2, mainIn) : mainIn; - const complementaryIn = complement(mainIn); + // Guard against undefined during HMR re-initialization (e.g. useLocalStorage cold start) + const safeMain = mainIn || '#1b50d8'; + const main = darkMode ? lighten(0.2, safeMain) : safeMain; + const complementaryIn = complement(safeMain); const complementary = darkMode ? lighten(0.2, complementaryIn) : complementaryIn; diff --git a/browser/data-browser/src/views/Card/ResourceCardTitle.tsx b/browser/data-browser/src/views/Card/ResourceCardTitle.tsx index cbcac398..2d068755 100644 --- a/browser/data-browser/src/views/Card/ResourceCardTitle.tsx +++ b/browser/data-browser/src/views/Card/ResourceCardTitle.tsx @@ -26,7 +26,9 @@ export const ResourceCardTitle: FC< - {alternateTitle ?? resource.title} + + {alternateTitle ?? resource.title} + {children} diff --git a/browser/data-browser/src/views/ChatRoomPage.tsx b/browser/data-browser/src/views/ChatRoomPage.tsx index ef7308ee..dde0fd07 100644 --- a/browser/data-browser/src/views/ChatRoomPage.tsx +++ b/browser/data-browser/src/views/ChatRoomPage.tsx @@ -39,12 +39,22 @@ export function ChatRoomPage({ resource }: ResourcePageProps) { const inputRef = useRef(null); const [textAreaHight, setTextAreaHight] = useState(1); + const shouldAutoScroll = useRef(true); + const scrollToBottom = () => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }; + const handleScroll = () => { + const el = scrollRef.current; + if (!el) return; + + shouldAutoScroll.current = + el.scrollHeight - el.scrollTop - el.clientHeight < 100; + }; + const disableSend = newMessageVal.length === 0; /** Creates a message using the internal state */ @@ -57,10 +67,7 @@ export function ChatRoomPage({ resource }: ResourcePageProps) { e?.preventDefault(); if (!disableSend) { - const subject = store.createSubject(resource.subject); - const msgResource = await store.newResource({ - subject, parent: resource.subject, isA: dataBrowser.classes.message, propVals: { @@ -99,7 +106,27 @@ export function ChatRoomPage({ resource }: ResourcePageProps) { { enableOnTags: ['TEXTAREA'] }, [], ); - useEffect(scrollToBottom, [messages.length, resource]); + // Scroll to bottom when new messages arrive, and re-enable auto-scroll + useEffect(() => { + shouldAutoScroll.current = true; + scrollToBottom(); + }, [messages.length, resource]); + + // Continue scrolling as async message content loads and expands the container + useEffect(() => { + const content = scrollRef.current?.firstElementChild; + if (!content) return; + + const observer = new ResizeObserver(() => { + if (shouldAutoScroll.current) { + scrollToBottom(); + } + }); + + observer.observe(content); + + return () => observer.disconnect(); + }, []); const handleReply = useCallback( (subject: string) => { @@ -140,7 +167,7 @@ export function ChatRoomPage({ resource }: ResourcePageProps) { - + {isReplyTo && ( diff --git a/browser/data-browser/src/views/EndpointPage.tsx b/browser/data-browser/src/views/EndpointPage.tsx index 4d14efe9..da869c6f 100644 --- a/browser/data-browser/src/views/EndpointPage.tsx +++ b/browser/data-browser/src/views/EndpointPage.tsx @@ -1,6 +1,8 @@ +import React from 'react'; import { properties, Resource, + server, useArray, useResource, useStore, @@ -28,11 +30,12 @@ function EndpointPage({ resource }: EndpointProps): JSX.Element { const [description] = useString(resource, properties.description); const [parameters] = useArray(resource, properties.endpoint.parameters); const [results] = useArray(resource, properties.endpoint.results); + const isPost = resource.get(server.properties.isPost) === true; const virtualResource = useResource(undefined, { newResource: true }); const store = useStore(); const navigate = useNavigateWithTransition(); + const [hasQueried, setHasQueried] = React.useState(false); - /** Create the URL using the variables */ async function constructSubject(e?: React.SyntheticEvent) { e?.preventDefault(); const url = new URL(resource.subject); @@ -41,13 +44,22 @@ function EndpointPage({ resource }: EndpointProps): JSX.Element { parameters.map(async propUrl => { const val = virtualResource.get(propUrl); - if (val !== undefined) { + // Skip params that are unset or explicitly false (e.g. boolean flags). + if (val !== undefined && val !== false) { const fullprop = await store.getProperty(propUrl); url.searchParams.set(fullprop.shortname, val.toString()); } }), ); - navigate(constructOpenURL(url.href)); + + setHasQueried(true); + + if (isPost) { + const response = await store.postToServer(url.href); + navigate(constructOpenURL(response.subject)); + } else { + navigate(constructOpenURL(url.href)); + } } return ( @@ -65,15 +77,12 @@ function EndpointPage({ resource }: EndpointProps): JSX.Element { ); })} - + - {results && results.length === 0 ? ( -

No hits

- ) : ( - results.map(result => { - return ; - }) - )} + {hasQueried && results && results.length === 0 &&

No hits

} + {results.map(result => ( + + ))} ); } diff --git a/browser/data-browser/src/views/ErrorPage.tsx b/browser/data-browser/src/views/ErrorPage.tsx index fbe70c76..e5dc3e6f 100644 --- a/browser/data-browser/src/views/ErrorPage.tsx +++ b/browser/data-browser/src/views/ErrorPage.tsx @@ -9,6 +9,8 @@ import { ResourcePageProps } from './ResourcePage'; import { Column, Row } from '../components/Row'; import CrashPage from './CrashPage'; import { clearAllLocalData } from '../helpers/clearData'; +import { AtomicLink } from '../components/AtomicLink'; +import { paths } from '../routes/paths'; import type { JSX } from 'react'; @@ -17,7 +19,7 @@ import type { JSX } from 'react'; * for App wide errors. */ function ErrorPage({ resource }: ResourcePageProps): JSX.Element { - const { agent } = useSettings(); + const { agent, baseURL } = useSettings(); const store = useStore(); if (isUnauthorized(resource.error)) { @@ -54,6 +56,13 @@ function ErrorPage({ resource }: ResourcePageProps): JSX.Element {

Could not open {resource.subject}

+ {resource.subject === baseURL && ( +

+ If this host has not been bound to a Drive yet, continue at{' '} + the onboarding page + . +

+ )} diff --git a/browser/data-browser/src/views/OnboardingPage.tsx b/browser/data-browser/src/views/OnboardingPage.tsx new file mode 100644 index 00000000..be4fbf24 --- /dev/null +++ b/browser/data-browser/src/views/OnboardingPage.tsx @@ -0,0 +1,131 @@ +import React, { useState } from 'react'; +import { Agent, useStore } from '@tomic/react'; +import { saveAgentToIDB } from '../helpers/agentStorage'; +import { ContainerNarrow } from '../components/Containers'; +import { Column } from '../components/Row'; +import { Button } from '../components/Button'; +import { InputWrapper } from '../components/forms/InputStyles'; +import { styled } from 'styled-components'; +import { Main } from '../components/Main'; +import { useSettings } from '../helpers/AppSettings'; +import { NewIdentitySection } from '../components/NewIdentitySection'; + +const Card = styled.div` + background: ${props => props.theme.colors.bg}; + border: 1px solid ${props => props.theme.colors.bg2}; + padding: 2rem; + border-radius: 1rem; + box-shadow: 0 0 1rem rgba(0, 0, 0, 0.1); +`; + +const INITIAL_DRIVE = 'https://atomicdata.dev/properties/initialDrive'; + +export const OnboardingPage: React.FC = () => { + const store = useStore(); + const { baseURL, setAgent } = useSettings(); + const [secret, setSecret] = useState(''); + const [loading, setLoading] = useState(false); + + async function setupServer(driveDID: string) { + const resp = await store.postToServer( + `${baseURL}/setup`, + JSON.stringify({ [INITIAL_DRIVE]: driveDID }), + ); + + if (resp.error) { + throw resp.error; + } + } + + const handleImport = async () => { + setLoading(true); + + try { + const newAgent = await Agent.fromSecret(secret); + setAgent(newAgent); + await saveAgentToIDB(secret); + + let driveToMap = newAgent.initialDrive; + + if (!driveToMap) { + const drives = await store.getResource( + 'https://atomicdata.dev/properties/drives', + ); + // @ts-ignore + const driveList = + drives.get('https://atomicdata.dev/properties/drives') || []; + + if (driveList.length > 0) { + driveToMap = driveList[0]; + } + } + + if (driveToMap) { + await setupServer(driveToMap); + window.location.reload(); + } + } catch (e) { + store.notifyError(e); + } finally { + setLoading(false); + } + }; + + return ( +
+ + +

Welcome to Atomic Data

+

+ This server node is currently uninitialized for{' '} + {new URL(baseURL).host}. +

+ + + + window.location.reload()} + doneLabel="Yes, I've stored it safely" + /> + +
+ + +

Use an existing identity

+

+ Paste your Atomic Data secret key below to connect your + existing identity to this node. +

+ +